forked from tornado-packages/noble-curves
Add Montgomery curve
This commit is contained in:
parent
b92866d9b8
commit
6b0d9611a5
11
README.md
11
README.md
@ -2,7 +2,9 @@
|
||||
|
||||
Minimal, zero-dependency JS implementation of elliptic curve cryptography.
|
||||
|
||||
Implements Short Weierstrass curve with ECDSA signatures & Twisted Edwards curve with EdDSA signatures.
|
||||
- Short Weierstrass curve with ECDSA signatures
|
||||
- Twisted Edwards curve with EdDSA signatures
|
||||
- Montgomery curve for ECDH key agreement
|
||||
|
||||
To keep the package minimal, no curve definitions are provided out-of-box. Use `micro-curve-definitions` module:
|
||||
|
||||
@ -13,8 +15,7 @@ To keep the package minimal, no curve definitions are provided out-of-box. Use `
|
||||
|
||||
Future plans:
|
||||
|
||||
- Edwards and Montgomery curves
|
||||
- hash-to-curve standard
|
||||
- hash to curve standard
|
||||
- point indistinguishability
|
||||
- pairings
|
||||
|
||||
@ -45,8 +46,8 @@ npm install @noble/curves
|
||||
```
|
||||
|
||||
```ts
|
||||
import weierstrass from '@noble/curves/weierstrass'; // Short Weierstrass curve
|
||||
import twistedEdwards from '@noble/curves/edwards'; // Twisted Edwards curve
|
||||
import { weierstrass } from '@noble/curves/weierstrass'; // Short Weierstrass curve
|
||||
import { twistedEdwards } from '@noble/curves/edwards'; // Twisted Edwards curve
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { hmac } from '@noble/hashes/hmac';
|
||||
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { sha512 } from '@noble/hashes/sha512';
|
||||
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
|
||||
import { twistedEdwards } from '@noble/curves/edwards';
|
||||
import { montgomery } from '@noble/curves/montgomery';
|
||||
import { mod, pow2, isNegativeLE } from '@noble/curves/modular';
|
||||
|
||||
const ed25519P = BigInt(
|
||||
@ -35,6 +36,17 @@ function ed25519_pow_2_252_3(x: bigint) {
|
||||
// ^ To pow to (p+3)/8, multiply it by x.
|
||||
return { pow_p_5_8, b2 };
|
||||
}
|
||||
function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
|
||||
// Section 5: For X25519, in order to decode 32 random bytes as an integer scalar,
|
||||
// set the three least significant bits of the first byte
|
||||
bytes[0] &= 248; // 0b1111_1000
|
||||
// and the most significant bit of the last to zero,
|
||||
bytes[31] &= 127; // 0b0111_1111
|
||||
// set the second most significant bit of the last byte to 1
|
||||
bytes[31] |= 64; // 0b0100_0000
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Just in case
|
||||
export const ED25519_TORSION_SUBGROUP = [
|
||||
'0100000000000000000000000000000000000000000000000000000000000000',
|
||||
@ -65,16 +77,7 @@ const ED25519_DEF = {
|
||||
Gy: BigInt('46316835694926478169428394003475163141307993866256225615783033603165251855960'),
|
||||
hash: sha512,
|
||||
randomBytes,
|
||||
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
|
||||
// Section 5: For X25519, in order to decode 32 random bytes as an integer scalar,
|
||||
// set the three least significant bits of the first byte
|
||||
bytes[0] &= 248; // 0b1111_1000
|
||||
// and the most significant bit of the last to zero,
|
||||
bytes[31] &= 127; // 0b0111_1111
|
||||
// set the second most significant bit of the last byte to 1
|
||||
bytes[31] |= 64; // 0b0100_0000
|
||||
return bytes;
|
||||
},
|
||||
adjustScalarBytes,
|
||||
// dom2
|
||||
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
|
||||
// Constant-time, u/√v
|
||||
@ -96,16 +99,6 @@ const ED25519_DEF = {
|
||||
if (isNegativeLE(x, P)) x = mod(-x, P);
|
||||
return { isValid: useRoot1 || useRoot2, value: x };
|
||||
},
|
||||
// ECDH
|
||||
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
|
||||
a24: BigInt('121665'),
|
||||
montgomeryBits: 255, // n is 253 bits
|
||||
powPminus2: (x: bigint): bigint => {
|
||||
const P = ed25519P;
|
||||
// x^(p-2) aka x^(2^255-21)
|
||||
const { pow_p_5_8, b2 } = ed25519_pow_2_252_3(x);
|
||||
return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P);
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ed25519 = twistedEdwards(ED25519_DEF);
|
||||
@ -124,3 +117,18 @@ export const ed25519ph = twistedEdwards({
|
||||
domain: ed25519_domain,
|
||||
preHash: sha512,
|
||||
});
|
||||
|
||||
export const x25519 = montgomery({
|
||||
P: ed25519P,
|
||||
a24: BigInt('121665'),
|
||||
montgomeryBits: 255, // n is 253 bits
|
||||
nByteLength: 32,
|
||||
Gu: '0900000000000000000000000000000000000000000000000000000000000000',
|
||||
powPminus2: (x: bigint): bigint => {
|
||||
const P = ed25519P;
|
||||
// x^(p-2) aka x^(2^255-21)
|
||||
const { pow_p_5_8, b2 } = ed25519_pow_2_252_3(x);
|
||||
return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P);
|
||||
},
|
||||
adjustScalarBytes,
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/h
|
||||
import { PointType, twistedEdwards } from '@noble/curves/edwards';
|
||||
import { mod, pow2, invert } from '@noble/curves/modular';
|
||||
import { numberToBytesLE } from '@noble/curves/utils';
|
||||
import { montgomery } from '../../lib/montgomery.js';
|
||||
|
||||
const _0n = BigInt(0);
|
||||
|
||||
@ -32,6 +33,17 @@ function ed448_pow_Pminus3div4(x: bigint): bigint {
|
||||
return (pow2(b223, 223n, P) * b222) % P;
|
||||
}
|
||||
|
||||
function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
|
||||
// Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most
|
||||
// significant bit of the last byte to 1.
|
||||
bytes[0] &= 252; // 0b11111100
|
||||
// and the most significant bit of the last byte to 1.
|
||||
bytes[55] |= 128; // 0b10000000
|
||||
// NOTE: is is NOOP for 56 bytes scalars (X25519/X448)
|
||||
bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits)
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const ED448_DEF = {
|
||||
// Param: a
|
||||
a: BigInt(1),
|
||||
@ -59,16 +71,7 @@ const ED448_DEF = {
|
||||
// SHAKE256(dom4(phflag,context)||x, 114)
|
||||
hash: shake256_114,
|
||||
randomBytes,
|
||||
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
|
||||
// Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most
|
||||
// significant bit of the last byte to 1.
|
||||
bytes[0] &= 252; // 0b11111100
|
||||
// and the most significant bit of the last byte to 1.
|
||||
bytes[55] |= 128; // 0b10000000
|
||||
// NOTE: is is NOOP for 56 bytes scalars (X25519/X448)
|
||||
bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits)
|
||||
return bytes;
|
||||
},
|
||||
adjustScalarBytes,
|
||||
// dom4
|
||||
domain: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
|
||||
if (ctx.length > 255) throw new Error(`Context is too big: ${ctx.length}`);
|
||||
@ -101,34 +104,37 @@ const ED448_DEF = {
|
||||
// square root exists, and the decoding fails.
|
||||
return { isValid: mod(x2 * v, P) === u, value: x };
|
||||
},
|
||||
// ECDH
|
||||
// basePointU:
|
||||
// '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
||||
// The constant a24 is (156326 - 2) / 4 = 39081 for curve448/X448.
|
||||
} as const;
|
||||
|
||||
export const ed448 = twistedEdwards(ED448_DEF);
|
||||
// NOTE: there is no ed448ctx, since ed448 supports ctx by default
|
||||
export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 });
|
||||
|
||||
export const x448 = montgomery({
|
||||
a24: BigInt('39081'),
|
||||
montgomeryBits: 448,
|
||||
nByteLength: 57,
|
||||
P: ed448P,
|
||||
Gu: '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
||||
powPminus2: (x: bigint): bigint => {
|
||||
const P = ed448P;
|
||||
const Pminus3div4 = ed448_pow_Pminus3div4(x);
|
||||
const Pminus3 = pow2(Pminus3div4, BigInt(2), P);
|
||||
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
|
||||
},
|
||||
adjustScalarBytes,
|
||||
// The 4-isogeny maps between the Montgomery curve and this Edwards
|
||||
// curve are:
|
||||
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)
|
||||
// (x, y) = (4*v*(u^2 - 1)/(u^4 - 2*u^2 + 4*v^2 + 1),
|
||||
// -(u^5 - 2*u^3 - 4*u*v^2 + u)/
|
||||
// (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u))
|
||||
UfromPoint: (p: PointType) => {
|
||||
const P = ed448P;
|
||||
const { x, y } = p;
|
||||
if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
|
||||
const invX = invert(x * x, P); // x^2
|
||||
const u = mod(y * y * invX, P); // (y^2/x^2)
|
||||
return numberToBytesLE(u, 56);
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ed448 = twistedEdwards(ED448_DEF);
|
||||
// NOTE: there is no ed448ctx, since ed448 supports ctx by default
|
||||
export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 });
|
||||
// xyToU: (p: PointType) => {
|
||||
// const P = ed448P;
|
||||
// const { x, y } = p;
|
||||
// if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
|
||||
// const invX = invert(x * x, P); // x^2
|
||||
// const u = mod(y * y * invX, P); // (y^2/x^2)
|
||||
// return numberToBytesLE(u, 56);
|
||||
// },
|
||||
});
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { deepStrictEqual, throws } from 'assert';
|
||||
import { should } from 'micro-should';
|
||||
import * as fc from 'fast-check';
|
||||
import { ed25519, ed25519ctx, ed25519ph } from '../lib/ed25519.js';
|
||||
import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../lib/ed25519.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
||||
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
|
||||
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
|
||||
import { sha512 } from '@noble/hashes/sha512';
|
||||
|
||||
const ed = ed25519;
|
||||
const hex = bytesToHex;
|
||||
@ -455,7 +454,7 @@ const rfc7748Mul = [
|
||||
for (let i = 0; i < rfc7748Mul.length; i++) {
|
||||
const v = rfc7748Mul[i];
|
||||
should(`RFC7748: scalarMult (${i})`, () => {
|
||||
deepStrictEqual(hex(ed.montgomeryCurve.scalarMult(v.u, v.scalar)), v.outputU);
|
||||
deepStrictEqual(hex(x25519.scalarMult(v.u, v.scalar)), v.outputU);
|
||||
});
|
||||
}
|
||||
|
||||
@ -467,8 +466,8 @@ const rfc7748Iter = [
|
||||
for (let i = 0; i < rfc7748Iter.length; i++) {
|
||||
const { scalar, iters } = rfc7748Iter[i];
|
||||
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
||||
let k = ed.montgomeryCurve.BASE_POINT_U;
|
||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k];
|
||||
let k = x25519.Gu;
|
||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(u, k), k];
|
||||
deepStrictEqual(hex(k), scalar);
|
||||
});
|
||||
}
|
||||
@ -479,33 +478,33 @@ should('RFC7748 getSharedKey', () => {
|
||||
const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
|
||||
const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
|
||||
const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
|
||||
deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate)));
|
||||
deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate)));
|
||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared);
|
||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared);
|
||||
deepStrictEqual(alicePublic, hex(x25519.getPublicKey(alicePrivate)));
|
||||
deepStrictEqual(bobPublic, hex(x25519.getPublicKey(bobPrivate)));
|
||||
deepStrictEqual(hex(x25519.scalarMult(bobPublic, alicePrivate)), shared);
|
||||
deepStrictEqual(hex(x25519.scalarMult(alicePublic, bobPrivate)), shared);
|
||||
});
|
||||
|
||||
should('X25519/getSharedSecret() should be commutative', () => {
|
||||
for (let i = 0; i < 512; i++) {
|
||||
const asec = ed.utils.randomPrivateKey();
|
||||
const apub = ed.getPublicKey(asec);
|
||||
const bsec = ed.utils.randomPrivateKey();
|
||||
const bpub = ed.getPublicKey(bsec);
|
||||
try {
|
||||
deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||
} catch (error) {
|
||||
console.error('not commutative', { asec, apub, bsec, bpub });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
// should('X25519/getSharedSecret() should be commutative', () => {
|
||||
// for (let i = 0; i < 512; i++) {
|
||||
// const asec = ed.utils.randomPrivateKey();
|
||||
// const apub = ed.getPublicKey(asec);
|
||||
// const bsec = ed.utils.randomPrivateKey();
|
||||
// const bpub = ed.getPublicKey(bsec);
|
||||
// try {
|
||||
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||
// } catch (error) {
|
||||
// console.error('not commutative', { asec, apub, bsec, bpub });
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
should('X25519: should convert base point to montgomery using fromPoint', () => {
|
||||
deepStrictEqual(
|
||||
hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
||||
ed.montgomeryCurve.BASE_POINT_U
|
||||
);
|
||||
});
|
||||
// should('X25519: should convert base point to montgomery using fromPoint', () => {
|
||||
// deepStrictEqual(
|
||||
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
||||
// ed.montgomeryCurve.BASE_POINT_U
|
||||
// );
|
||||
// });
|
||||
|
||||
{
|
||||
const group = x25519vectors.testGroups[0];
|
||||
@ -514,7 +513,7 @@ should('X25519: should convert base point to montgomery using fromPoint', () =>
|
||||
should(`Wycheproof/X25519(${i}, ${v.result}) ${v.comment}`, () => {
|
||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
||||
try {
|
||||
const shared = hex(ed.montgomeryCurve.getSharedSecret(v.private, v.public));
|
||||
const shared = hex(x25519.scalarMult(v.public, v.private));
|
||||
deepStrictEqual(shared, v.shared, 'valid');
|
||||
} catch (e) {
|
||||
// We are more strict
|
||||
@ -525,7 +524,7 @@ should('X25519: should convert base point to montgomery using fromPoint', () =>
|
||||
} else if (v.result === 'invalid') {
|
||||
let failed = false;
|
||||
try {
|
||||
ed.montgomeryCurve.getSharedSecret(v.private, v.public);
|
||||
x25519.scalarMult(v.public, v.private);
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { deepStrictEqual, throws } from 'assert';
|
||||
import { should } from 'micro-should';
|
||||
import * as fc from 'fast-check';
|
||||
import { ed448, ed448ph } from '../lib/ed448.js';
|
||||
import { ed448, ed448ph, x448 } from '../lib/ed448.js';
|
||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
||||
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
|
||||
import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' };
|
||||
@ -478,7 +478,7 @@ const rfc7748Mul = [
|
||||
for (let i = 0; i < rfc7748Mul.length; i++) {
|
||||
const v = rfc7748Mul[i];
|
||||
should(`RFC7748: scalarMult (${i})`, () => {
|
||||
deepStrictEqual(hex(ed.montgomeryCurve.scalarMult(v.u, v.scalar)), v.outputU);
|
||||
deepStrictEqual(hex(x448.scalarMult(v.u, v.scalar)), v.outputU);
|
||||
});
|
||||
}
|
||||
|
||||
@ -498,8 +498,8 @@ const rfc7748Iter = [
|
||||
for (let i = 0; i < rfc7748Iter.length; i++) {
|
||||
const { scalar, iters } = rfc7748Iter[i];
|
||||
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
||||
let k = ed.montgomeryCurve.BASE_POINT_U;
|
||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k];
|
||||
let k = x448.Gu;
|
||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(u, k), k];
|
||||
deepStrictEqual(hex(k), scalar);
|
||||
});
|
||||
}
|
||||
@ -515,10 +515,10 @@ should('RFC7748 getSharedKey', () => {
|
||||
'3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609';
|
||||
const shared =
|
||||
'07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d';
|
||||
deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate)));
|
||||
deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate)));
|
||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared);
|
||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared);
|
||||
deepStrictEqual(alicePublic, hex(x448.getPublicKey(alicePrivate)));
|
||||
deepStrictEqual(bobPublic, hex(x448.getPublicKey(bobPrivate)));
|
||||
deepStrictEqual(hex(x448.scalarMult(bobPublic, alicePrivate)), shared);
|
||||
deepStrictEqual(hex(x448.scalarMult(alicePublic, bobPrivate)), shared);
|
||||
});
|
||||
|
||||
{
|
||||
@ -528,7 +528,7 @@ should('RFC7748 getSharedKey', () => {
|
||||
should(`Wycheproof/X448(${i}, ${v.result}) ${v.comment}`, () => {
|
||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
||||
try {
|
||||
const shared = hex(ed.montgomeryCurve.getSharedSecret(v.private, v.public));
|
||||
const shared = hex(x448.scalarMult(v.public, v.private));
|
||||
deepStrictEqual(shared, v.shared, 'valid');
|
||||
} catch (e) {
|
||||
// We are more strict
|
||||
@ -540,7 +540,7 @@ should('RFC7748 getSharedKey', () => {
|
||||
} else if (v.result === 'invalid') {
|
||||
let failed = false;
|
||||
try {
|
||||
ed.montgomeryCurve.getSharedSecret(v.private, v.public);
|
||||
x448.scalarMult(v.public, v.private);
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
}
|
||||
@ -550,27 +550,27 @@ should('RFC7748 getSharedKey', () => {
|
||||
}
|
||||
}
|
||||
|
||||
should('X448: should convert base point to montgomery using fromPoint', () => {
|
||||
deepStrictEqual(
|
||||
hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
||||
ed.montgomeryCurve.BASE_POINT_U
|
||||
);
|
||||
});
|
||||
// should('X448: should convert base point to montgomery using fromPoint', () => {
|
||||
// deepStrictEqual(
|
||||
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
||||
// ed.montgomeryCurve.BASE_POINT_U
|
||||
// );
|
||||
// });
|
||||
|
||||
should('X448/getSharedSecret() should be commutative', async () => {
|
||||
for (let i = 0; i < 512; i++) {
|
||||
const asec = ed.utils.randomPrivateKey();
|
||||
const apub = ed.getPublicKey(asec);
|
||||
const bsec = ed.utils.randomPrivateKey();
|
||||
const bpub = ed.getPublicKey(bsec);
|
||||
try {
|
||||
deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||
} catch (error) {
|
||||
console.error('not commutative', { asec, apub, bsec, bpub });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
// should('X448/getSharedSecret() should be commutative', async () => {
|
||||
// for (let i = 0; i < 512; i++) {
|
||||
// const asec = ed.utils.randomPrivateKey();
|
||||
// const apub = ed.getPublicKey(asec);
|
||||
// const bsec = ed.utils.randomPrivateKey();
|
||||
// const bpub = ed.getPublicKey(bsec);
|
||||
// try {
|
||||
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||
// } catch (error) {
|
||||
// console.error('not commutative', { asec, apub, bsec, bpub });
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
const VECTORS_RFC8032_CTX = [
|
||||
{
|
||||
|
15
package.json
15
package.json
@ -32,21 +32,26 @@
|
||||
},
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
"./edwards": {
|
||||
"types": "./lib/edwards.d.ts",
|
||||
"import": "./lib/esm/edwards.js",
|
||||
"default": "./lib/edwards.js"
|
||||
},
|
||||
"./modular": {
|
||||
"types": "./lib/modular.d.ts",
|
||||
"import": "./lib/esm/modular.js",
|
||||
"default": "./lib/modular.js"
|
||||
},
|
||||
"./montgomery": {
|
||||
"types": "./lib/montgomery.d.ts",
|
||||
"import": "./lib/esm/montgomery.js",
|
||||
"default": "./lib/montgomery.js"
|
||||
},
|
||||
"./weierstrass": {
|
||||
"types": "./lib/weierstrass.d.ts",
|
||||
"import": "./lib/esm/weierstrass.js",
|
||||
"default": "./lib/weierstrass.js"
|
||||
},
|
||||
"./edwards": {
|
||||
"types": "./lib/edwards.d.ts",
|
||||
"import": "./lib/esm/edwards.js",
|
||||
"default": "./lib/edwards.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./lib/utils.d.ts",
|
||||
"import": "./lib/esm/utils.js",
|
||||
|
230
src/edwards.ts
230
src/edwards.ts
@ -16,6 +16,7 @@ import {
|
||||
concatBytes,
|
||||
ensureBytes,
|
||||
numberToBytesLE,
|
||||
bytesToNumberLE,
|
||||
nLength,
|
||||
hashToPrivateScalar,
|
||||
} from './utils.js';
|
||||
@ -56,14 +57,6 @@ export type CurveType = {
|
||||
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
||||
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
|
||||
preHash?: CHash;
|
||||
// ECDH related
|
||||
// Other constants
|
||||
a24: bigint; // Related to d, but cannot be derived from it
|
||||
// ECDH bits (can be different from N bits)
|
||||
montgomeryBits?: number;
|
||||
basePointU?: string; // TODO: why not bigint?
|
||||
powPminus2?: (x: bigint) => bigint;
|
||||
UfromPoint?: (p: PointType) => Uint8Array;
|
||||
};
|
||||
|
||||
// We accept hex strings besides Uint8Array for simplicity
|
||||
@ -75,11 +68,11 @@ type PrivKey = Hex | bigint | number;
|
||||
function validateOpts(curve: CurveType) {
|
||||
if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen))
|
||||
throw new Error('Invalid hash function');
|
||||
for (const i of ['a', 'd', 'P', 'n', 'h', 'Gx', 'Gy', 'a24'] as const) {
|
||||
for (const i of ['a', 'd', 'P', 'n', 'h', 'Gx', 'Gy'] as const) {
|
||||
if (typeof curve[i] !== 'bigint')
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
}
|
||||
for (const i of ['nBitLength', 'nByteLength', 'montgomeryBits'] as const) {
|
||||
for (const i of ['nBitLength', 'nByteLength'] as const) {
|
||||
if (curve[i] === undefined) continue; // Optional
|
||||
if (!Number.isSafeInteger(curve[i]))
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
@ -87,21 +80,10 @@ function validateOpts(curve: CurveType) {
|
||||
for (const fn of ['randomBytes'] as const) {
|
||||
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
||||
}
|
||||
for (const fn of [
|
||||
'adjustScalarBytes',
|
||||
'domain',
|
||||
'uvRatio',
|
||||
'powPminus2',
|
||||
'UfromPoint',
|
||||
] as const) {
|
||||
for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio'] as const) {
|
||||
if (curve[fn] === undefined) continue; // Optional
|
||||
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
||||
}
|
||||
for (const i of ['basePointU'] as const) {
|
||||
if (curve[i] === undefined) continue; // Optional
|
||||
if (typeof curve[i] !== 'string')
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
}
|
||||
// Set defaults
|
||||
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
|
||||
}
|
||||
@ -176,20 +158,11 @@ export type SigType = Hex | SignatureType;
|
||||
export type CurveFn = {
|
||||
CURVE: ReturnType<typeof validateOpts>;
|
||||
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
|
||||
getSharedSecret: (privateKey: PrivKey, publicKey: Hex) => Uint8Array;
|
||||
sign: (message: Hex, privateKey: Hex) => Uint8Array;
|
||||
verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean;
|
||||
Point: PointConstructor;
|
||||
ExtendedPoint: ExtendedPointConstructor;
|
||||
Signature: SignatureConstructor;
|
||||
montgomeryCurve: {
|
||||
BASE_POINT_U: string;
|
||||
UfromPoint: (p: PointType) => Uint8Array;
|
||||
scalarMult: (u: Hex, scalar: Hex) => Uint8Array;
|
||||
scalarMultBase: (scalar: Hex) => Uint8Array;
|
||||
getPublicKey: (privateKey: Hex) => Uint8Array;
|
||||
getSharedSecret: (privateKey: Hex, publicKey: Hex) => Uint8Array;
|
||||
};
|
||||
utils: {
|
||||
mod: (a: bigint, b?: bigint) => bigint;
|
||||
invert: (number: bigint, modulo?: bigint) => bigint;
|
||||
@ -229,8 +202,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
}
|
||||
const uvRatio = CURVE.uvRatio || _uvRatio;
|
||||
|
||||
const _powPminus2 = (x: bigint) => mod.pow(x, P - _2n, P);
|
||||
const powPminus2 = CURVE.powPminus2 || _powPminus2;
|
||||
const _adjustScalarBytes = (bytes: Uint8Array) => bytes; // NOOP
|
||||
const adjustScalarBytes = CURVE.adjustScalarBytes || _adjustScalarBytes;
|
||||
function _domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
|
||||
@ -553,10 +524,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
}
|
||||
|
||||
// Little Endian
|
||||
function bytesToNumberLE(uint8a: Uint8Array): bigint {
|
||||
if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse()));
|
||||
}
|
||||
// function bytesToNumberLE(uint8a: Uint8Array): bigint {
|
||||
// if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
// return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse()));
|
||||
// }
|
||||
|
||||
// -------------------------
|
||||
// Little-endian SHA512 with modulo n
|
||||
@ -684,194 +655,19 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
// Enable precomputes. Slows down first publicKey computation by 20ms.
|
||||
Point.BASE._setWindowSize(8);
|
||||
|
||||
// ECDH (X22519/X448)
|
||||
// https://datatracker.ietf.org/doc/html/rfc7748
|
||||
// Every twisted Edwards curve is birationally equivalent to an elliptic curve in Montgomery form and vice versa.
|
||||
const montgomeryBits = CURVE.montgomeryBits || CURVE.nBitLength;
|
||||
const montgomeryBytes = Math.ceil(montgomeryBits / 8);
|
||||
|
||||
// cswap from RFC7748
|
||||
function cswap(swap: bigint, x_2: bigint, x_3: bigint): [bigint, bigint] {
|
||||
const dummy = modP(swap * (x_2 - x_3));
|
||||
x_2 = modP(x_2 - dummy);
|
||||
x_3 = modP(x_3 + dummy);
|
||||
return [x_2, x_3];
|
||||
}
|
||||
|
||||
// x25519 from 4
|
||||
/**
|
||||
*
|
||||
* @param pointU u coordinate (x) on Montgomery Curve 25519
|
||||
* @param scalar by which the point would be multiplied
|
||||
* @returns new Point on Montgomery curve
|
||||
*/
|
||||
function montgomeryLadder(pointU: bigint, scalar: bigint): bigint {
|
||||
const { P } = CURVE;
|
||||
const u = normalizeScalar(pointU, P);
|
||||
// Section 5: Implementations MUST accept non-canonical values and process them as
|
||||
// if they had been reduced modulo the field prime.
|
||||
const k = normalizeScalar(scalar, P);
|
||||
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
|
||||
const a24 = CURVE.a24;
|
||||
const x_1 = u;
|
||||
let x_2 = _1n;
|
||||
let z_2 = _0n;
|
||||
let x_3 = u;
|
||||
let z_3 = _1n;
|
||||
let swap = _0n;
|
||||
let sw: [bigint, bigint];
|
||||
for (let t = BigInt(montgomeryBits - 1); t >= _0n; t--) {
|
||||
const k_t = (k >> t) & _1n;
|
||||
swap ^= k_t;
|
||||
sw = cswap(swap, x_2, x_3);
|
||||
x_2 = sw[0];
|
||||
x_3 = sw[1];
|
||||
sw = cswap(swap, z_2, z_3);
|
||||
z_2 = sw[0];
|
||||
z_3 = sw[1];
|
||||
swap = k_t;
|
||||
|
||||
const A = x_2 + z_2;
|
||||
const AA = modP(A * A);
|
||||
const B = x_2 - z_2;
|
||||
const BB = modP(B * B);
|
||||
const E = AA - BB;
|
||||
const C = x_3 + z_3;
|
||||
const D = x_3 - z_3;
|
||||
const DA = modP(D * A);
|
||||
const CB = modP(C * B);
|
||||
const dacb = DA + CB;
|
||||
const da_cb = DA - CB;
|
||||
x_3 = modP(dacb * dacb);
|
||||
z_3 = modP(x_1 * modP(da_cb * da_cb));
|
||||
x_2 = modP(AA * BB);
|
||||
z_2 = modP(E * (AA + modP(a24 * E)));
|
||||
}
|
||||
// (x_2, x_3) = cswap(swap, x_2, x_3)
|
||||
sw = cswap(swap, x_2, x_3);
|
||||
x_2 = sw[0];
|
||||
x_3 = sw[1];
|
||||
// (z_2, z_3) = cswap(swap, z_2, z_3)
|
||||
sw = cswap(swap, z_2, z_3);
|
||||
z_2 = sw[0];
|
||||
z_3 = sw[1];
|
||||
// z_2^(p - 2)
|
||||
const z2 = powPminus2(z_2);
|
||||
// Return x_2 * (z_2^(p - 2))
|
||||
return modP(x_2 * z2);
|
||||
}
|
||||
|
||||
function encodeUCoordinate(u: bigint): Uint8Array {
|
||||
return numberToBytesLE(modP(u), montgomeryBytes);
|
||||
}
|
||||
|
||||
function decodeUCoordinate(uEnc: Hex): bigint {
|
||||
const u = ensureBytes(uEnc, montgomeryBytes);
|
||||
// Section 5: When receiving such an array, implementations of X25519
|
||||
// MUST mask the most significant bit in the final byte.
|
||||
// This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP
|
||||
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
|
||||
u[fieldLen - 1] &= 127; // 0b0111_1111
|
||||
return bytesToNumberLE(u);
|
||||
}
|
||||
|
||||
function decodeScalar(n: Hex): bigint {
|
||||
const bytes = ensureBytes(n);
|
||||
if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen)
|
||||
throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`);
|
||||
return bytesToNumberLE(adjustScalarBytes(bytes));
|
||||
}
|
||||
/*
|
||||
Converts Point to Montgomery Curve
|
||||
- u, v: curve25519 coordinates
|
||||
- x, y: ed25519 coordinates
|
||||
RFC 7748 (https://www.rfc-editor.org/rfc/rfc7748) says
|
||||
- The birational maps are (25519):
|
||||
(u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x)
|
||||
(x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1))
|
||||
- The birational maps are (448):
|
||||
(u, v) = ((y-1)/(y+1), sqrt(156324)*u/x)
|
||||
(x, y) = (sqrt(156324)*u/v, (1+u)/(1-u))
|
||||
|
||||
But original Twisted Edwards paper (https://eprint.iacr.org/2008/013.pdf) and hyperelliptics (http://hyperelliptic.org/EFD/g1p/data/twisted/coordinates)
|
||||
says that mapping is always:
|
||||
- u = (1+y)/(1-y)
|
||||
- v = 2 (1+y)/(x(1-y))
|
||||
- x = 2 u/v
|
||||
- y = (u-1)/(u+1)
|
||||
|
||||
Which maps correctly, but to completely different curve. There is different mapping for ed448 (which done with replaceble function).
|
||||
Returns 'u' coordinate of curve25519 point.
|
||||
|
||||
NOTE: jubjub will need full mapping, for now only Point -> U is enough
|
||||
*/
|
||||
function _UfromPoint(p: Point): Uint8Array {
|
||||
if (!(p instanceof Point)) throw new Error('Wrong point');
|
||||
const { y } = p;
|
||||
const u = modP((y + _1n) * mod.invert(_1n - y, P));
|
||||
return numberToBytesLE(u, montgomeryBytes);
|
||||
}
|
||||
const UfromPoint = CURVE.UfromPoint || _UfromPoint;
|
||||
|
||||
const BASE_POINT_U = CURVE.basePointU || bytesToHex(UfromPoint(Point.BASE));
|
||||
// Multiply point u by scalar
|
||||
function scalarMult(u: Hex, scalar: Hex): Uint8Array {
|
||||
const pointU = decodeUCoordinate(u);
|
||||
const _scalar = decodeScalar(scalar);
|
||||
const pu = montgomeryLadder(pointU, _scalar);
|
||||
// The result was not contributory
|
||||
// https://cr.yp.to/ecdh.html#validate
|
||||
if (pu === _0n) throw new Error('Invalid private or public key received');
|
||||
return encodeUCoordinate(pu);
|
||||
}
|
||||
// Multiply base point by scalar
|
||||
const scalarMultBase = (scalar: Hex): Uint8Array =>
|
||||
montgomeryCurve.scalarMult(montgomeryCurve.BASE_POINT_U, scalar);
|
||||
|
||||
const montgomeryCurve = {
|
||||
BASE_POINT_U,
|
||||
UfromPoint,
|
||||
// NOTE: we can get 'y' coordinate from 'u', but Point.fromHex also wants 'x' coordinate oddity flag, and we cannot get 'x' without knowing 'v'
|
||||
// Need to add generic conversion between twisted edwards and complimentary curve for JubJub
|
||||
scalarMult,
|
||||
scalarMultBase,
|
||||
// NOTE: these function work on complimentary montgomery curve
|
||||
getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(publicKey, privateKey),
|
||||
getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey),
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates X25519 DH shared secret from ed25519 private & public keys.
|
||||
* Curve25519 used in X25519 consumes private keys as-is, while ed25519 hashes them with sha512.
|
||||
* Which means we will need to normalize ed25519 seeds to "hashed repr".
|
||||
* @param privateKey ed25519 private key
|
||||
* @param publicKey ed25519 public key
|
||||
* @returns X25519 shared key
|
||||
*/
|
||||
function getSharedSecret(privateKey: PrivKey, publicKey: Hex): Uint8Array {
|
||||
const { head } = getExtendedPublicKey(privateKey);
|
||||
const u = montgomeryCurve.UfromPoint(Point.fromHex(publicKey));
|
||||
return montgomeryCurve.getSharedSecret(head, u);
|
||||
}
|
||||
|
||||
const utils = {
|
||||
getExtendedPublicKey,
|
||||
mod: modP,
|
||||
invert: (a: bigint, m = CURVE.P) => mod.invert(a, m),
|
||||
|
||||
/**
|
||||
* Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF
|
||||
* and convert them into private scalar, with the modulo bias being neglible.
|
||||
* As per FIPS 186 B.4.1.
|
||||
* Not needed for ed25519 private keys. Needed if you use scalars directly (rare).
|
||||
* @param hash hash output from sha512, or a similar function
|
||||
* @returns valid private scalar
|
||||
*/
|
||||
hashToPrivateScalar: (hash: Hex): bigint => hashToPrivateScalar(hash, CURVE_ORDER, true),
|
||||
|
||||
/**
|
||||
* ed25519 private keys are uniform 32-bit strings. We do not need to check for
|
||||
* modulo bias like we do in noble-secp256k1 randomPrivateKey()
|
||||
* modulo bias like we do in secp256k1 randomPrivateKey()
|
||||
*/
|
||||
randomPrivateKey: (): Uint8Array => randomBytes(fieldLen),
|
||||
|
||||
@ -891,14 +687,12 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
|
||||
return {
|
||||
CURVE,
|
||||
montgomeryCurve,
|
||||
getSharedSecret,
|
||||
getPublicKey,
|
||||
sign,
|
||||
verify,
|
||||
ExtendedPoint,
|
||||
Point,
|
||||
Signature,
|
||||
getPublicKey,
|
||||
utils,
|
||||
sign,
|
||||
verify,
|
||||
};
|
||||
}
|
||||
|
201
src/montgomery.ts
Normal file
201
src/montgomery.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import * as mod from './modular.js';
|
||||
import {
|
||||
ensureBytes,
|
||||
numberToBytesLE,
|
||||
bytesToNumberLE,
|
||||
// nLength,
|
||||
} from './utils.js';
|
||||
|
||||
const _0n = BigInt(0);
|
||||
const _1n = BigInt(1);
|
||||
type Hex = string | Uint8Array;
|
||||
|
||||
export type CurveType = {
|
||||
// Field over which we'll do calculations. Verify with:
|
||||
P: bigint;
|
||||
nByteLength: number;
|
||||
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
|
||||
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
||||
a24: bigint; // Related to d, but cannot be derived from it
|
||||
montgomeryBits: number;
|
||||
powPminus2?: (x: bigint) => bigint;
|
||||
xyToU?: (x: bigint, y: bigint) => bigint;
|
||||
Gu: string;
|
||||
};
|
||||
export type CurveFn = {
|
||||
scalarMult: (u: Hex, scalar: Hex) => Uint8Array;
|
||||
scalarMultBase: (scalar: Hex) => Uint8Array;
|
||||
getPublicKey: (privateKey: Hex) => Uint8Array;
|
||||
Gu: string;
|
||||
};
|
||||
|
||||
function validateOpts(curve: CurveType) {
|
||||
for (const i of ['a24'] as const) {
|
||||
if (typeof curve[i] !== 'bigint')
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
}
|
||||
for (const i of ['montgomeryBits', 'nByteLength'] as const) {
|
||||
if (curve[i] === undefined) continue; // Optional
|
||||
if (!Number.isSafeInteger(curve[i]))
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
}
|
||||
for (const fn of ['adjustScalarBytes', 'domain', 'powPminus2'] as const) {
|
||||
if (curve[fn] === undefined) continue; // Optional
|
||||
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
||||
}
|
||||
for (const i of ['Gu'] as const) {
|
||||
if (curve[i] === undefined) continue; // Optional
|
||||
if (typeof curve[i] !== 'string')
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
}
|
||||
// Set defaults
|
||||
// ...nLength(curve.n, curve.nBitLength),
|
||||
return Object.freeze({ ...curve } as const);
|
||||
}
|
||||
|
||||
export function montgomery(curveDef: CurveType): CurveFn {
|
||||
const CURVE = validateOpts(curveDef);
|
||||
const { P } = CURVE;
|
||||
const modP = (a: bigint) => mod.mod(a, P);
|
||||
const montgomeryBits = CURVE.montgomeryBits;
|
||||
const montgomeryBytes = Math.ceil(montgomeryBits / 8);
|
||||
const fieldLen = CURVE.nByteLength;
|
||||
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes);
|
||||
const powPminus2 = CURVE.powPminus2 || ((x: bigint) => mod.pow(x, P - BigInt(2), P));
|
||||
|
||||
/**
|
||||
* Checks for num to be in range:
|
||||
* For strict == true: `0 < num < max`.
|
||||
* For strict == false: `0 <= num < max`.
|
||||
* Converts non-float safe numbers to bigints.
|
||||
*/
|
||||
function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint {
|
||||
if (!max) throw new TypeError('Specify max value');
|
||||
if (typeof num === 'number' && Number.isSafeInteger(num)) num = BigInt(num);
|
||||
if (typeof num === 'bigint' && num < max) {
|
||||
if (strict) {
|
||||
if (_0n < num) return num;
|
||||
} else {
|
||||
if (_0n <= num) return num;
|
||||
}
|
||||
}
|
||||
throw new TypeError('Expected valid scalar: 0 < scalar < max');
|
||||
}
|
||||
|
||||
// cswap from RFC7748
|
||||
function cswap(swap: bigint, x_2: bigint, x_3: bigint): [bigint, bigint] {
|
||||
const dummy = modP(swap * (x_2 - x_3));
|
||||
x_2 = modP(x_2 - dummy);
|
||||
x_3 = modP(x_3 + dummy);
|
||||
return [x_2, x_3];
|
||||
}
|
||||
|
||||
// x25519 from 4
|
||||
/**
|
||||
*
|
||||
* @param pointU u coordinate (x) on Montgomery Curve 25519
|
||||
* @param scalar by which the point would be multiplied
|
||||
* @returns new Point on Montgomery curve
|
||||
*/
|
||||
function montgomeryLadder(pointU: bigint, scalar: bigint): bigint {
|
||||
const { P } = CURVE;
|
||||
const u = normalizeScalar(pointU, P);
|
||||
// Section 5: Implementations MUST accept non-canonical values and process them as
|
||||
// if they had been reduced modulo the field prime.
|
||||
const k = normalizeScalar(scalar, P);
|
||||
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
|
||||
const a24 = CURVE.a24;
|
||||
const x_1 = u;
|
||||
let x_2 = _1n;
|
||||
let z_2 = _0n;
|
||||
let x_3 = u;
|
||||
let z_3 = _1n;
|
||||
let swap = _0n;
|
||||
let sw: [bigint, bigint];
|
||||
for (let t = BigInt(montgomeryBits - 1); t >= _0n; t--) {
|
||||
const k_t = (k >> t) & _1n;
|
||||
swap ^= k_t;
|
||||
sw = cswap(swap, x_2, x_3);
|
||||
x_2 = sw[0];
|
||||
x_3 = sw[1];
|
||||
sw = cswap(swap, z_2, z_3);
|
||||
z_2 = sw[0];
|
||||
z_3 = sw[1];
|
||||
swap = k_t;
|
||||
|
||||
const A = x_2 + z_2;
|
||||
const AA = modP(A * A);
|
||||
const B = x_2 - z_2;
|
||||
const BB = modP(B * B);
|
||||
const E = AA - BB;
|
||||
const C = x_3 + z_3;
|
||||
const D = x_3 - z_3;
|
||||
const DA = modP(D * A);
|
||||
const CB = modP(C * B);
|
||||
const dacb = DA + CB;
|
||||
const da_cb = DA - CB;
|
||||
x_3 = modP(dacb * dacb);
|
||||
z_3 = modP(x_1 * modP(da_cb * da_cb));
|
||||
x_2 = modP(AA * BB);
|
||||
z_2 = modP(E * (AA + modP(a24 * E)));
|
||||
}
|
||||
// (x_2, x_3) = cswap(swap, x_2, x_3)
|
||||
sw = cswap(swap, x_2, x_3);
|
||||
x_2 = sw[0];
|
||||
x_3 = sw[1];
|
||||
// (z_2, z_3) = cswap(swap, z_2, z_3)
|
||||
sw = cswap(swap, z_2, z_3);
|
||||
z_2 = sw[0];
|
||||
z_3 = sw[1];
|
||||
// z_2^(p - 2)
|
||||
const z2 = powPminus2(z_2);
|
||||
// Return x_2 * (z_2^(p - 2))
|
||||
return modP(x_2 * z2);
|
||||
}
|
||||
|
||||
function encodeUCoordinate(u: bigint): Uint8Array {
|
||||
return numberToBytesLE(modP(u), montgomeryBytes);
|
||||
}
|
||||
|
||||
function decodeUCoordinate(uEnc: Hex): bigint {
|
||||
const u = ensureBytes(uEnc, montgomeryBytes);
|
||||
// Section 5: When receiving such an array, implementations of X25519
|
||||
// MUST mask the most significant bit in the final byte.
|
||||
// This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP
|
||||
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
|
||||
u[fieldLen - 1] &= 127; // 0b0111_1111
|
||||
return bytesToNumberLE(u);
|
||||
}
|
||||
|
||||
function decodeScalar(n: Hex): bigint {
|
||||
const bytes = ensureBytes(n);
|
||||
if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen)
|
||||
throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`);
|
||||
return bytesToNumberLE(adjustScalarBytes(bytes));
|
||||
}
|
||||
// Multiply point u by scalar
|
||||
function scalarMult(u: Hex, scalar: Hex): Uint8Array {
|
||||
const pointU = decodeUCoordinate(u);
|
||||
const _scalar = decodeScalar(scalar);
|
||||
const pu = montgomeryLadder(pointU, _scalar);
|
||||
// The result was not contributory
|
||||
// https://cr.yp.to/ecdh.html#validate
|
||||
if (pu === _0n) throw new Error('Invalid private or public key received');
|
||||
return encodeUCoordinate(pu);
|
||||
}
|
||||
// Multiply base point by scalar
|
||||
function scalarMultBase(scalar: Hex): Uint8Array {
|
||||
return scalarMult(CURVE.Gu, scalar);
|
||||
}
|
||||
|
||||
return {
|
||||
// NOTE: we can get 'y' coordinate from 'u', but Point.fromHex also wants 'x' coordinate oddity flag, and we cannot get 'x' without knowing 'v'
|
||||
// Need to add generic conversion between twisted edwards and complimentary curve for JubJub
|
||||
scalarMult,
|
||||
scalarMultBase,
|
||||
// NOTE: these function work on complimentary montgomery curve
|
||||
// getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(publicKey, privateKey),
|
||||
getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey),
|
||||
Gu: CURVE.Gu,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user