From: Chris Duncan Date: Mon, 1 Dec 2025 06:14:16 +0000 (-0800) Subject: Convert Point from class to factory function to resolve final class self-references. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=4b6fa38baa07dc7947f8c65700caa59e0314656e;p=libnemo.git Convert Point from class to factory function to resolve final class self-references. --- diff --git a/src/lib/crypto/secp256k1.ts b/src/lib/crypto/secp256k1.ts index 8f5970f..0c48cbc 100644 --- a/src/lib/crypto/secp256k1.ts +++ b/src/lib/crypto/secp256k1.ts @@ -11,7 +11,25 @@ * Functionality wrapped in a class for embedding into a Web Worker. */ -type Point = InstanceType +type Point = { + X: bigint + Y: bigint + Z: bigint + get x (): bigint + get y (): bigint + equals: (other: Point) => boolean + is0: () => boolean + negate: () => Point + double: () => Point + add: (other: Point) => Point + subtract: (other: Point) => Point + multiply: (n: bigint, safe?: boolean) => Point + multiplyUnsafe: (scalar: bigint) => Point + toAffine: () => AffinePoint + assertValidity: () => Point + toBytes: (isCompressed?: boolean) => Bytes + toHex: (isCompressed?: boolean) => string +} type Signature = ReturnType /** Alias to Uint8Array. */ @@ -260,140 +278,136 @@ export class Secp256k1 { } /** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */ - static Point = class { - readonly X: bigint - readonly Y: bigint - readonly Z: bigint - constructor (X: bigint, Y: bigint, Z: bigint) { - this.X = Secp256k1.FpIsValid(X) - this.Y = Secp256k1.FpIsValidNot0(Y) // Y can't be 0 in Projective - this.Z = Secp256k1.FpIsValid(Z) - Object.freeze(this) - } - get x (): bigint { - return this.toAffine().x - } - get y (): bigint { - return this.toAffine().y - } - /** Equality check: compare points P&Q. */ - equals (other: Point): boolean { - const { X: X1, Y: Y1, Z: Z1 } = this - const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other) // checks class equality - const X1Z2 = Secp256k1.M(X1 * Z2) - const X2Z1 = Secp256k1.M(X2 * Z1) - const Y1Z2 = Secp256k1.M(Y1 * Z2) - const Y2Z1 = Secp256k1.M(Y2 * Z1) - return X1Z2 === X2Z1 && Y1Z2 === Y2Z1 - } - is0 (): boolean { - return this.equals(Secp256k1.I) - } - /** Flip point over y coordinate. */ - negate (): Point { - return new (this.constructor as typeof Secp256k1.Point)(this.X, Secp256k1.M(-this.Y), this.Z) - } - /** Point doubling: P+P, complete formula. */ - double (): Point { - return this.add(this) - } - /** - * Point addition: P+Q, complete, exception-free formula - * (Renes-Costello-Batina, algo 1 of [2015/1060](https://eprint.iacr.org/2015/1060)). - * Cost: `12M + 0S + 3*a + 3*b3 + 23add`. - */ - // prettier-ignore - add (other: Point): Point { - const { X: X1, Y: Y1, Z: Z1 } = this - const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other) - const a = 0n - const b = Secp256k1._b - let X3 = 0n, Y3 = 0n, Z3 = 0n - const b3 = Secp256k1.M(b * 3n) - let t0 = Secp256k1.M(X1 * X2), t1 = Secp256k1.M(Y1 * Y2), t2 = Secp256k1.M(Z1 * Z2), t3 = Secp256k1.M(X1 + Y1) // step 1 - let t4 = Secp256k1.M(X2 + Y2) // step 5 - t3 = Secp256k1.M(t3 * t4); t4 = Secp256k1.M(t0 + t1); t3 = Secp256k1.M(t3 - t4); t4 = Secp256k1.M(X1 + Z1) - let t5 = Secp256k1.M(X2 + Z2) // step 10 - t4 = Secp256k1.M(t4 * t5); t5 = Secp256k1.M(t0 + t2); t4 = Secp256k1.M(t4 - t5); t5 = Secp256k1.M(Y1 + Z1) - X3 = Secp256k1.M(Y2 + Z2) // step 15 - t5 = Secp256k1.M(t5 * X3); X3 = Secp256k1.M(t1 + t2); t5 = Secp256k1.M(t5 - X3); Z3 = Secp256k1.M(a * t4) - X3 = Secp256k1.M(b3 * t2) // step 20 - Z3 = Secp256k1.M(X3 + Z3); X3 = Secp256k1.M(t1 - Z3); Z3 = Secp256k1.M(t1 + Z3); Y3 = Secp256k1.M(X3 * Z3) - t1 = Secp256k1.M(t0 + t0) // step 25 - t1 = Secp256k1.M(t1 + t0); t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(b3 * t4); t1 = Secp256k1.M(t1 + t2) - t2 = Secp256k1.M(t0 - t2) // step 30 - t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(t4 + t2); t0 = Secp256k1.M(t1 * t4); Y3 = Secp256k1.M(Y3 + t0) - t0 = Secp256k1.M(t5 * t4) // step 35 - X3 = Secp256k1.M(t3 * X3); X3 = Secp256k1.M(X3 - t0); t0 = Secp256k1.M(t3 * t1); Z3 = Secp256k1.M(t5 * Z3) - Z3 = Secp256k1.M(Z3 + t0) // step 40 - return new Secp256k1.Point(X3, Y3, Z3) - } - subtract (other: Point): Point { - return this.add(Secp256k1.apoint(other).negate()) - } - /** - * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n. - * Uses {@link wNAF} for base point. - * Uses fake point to mitigate side-channel leakage. - * @param n scalar by which point is multiplied - * @param safe safe mode guards against timing attacks; unsafe mode is faster - */ - multiply (n: bigint, safe = true): Point { - if (!safe && n === 0n) return Secp256k1.I - Secp256k1.FnIsValidNot0(n) - if (n === 1n) return this - if (this.equals(Secp256k1.G)) return Secp256k1.wNAF(n).p - // init result point & fake point - let p = Secp256k1.I - let f = Secp256k1.G - for (let d: Point = this; n > 0n; d = d.double(), n >>= 1n) { - // if bit is present, add to point - // if not present, add to fake, for timing safety - if (n & 1n) p = p.add(d) - else if (safe) f = f.add(d) + static Point = (X: bigint, Y: bigint, Z: bigint): Point => { + const secp256k1 = this + return Object.freeze({ + X: this.FpIsValid(X), + Y: this.FpIsValidNot0(Y), // Y can't be 0 in Projective + Z: this.FpIsValid(Z), + get x (): bigint { + return this.toAffine().x + }, + get y (): bigint { + return this.toAffine().y + }, + /** Equality check: compare points P&Q. */ + equals (other: Point): boolean { + const { X: X1, Y: Y1, Z: Z1 } = this + const { X: X2, Y: Y2, Z: Z2 } = other + const X1Z2 = secp256k1.M(X1 * Z2) + const X2Z1 = secp256k1.M(X2 * Z1) + const Y1Z2 = secp256k1.M(Y1 * Z2) + const Y2Z1 = secp256k1.M(Y2 * Z1) + return X1Z2 === X2Z1 && Y1Z2 === Y2Z1 + }, + is0 (): boolean { + return this.equals(secp256k1.I) + }, + /** Flip point over y coordinate. */ + negate: (): Point => { + return this.Point(X, this.M(-Y), Z) + }, + /** Point doubling: P+P, complete formula. */ + double (): Point { + return this.add(this) + }, + /** + * Point addition: P+Q, complete, exception-free formula + * (Renes-Costello-Batina, algo 1 of [2015/1060](https://eprint.iacr.org/2015/1060)). + * Cost: `12M + 0S + 3*a + 3*b3 + 23add`. + */ + // prettier-ignore + add: (other: Point): Point => { + const { X: X1, Y: Y1, Z: Z1 } = { X, Y, Z } + const { X: X2, Y: Y2, Z: Z2 } = other + const a = 0n + const b = this._b + let X3 = 0n, Y3 = 0n, Z3 = 0n + const b3 = this.M(b * 3n) + let t0 = this.M(X1 * X2), t1 = this.M(Y1 * Y2), t2 = this.M(Z1 * Z2), t3 = this.M(X1 + Y1) // step 1 + let t4 = this.M(X2 + Y2) // step 5 + t3 = this.M(t3 * t4); t4 = this.M(t0 + t1); t3 = this.M(t3 - t4); t4 = this.M(X1 + Z1) + let t5 = this.M(X2 + Z2) // step 10 + t4 = this.M(t4 * t5); t5 = this.M(t0 + t2); t4 = this.M(t4 - t5); t5 = this.M(Y1 + Z1) + X3 = this.M(Y2 + Z2) // step 15 + t5 = this.M(t5 * X3); X3 = this.M(t1 + t2); t5 = this.M(t5 - X3); Z3 = this.M(a * t4) + X3 = this.M(b3 * t2) // step 20 + Z3 = this.M(X3 + Z3); X3 = this.M(t1 - Z3); Z3 = this.M(t1 + Z3); Y3 = this.M(X3 * Z3) + t1 = this.M(t0 + t0) // step 25 + t1 = this.M(t1 + t0); t2 = this.M(a * t2); t4 = this.M(b3 * t4); t1 = this.M(t1 + t2) + t2 = this.M(t0 - t2) // step 30 + t2 = this.M(a * t2); t4 = this.M(t4 + t2); t0 = this.M(t1 * t4); Y3 = this.M(Y3 + t0) + t0 = this.M(t5 * t4) // step 35 + X3 = this.M(t3 * X3); X3 = this.M(X3 - t0); t0 = this.M(t3 * t1); Z3 = this.M(t5 * Z3) + Z3 = this.M(Z3 + t0) // step 40 + return this.Point(X3, Y3, Z3) + }, + subtract (other: Point): Point { + return this.add(other.negate()) + }, + /** + * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n. + * Uses {@link wNAF} for base point. + * Uses fake point to mitigate side-channel leakage. + * @param n scalar by which point is multiplied + * @param safe safe mode guards against timing attacks; unsafe mode is faster + */ + multiply (n: bigint, safe = true): Point { + if (!safe && n === 0n) return secp256k1.I + secp256k1.FnIsValidNot0(n) + if (n === 1n) return this + if (this.equals(secp256k1.G)) return secp256k1.wNAF(n).p + // init result point & fake point + let p = secp256k1.I + let f = secp256k1.G + for (let d: Point = this; n > 0n; d = d.double(), n >>= 1n) { + // if bit is present, add to point + // if not present, add to fake, for timing safety + if (n & 1n) p = p.add(d) + else if (safe) f = f.add(d) + } + return p + }, + multiplyUnsafe (scalar: bigint): Point { + return this.multiply(scalar, false) + }, + /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */ + toAffine (): AffinePoint { + const { X: x, Y: y, Z: z } = this + // fast-paths for ZERO point OR Z=1 + if (this.equals(secp256k1.I)) return { x: 0n, y: 0n } + if (z === 1n) return { x, y } + const iz = secp256k1.invert(z, secp256k1.P) + // (Z * Z^-1) must be 1, otherwise bad math + if (secp256k1.M(z * iz) !== 1n) secp256k1.err('inverse invalid') + // x = X*Z^-1; y = Y*Z^-1 + return { x: secp256k1.M(x * iz), y: secp256k1.M(y * iz) } + }, + /** Checks if the point is valid and on-curve. */ + assertValidity (): Point { + const { x, y } = this.toAffine() // convert to 2d xy affine point. + secp256k1.FpIsValidNot0(x) // must be in range 1 <= x,y < P + secp256k1.FpIsValidNot0(y) + // y² == x³ + ax + b, equation sides must be equal + return secp256k1.M(y * y) === secp256k1.koblitz(x) ? this : secp256k1.err('bad point: not on curve') + }, + /** Converts point to 33/65-byte Uint8Array. */ + toBytes (isCompressed = true): Bytes { + const { x, y } = this.assertValidity().toAffine() + const x32b = secp256k1.numTo32b(x) + if (isCompressed) return secp256k1.concatBytes(secp256k1.getPrefix(y), x32b) + return secp256k1.concatBytes(secp256k1.u8of(0x04), x32b, secp256k1.numTo32b(y)) + }, + toHex (isCompressed?: boolean): string { + return secp256k1.bytesToHex(this.toBytes(isCompressed)) } - return p - } - multiplyUnsafe (scalar: bigint): Point { - return this.multiply(scalar, false) - } - /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */ - toAffine (): AffinePoint { - const { X: x, Y: y, Z: z } = this - // fast-paths for ZERO point OR Z=1 - if (this.equals(Secp256k1.I)) return { x: 0n, y: 0n } - if (z === 1n) return { x, y } - const iz = Secp256k1.invert(z, Secp256k1.P) - // (Z * Z^-1) must be 1, otherwise bad math - if (Secp256k1.M(z * iz) !== 1n) Secp256k1.err('inverse invalid') - // x = X*Z^-1; y = Y*Z^-1 - return { x: Secp256k1.M(x * iz), y: Secp256k1.M(y * iz) } - } - /** Checks if the point is valid and on-curve. */ - assertValidity (): Point { - const { x, y } = this.toAffine() // convert to 2d xy affine point. - Secp256k1.FpIsValidNot0(x) // must be in range 1 <= x,y < P - Secp256k1.FpIsValidNot0(y) - // y² == x³ + ax + b, equation sides must be equal - return Secp256k1.M(y * y) === Secp256k1.koblitz(x) ? this : Secp256k1.err('bad point: not on curve') - } - /** Converts point to 33/65-byte Uint8Array. */ - toBytes (isCompressed = true): Bytes { - const { x, y } = this.assertValidity().toAffine() - const x32b = Secp256k1.numTo32b(x) - if (isCompressed) return Secp256k1.concatBytes(Secp256k1.getPrefix(y), x32b) - return Secp256k1.concatBytes(Secp256k1.u8of(0x04), x32b, Secp256k1.numTo32b(y)) - } - - toHex (isCompressed?: boolean): string { - return Secp256k1.bytesToHex(this.toBytes(isCompressed)) - } + }) } /** Create 3d xyz point from 2d xy. (0, 0) => (0, 1, 0), not (0, 0, 1) */ static pointFromAffine (ap: AffinePoint): Point { const { x, y } = ap - return x === 0n && y === 0n ? this.I : new this.Point(x, y, 1n) + return x === 0n && y === 0n ? this.I : this.Point(x, y, 1n) } /** Convert Uint8Array or hex string to Point. */ static pointFromBytes (bytes: Bytes): Point { @@ -412,10 +426,10 @@ export class Secp256k1 { const evenY = this.isEven(y) const evenH = this.isEven(this.big(head)) if (evenH !== evenY) y = this.M(-y) - p = new this.Point(x, y, 1n) + p = this.Point(x, y, 1n) } // Uncompressed 65-byte point, 0x04 prefix - if (length === uncomp && head === 0x04) p = new this.Point(x, this.sliceBytesNumBE(tail, this.L, this.L2), 1n) + if (length === uncomp && head === 0x04) p = this.Point(x, this.sliceBytesNumBE(tail, this.L, this.L2), 1n) // Validate point return p ? p.assertValidity() : this.err('bad point: not on curve') } @@ -424,9 +438,9 @@ export class Secp256k1 { } /** Generator / base point */ - static G: Point = new this.Point(this.Gx, this.Gy, 1n) + static G: Point = this.Point(this.Gx, this.Gy, 1n) /** Identity / zero point */ - static I: Point = new this.Point(0n, 1n, 0n) + static I: Point = this.Point(0n, 1n, 0n) /** `Q = u1⋅G + u2⋅R`. Verifies Q is not ZERO. Unsafe: non-CT. */ static doubleScalarMulUns = (R: Point, u1: bigint, u2: bigint): Point => { return this.G.multiply(u1, false).add(R.multiply(u2, false)).assertValidity() @@ -1042,7 +1056,7 @@ export class Secp256k1 { const y_ = this.isEven(y) ? y : this.M(-y) // Return the unique point P such that x(P) = x and // y(P) = y if y mod 2 = 0 or y(P) = p-y otherwise. - const P_ = new this.Point(x, y_, 1n).assertValidity() + const P_ = this.Point(x, y_, 1n).assertValidity() const px = this.numTo32b(P_.toAffine().x) // P = lift_x(int(pk)); fail if that fails const r = this.sliceBytesNumBE(sig, 0, this.L) // Let r = int(sig[0:32]); fail if r ≥ p.