From: Chris Duncan Date: Wed, 3 Dec 2025 22:41:01 +0000 (-0800) Subject: Convert arrow function properties to regular static methods. Fix scope of M in point... X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=84725b58819cf0b12bca54f8050fe764898c3e69;p=libnemo.git Convert arrow function properties to regular static methods. Fix scope of M in point addition. Use Typescript arithmetic to typecheck buffer lengths. --- diff --git a/src/lib/crypto/secp256k1.ts b/src/lib/crypto/secp256k1.ts index bce30a9..c5c0560 100644 --- a/src/lib/crypto/secp256k1.ts +++ b/src/lib/crypto/secp256k1.ts @@ -100,6 +100,22 @@ type KeygenFn = (seed?: Bytes) => KeysSecPub type MaybePromise = T | Promise +type EnsureNumber = A extends number ? A : never +type Length = EnsureNumber +type Tuple = Length extends N ? Accumulator : Tuple +type Add = Length<[...Tuple, ...Tuple]> +type Subtract = Tuple extends [...Tuple, ...infer Rest,] ? Length : never +type IsLessThanOrEqual = Tuple extends [...Tuple, ...infer _Rest,] ? true : false +type IsLessThan = IsLessThanOrEqual, B> +type Multiply = B extends 0 ? 0 : Add>, A> +type Quotient = B extends 0 ? never : B extends 1 ? A : A extends 0 ? 0 : A extends B ? 1 : IsLessThan extends true ? never : Multiply extends A ? C : Quotient> +type Secp256k1Lengths = { + publicKey: Add + publicKeyUncompressed: Add + signature: typeof Secp256k1.L2 + seed: Add> +} + export class Secp256k1 { /** * Curve params. secp256k1 is short weierstrass / koblitz curve. Equation is y² == x³ + ax + b. @@ -128,13 +144,13 @@ export class Secp256k1 { Gy: this.Gy, } - static L = 32 // field / group byte length - static L2 = 64 - static lengths = { - publicKey: this.L + 1, - publicKeyUncompressed: this.L2 + 1, - signature: this.L2, - seed: this.L + this.L / 2, + static L: 32 = 32 // field / group byte length + static L2: 64 = 64 + static lengths: Secp256k1Lengths = { + publicKey: 33, + publicKeyUncompressed: 65, + signature: 64, + seed: 48, } // Helpers and Precomputes sections are reused between libraries @@ -149,7 +165,7 @@ export class Secp256k1 { /** Asserts something is Uint8Array. */ static abytes (value: unknown, length?: number, title: string = ''): Bytes { - function isUint8Array (a: unknown): a is Uint8Array { + function isUint8Array (a: unknown): a is Bytes { return (a instanceof Uint8Array && a.buffer instanceof ArrayBuffer) || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array') } const isBytes = isUint8Array(value) @@ -219,7 +235,7 @@ export class Secp256k1 { /** Modular inversion using eucledian GCD (non-CT). No negative exponent for now. */ // prettier-ignore - static invert = (num: bigint, md: bigint): bigint => { + static invert (num: bigint, md: bigint): bigint { if (num === 0n || md <= 0n) this.err('no inverse n=' + num + ' mod=' + md) let a = this.M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n while (a !== 0n) { @@ -230,7 +246,7 @@ export class Secp256k1 { return b === 1n ? this.M(x, md) : this.err('no inverse') // b is gcd at this point } - static callHash = (name: string) => { + static callHash (name: string) { // @ts-ignore const fn = hashes[name] if (typeof fn !== 'function') this.err('hashes.' + name + ' not set') @@ -252,7 +268,7 @@ export class Secp256k1 { static u8of = (n: number): Bytes => Uint8Array.of(n) static getPrefix = (y: bigint) => this.u8of(this.isEven(y) ? 0x02 : 0x03) /** lift_x from BIP340 calculates square root. Validates x, then validates root*root. */ - static lift_x = (x: bigint) => { + static lift_x (x: bigint) { // Let c = x³ + 7 mod p. Fail if x ≥ p. (also fail if x < 1) const c = this.koblitz(this.FpIsValidNot0(x)) // c = √y @@ -269,7 +285,7 @@ export class Secp256k1 { } /** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */ - static Point = (X: bigint, Y: bigint, Z: bigint): Point => { + static Point (X: bigint, Y: bigint, Z: bigint): Point { const secp256k1 = this return Object.freeze({ X: this.FpIsValid(X), @@ -286,8 +302,8 @@ export class Secp256k1 { return X1Z2 === X2Z1 && Y1Z2 === Y2Z1 }, /** Flip point over y coordinate. */ - negate: (): Point => { - return this.Point(X, this.M(-Y), Z) + negate (): Point { + return secp256k1.Point(X, secp256k1.M(-Y), Z) }, /** Point doubling: P+P, complete formula. */ double (): Point { @@ -300,11 +316,11 @@ export class Secp256k1 { */ // prettier-ignore add (other: Point): Point { - const { M, CURVE, Point } = secp256k1 + const M = (v: bigint): bigint => secp256k1.M(v) const { X: X1, Y: Y1, Z: Z1 } = { X, Y, Z } const { X: X2, Y: Y2, Z: Z2 } = other const a = 0n - const b = CURVE.b + const b = secp256k1.CURVE.b const b3 = M(b * 3n) let X3 = 0n, Y3 = 0n, Z3 = 0n let t0 = M(X1 * X2), t1 = M(Y1 * Y2), t2 = M(Z1 * Z2), t3 = M(X1 + Y1) // step 1 @@ -323,7 +339,7 @@ export class Secp256k1 { t0 = M(t5 * t4) // step 35 X3 = M(t3 * X3); X3 = M(X3 - t0); t0 = M(t3 * t1); Z3 = M(t5 * Z3) Z3 = M(Z3 + t0) // step 40 - return Point(X3, Y3, Z3) + return secp256k1.Point(X3, Y3, Z3) }, subtract (other: Point): Point { return this.add(other.negate()) @@ -443,25 +459,25 @@ export class Secp256k1 { ) } /** Normalize private key to scalar (bigint). Verifies scalar is in range 1 { + static secretKeyToScalar (secretKey: Bytes): bigint { const num = this.bytesToBigint(this.abytes(secretKey, this.L, 'secret key')) return this.bigintInRange(num, 1n, this.N, 'invalid secret key: outside of range') } /** For Signature malleability, validates sig.s is bigger than N/2. */ static highS = (n: bigint): boolean => n > this.N >> 1n /** Creates 33/65-byte public key from 32-byte private key. */ - static getPublicKey = (privKey: Bytes, isCompressed = true): Bytes => { + static getPublicKey (privKey: Bytes, isCompressed = true): Bytes { return this.G.multiply(this.secretKeyToScalar(privKey)).toBytes(isCompressed) } - static isValidSecretKey = (secretKey: Bytes): boolean => { + static isValidSecretKey (secretKey: Bytes): boolean { try { return !!this.secretKeyToScalar(secretKey) } catch (error) { return false } } - static isValidPublicKey = (publicKey: Bytes, isCompressed?: boolean): boolean => { + static isValidPublicKey (publicKey: Bytes, isCompressed?: boolean): boolean { const { publicKey: comp, publicKeyUncompressed } = this.lengths try { const l = publicKey.length @@ -473,15 +489,15 @@ export class Secp256k1 { } } - static assertRecoveryBit = (recovery?: number) => { + static assertRecoveryBit (recovery?: number) { if (![0, 1, 2, 3].includes(recovery!)) this.err('recovery id must be valid and present') } - static assertSigFormat = (format?: ECDSASignatureFormat) => { + static assertSigFormat (format?: ECDSASignatureFormat) { if (format != null && !this.ALL_SIG.includes(format)) this.err(`Signature format must be one of: ${this.ALL_SIG.join(', ')}`) if (format === this.SIG_DER) this.err('Signature format "der" is not supported: switch to noble-curves') } - static assertSigLength = (sig: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT) => { + static assertSigLength (sig: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT) { this.assertSigFormat(format) const SL = this.lengths.signature const RL = SL + 1 @@ -513,7 +529,7 @@ export class Secp256k1 { static ALL_SIG = [this.SIG_COMPACT, this.SIG_RECOVERED, this.SIG_DER] as const /** ECDSA Signature class. Supports only compact 64-byte representation, not DER. */ - static Signature = (r: bigint, s: bigint, recovery?: number) => { + static Signature (r: bigint, s: bigint, recovery?: number) { return Object.freeze({ r: this.FnIsValidNot0(r), // 1 <= r < N s: this.FnIsValidNot0(s), // 1 <= s < N @@ -553,7 +569,7 @@ export class Secp256k1 { * FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits, * which matches bits2int. bits2int can produce res>N. */ - static bits2int = (bytes: Bytes): bigint => { + static bits2int (bytes: Bytes): bigint { const delta = bytes.length * 8 - 256 if (delta > 1024) this.err('msg invalid') // our CUSTOM check, "just-in-case": prohibit long inputs const num = this.bytesToBigint(bytes) @@ -592,7 +608,7 @@ export class Secp256k1 { static _maxDrbgIters = 1000 static _drbgErr = 'drbg: tried max amount of iterations' // HMAC-DRBG from NIST 800-90. Minimal, non-full-spec - used for RFC6979 signatures. - static hmacDrbg = (seed: Bytes, pred: Pred): Bytes => { + static hmacDrbg (seed: Bytes, pred: Pred): Bytes { let v = new Uint8Array(this.L) // Steps B, C of RFC6979 3.2: set hashLen let k = new Uint8Array(this.L) // In our case, it's always equal to L let i = 0 // Iterations counter, will throw when over max @@ -625,7 +641,7 @@ export class Secp256k1 { } // Identical to hmacDrbg, but async: uses built-in WebCrypto - static hmacDrbgAsync = async (seed: Bytes, pred: Pred): Promise => { + static async hmacDrbgAsync (seed: Bytes, pred: Pred): Promise { let v = new Uint8Array(this.L) // Steps B, C of RFC6979 3.2: set hashLen let k = new Uint8Array(this.L) // In our case, it's always equal to L let i = 0 // Iterations counter, will throw when over max @@ -659,12 +675,7 @@ export class Secp256k1 { // RFC6979 signature generation, preparation step. // Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.2 & RFC6979. - static _sign = ( - messageHash: Bytes, - secretKey: Bytes, - opts: ECDSASignOpts, - hmacDrbg: (seed: Bytes, pred: Pred) => T - ): T => { + static _sign (messageHash: Bytes, secretKey: Bytes, opts: ECDSASignOpts, hmacDrbg: (seed: Bytes, pred: Pred) => T): T { let { lowS, extraEntropy } = opts // generates low-s sigs by default // RFC6979 3.2: we skip step A const int2octets = this.bigintTo32Bytes // int to octets @@ -714,7 +725,7 @@ export class Secp256k1 { } // Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.4. - static _verify = (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}) => { + static _verify (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): boolean { const { lowS, format } = opts if (sig instanceof this.Signature) this.err('Signature must be in Uint8Array, use .toBytes()') this.assertSigLength(sig, format) @@ -736,7 +747,7 @@ export class Secp256k1 { } } - static setDefaults = (opts: ECDSASignOpts): Required => { + static setDefaults (opts: ECDSASignOpts): Required { const res: ECDSASignOpts = {} Object.keys(this.defaultSignOpts).forEach((k: string) => { // @ts-ignore @@ -758,7 +769,7 @@ export class Secp256k1 { * sign(msg, secretKey, { format: 'recovered' }); * ``` */ - static sign = (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes => { + static sign (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes { opts = this.setDefaults(opts) message = this.prepMsg(message, opts, false) as Bytes return this._sign(message, secretKey, opts, this.hmacDrbg) @@ -777,11 +788,7 @@ export class Secp256k1 { * await signAsync(msg, secretKey, { format: 'recovered' }); * ``` */ - static signAsync = async ( - message: Bytes, - secretKey: Bytes, - opts: ECDSASignOpts = {} - ): Promise => { + static async signAsync (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Promise { opts = this.setDefaults(opts) message = await this.prepMsg(message, opts, true) return this._sign(message, secretKey, opts, this.hmacDrbgAsync) @@ -802,12 +809,7 @@ export class Secp256k1 { * verify(sigr, msg, publicKey, { format: 'recovered' }); * ``` */ - static verify = ( - signature: Bytes, - message: Bytes, - publicKey: Bytes, - opts: ECDSAVerifyOpts = {} - ): boolean => { + static verify (signature: Bytes, message: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): boolean { opts = this.setDefaults(opts) message = this.prepMsg(message, opts, false) as Bytes return this._verify(signature, message, publicKey, opts) @@ -828,18 +830,13 @@ export class Secp256k1 { * verify(sigr, msg, publicKey, { format: 'recovered' }); * ``` */ - static verifyAsync = async ( - sig: Bytes, - message: Bytes, - publicKey: Bytes, - opts: ECDSAVerifyOpts = {} - ): Promise => { + static async verifyAsync (sig: Bytes, message: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): Promise { opts = this.setDefaults(opts) message = await this.prepMsg(message, opts, true) return this._verify(sig, message, publicKey, opts) } - static _recover = (signature: Bytes, messageHash: Bytes) => { + static _recover (signature: Bytes, messageHash: Bytes): Bytes { const sig = this.signatureFromBytes(signature, 'recovered') const { r, s, recovery } = sig // 0 or 1 recovery id determines sign of "y" coordinate. @@ -862,16 +859,12 @@ export class Secp256k1 { * ECDSA public key recovery. Requires msg hash and recovery id. * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.6. */ - static recoverPublicKey = (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Bytes => { + static recoverPublicKey (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Bytes { message = this.prepMsg(message, this.setDefaults(opts), false) as Bytes return this._recover(signature, message) } - static recoverPublicKeyAsync = async ( - signature: Bytes, - message: Bytes, - opts: ECDSARecoverOpts = {} - ): Promise => { + static async recoverPublicKeyAsync (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Promise { message = await this.prepMsg(message, this.setDefaults(opts), true) return this._recover(signature, message) } @@ -882,13 +875,13 @@ export class Secp256k1 { * @param isCompressed 33-byte (true) or 65-byte (false) output * @returns public key C */ - static getSharedSecret = (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes => { + static getSharedSecret (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes { return this.pointFromBytes(publicKeyB).multiply(this.secretKeyToScalar(secretKeyA)).toBytes(isCompressed) } // FIPS 186 B.4.1 compliant key generation produces private keys // with modulo bias being neglible. takes >N+16 bytes, returns (hash mod n-1)+1 - static randomSecretKey (seed?: Bytes) { + static randomSecretKey (seed?: Bytes): Bytes { seed ??= crypto.getRandomValues(new Uint8Array(this.lengths.seed)) this.abytes(seed) if (seed.length < this.lengths.seed || seed.length > 1024) this.err('expected 40-1024b') @@ -928,12 +921,12 @@ export class Secp256k1 { static T_AUX = 'aux' static T_NONCE = 'nonce' static T_CHALLENGE = 'challenge' - static taggedHash = (tag: string, ...messages: Bytes[]): Bytes => { + static taggedHash (tag: string, ...messages: Bytes[]): Bytes { const fn = this.callHash('sha256') const tagH = fn(this.getTag(tag)) return fn(this.concatBytes(tagH, tagH, ...messages)) } - static taggedHashAsync = async (tag: string, ...messages: Bytes[]): Promise => { + static async taggedHashAsync (tag: string, ...messages: Bytes[]): Promise { const fn = this.hashes.sha256Async const tagH = await fn(this.getTag(tag)) return await fn(this.concatBytes(tagH, tagH, ...messages)) @@ -941,7 +934,7 @@ export class Secp256k1 { // ECDSA compact points are 33-byte. Schnorr is 32: we strip first byte 0x02 or 0x03 // Calculate point, scalar and bytes - static extpubSchnorr = (priv: Bytes) => { + static extpubSchnorr (priv: Bytes) { const d_ = this.secretKeyToScalar(priv) const p = this.G.multiply(d_) // P = d'⋅G; 0 < d' < n check is done inside const { x, y } = p.assertValidity().toAffine() // validate Point is not at infinity @@ -958,19 +951,19 @@ export class Secp256k1 { /** * Schnorr public key is just `x` coordinate of Point as per BIP340. */ - static pubSchnorr = (secretKey: Bytes): Bytes => { + static pubSchnorr (secretKey: Bytes): Bytes { return this.extpubSchnorr(secretKey).px // d'=int(sk). Fail if d'=0 or d'≥n. Ret bytes(d'⋅G) } static keygenSchnorr: KeygenFn = this.createKeygen(this.pubSchnorr) // Common preparation fn for both sync and async signing - static prepSigSchnorr = (message: Bytes, secretKey: Bytes, auxRand: Bytes) => { + static prepSigSchnorr (message: Bytes, secretKey: Bytes, auxRand: Bytes) { const { px, d } = this.extpubSchnorr(secretKey) return { m: this.abytes(message), px, d, a: this.abytes(auxRand, this.L) } } - static extractK = (rand: Bytes) => { + static extractK (rand: Bytes) { const k_ = this.bytesModN(rand) // Let k' = int(rand) mod n if (k_ === 0n) this.err('sign failed: k is zero') // Fail if k' = 0. const { px, d } = this.extpubSchnorr(this.bigintTo32Bytes(k_)) // Let R = k'⋅G. @@ -978,7 +971,7 @@ export class Secp256k1 { } // Common signature creation helper - static createSigSchnorr = (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes => { + static createSigSchnorr (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes { return this.concatBytes(px, this.bigintTo32Bytes(this.modN(k + e * d))) } @@ -987,7 +980,7 @@ export class Secp256k1 { * Creates Schnorr signature as per BIP340. Verifies itself before returning anything. * auxRand is optional and is not the sole source of k generation: bad CSPRNG won't be dangerous. */ - static signSchnorr = (message: Bytes, secretKey: Bytes, auxRand?: Bytes): Bytes => { + static signSchnorr (message: Bytes, secretKey: Bytes, auxRand?: Bytes): Bytes { auxRand ??= crypto.getRandomValues(new Uint8Array(this.L)) const { m, px, d, a } = this.prepSigSchnorr(message, secretKey, auxRand) const aux = this.taggedHash(this.T_AUX, a) @@ -1027,12 +1020,7 @@ export class Secp256k1 { return res instanceof Promise ? res.then(later) : later(res) } - static _verifSchnorr ( - signature: Bytes, - message: Bytes, - publicKey: Bytes, - challengeFn: (...args: Bytes[]) => bigint | Promise - ): boolean | Promise { + static _verifSchnorr (signature: Bytes, message: Bytes, publicKey: Bytes, challengeFn: (...args: Bytes[]) => bigint | Promise): boolean | Promise { const sig = this.abytes(signature, this.L2, 'signature') const msg = this.abytes(message, undefined, 'message') const pub = this.abytes(publicKey, this.L, 'publicKey') @@ -1121,7 +1109,7 @@ export class Secp256k1 { * * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply(). */ - static wNAF = (n: bigint): { p: Point; f: Point } => { + static wNAF (n: bigint): { p: Point; f: Point } { const comp = this.Gpows || (this.Gpows = this.precompute()) let p = this.I let f = this.G // f must be G, or could become I in the end