From 7f7d90b5c5d5c3224afb2add37ba02f0eeed365f Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Tue, 2 Dec 2025 14:40:02 -0800 Subject: [PATCH] Remove unused point function. Consolidate error helpers. Consolidate byte assertion. Deprecate redundant Uint8Array construction helper. Convert arrow functions to regular functions. Clarify some function names. Whitespace for legibility. --- src/lib/crypto/secp256k1.ts | 154 +++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 72 deletions(-) diff --git a/src/lib/crypto/secp256k1.ts b/src/lib/crypto/secp256k1.ts index 4f55d80..d468540 100644 --- a/src/lib/crypto/secp256k1.ts +++ b/src/lib/crypto/secp256k1.ts @@ -18,7 +18,6 @@ type Point = { get x (): bigint get y (): bigint equals: (other: Point) => boolean - is0: () => boolean negate: () => Point double: () => Point add: (other: Point) => Point @@ -144,49 +143,50 @@ export class Secp256k1 { // ## Helpers // ---------- - static captureTrace = (...args: Parameters): void => { - if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(...args) - } - } - static err = (message = ''): never => { + static err (message = ''): never { const e = new Error(message) - this.captureTrace(e, this.err) + Error.captureStackTrace?.(e, this.err) throw e } + /** Asserts something is Uint8Array. */ - static isBytes = (a: unknown): a is Uint8Array => - a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array') - static abytes = (value: Bytes, length?: number, title: string = ''): Bytes => { - const bytes = this.isBytes(value) - const len = value?.length + static abytes (value: unknown, length?: number, title: string = ''): Bytes { + function isUint8Array (a: unknown): a is Uint8Array { + return (a instanceof Uint8Array && a.buffer instanceof ArrayBuffer) || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array') + } + const isBytes = isUint8Array(value) const needsLen = length !== undefined - if (!bytes || (needsLen && len !== length)) { + if (!isBytes || (needsLen && value.length !== length)) { const prefix = title && `"${title}" ` - const ofLen = needsLen ? ` of length ${length}` : '' - const got = bytes ? `length=${len}` : `type=${typeof value}` - this.err(prefix + 'expected Uint8Array' + ofLen + ', got ' + got) + const ofLength = needsLen ? ` of length ${length}` : '' + const actual = isBytes ? `length=${value.length}` : `type=${typeof value}` + this.err(prefix + 'expected Uint8Array' + ofLength + ', got ' + actual) } return value } - /** create Uint8Array */ - static u8n = (len: number): Bytes => new Uint8Array(len) - static bytesToHex = (b: Bytes): string => - Array.from(this.abytes(b)).map((e) => e.toString(16).padStart(2, '0')).join('') - static C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 } as const // ASCII characters - static _ch = (ch: number): number | undefined => { + + /** converts bytes to hex string */ + static bytesToHex (b: Bytes): string { + return Array.from(this.abytes(b)).map((e) => e.toString(16).padStart(2, '0')).join('') + } + + // ASCII characters + static C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 } as const + + static _ch (ch: number): number | undefined { if (ch >= this.C._0 && ch <= this.C._9) return ch - this.C._0 // '2' => 50-48 if (ch >= this.C.A && ch <= this.C.F) return ch - (this.C.A - 10) // 'B' => 66-(65-10) if (ch >= this.C.a && ch <= this.C.f) return ch - (this.C.a - 10) // 'b' => 98-(97-10) return } - static hexToBytes = (hex: string): Bytes => { + + static hexToBytes (hex: string): Bytes { const e = 'hex invalid' if (typeof hex !== 'string') return this.err(e) const hl = hex.length const al = hl / 2 if (hl % 2) return this.err(e) - const array = this.u8n(al) + const array = new Uint8Array(al) for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) { // treat each char as ASCII const n1 = this._ch(hex.charCodeAt(hi)) // parse first char, multiply it by 16 @@ -196,25 +196,34 @@ export class Secp256k1 { } return array } + // prettier-ignore - static concatBytes = (...arrs: Bytes[]): Bytes => { - const r = this.u8n(arrs.reduce((sum, a) => sum + this.abytes(a).length, 0)) // create u8a of summed length + static concatBytes (...arrs: Bytes[]): Bytes { + const r = new Uint8Array(arrs.reduce((sum, a) => sum + this.abytes(a).length, 0)) // create u8a of summed length let pad = 0 // walk through each array, arrs.forEach(a => { r.set(a, pad); pad += a.length }) // ensure they have proper type return r } + /** WebCrypto OS-level CSPRNG (random number generator). Will throw when not available. */ static randomBytes = (len: number = this.L): Bytes => { - return crypto.getRandomValues(this.u8n(len)) + return crypto.getRandomValues(new Uint8Array(len)) } - static arange = (n: bigint, min: bigint, max: bigint, msg = 'bad number: out of range'): bigint => - typeof n === 'bigint' && min <= n && n < max ? n : this.err(msg) + + static arange (n: bigint, min: bigint, max: bigint, msg = 'bad number: out of range'): bigint { + return typeof n === 'bigint' && min <= n && n < max + ? n + : this.err(msg) + } + /** modular division */ - static M = (a: bigint, b: bigint = this.P) => { + static M (a: bigint, b: bigint = this.P): bigint { const r = a % b return r >= 0n ? r : b + r } + static modN = (a: bigint) => this.M(a, this.N) + /** Modular inversion using eucledian GCD (non-CT). No negative exponent for now. */ // prettier-ignore static invert = (num: bigint, md: bigint): bigint => { @@ -227,6 +236,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) => { // @ts-ignore const fn = hashes[name] @@ -288,9 +298,6 @@ export class Secp256k1 { 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) @@ -382,9 +389,9 @@ export class Secp256k1 { /** Converts point to 33/65-byte Uint8Array. */ toBytes (isCompressed = true): Bytes { const { x, y } = this.assertValidity().toAffine() - const x32b = secp256k1.numTo32b(x) + const x32b = secp256k1.bigintTo32Bytes(x) if (isCompressed) return secp256k1.concatBytes(secp256k1.getPrefix(y), x32b) - return secp256k1.concatBytes(secp256k1.u8of(0x04), x32b, secp256k1.numTo32b(y)) + return secp256k1.concatBytes(secp256k1.u8of(0x04), x32b, secp256k1.bigintTo32Bytes(y)) }, toHex (isCompressed?: boolean): string { return secp256k1.bytesToHex(this.toBytes(isCompressed)) @@ -397,6 +404,7 @@ export class Secp256k1 { const { x, y } = ap return x === 0n && y === 0n ? this.I : this.Point(x, y, 1n) } + /** Convert Uint8Array or hex string to Point. */ static pointFromBytes (bytes: Bytes): Point { this.abytes(bytes) @@ -429,24 +437,26 @@ export class Secp256k1 { static G: Point = this.Point(this.Gx, this.Gy, 1n) /** Identity / zero point */ 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() - } - static bytesToNumBE = (b: Bytes): bigint => BigInt('0x' + (this.bytesToHex(b) || '0')) - static sliceBytesNumBE = (b: Bytes, from: number, to: number) => this.bytesToNumBE(b.subarray(from, to)) - static B256 = 2n ** 256n // secp256k1 is weierstrass curve. Equation is x³ + ax + b. - /** Number to 32b. Must be 0 <= num < B256. validate, pad, to bytes. */ - static numTo32b = (num: bigint): Bytes => { + + /** `Q = u1⋅G + u2⋅R`. Verifies Q is not ZERO. Unsafe: non-constant-time. */ + static doubleScalarMultiplyUnsafe (R: Point, u1: bigint, u2: bigint): Point { + return this.G.multiplyUnsafe(u1).add(R.multiplyUnsafe(u2)).assertValidity() + } + + static bytesToBigint = (b: Bytes): bigint => BigInt('0x' + (this.bytesToHex(b) || '0')) + static sliceBytesNumBE = (b: Bytes, from: number, to: number) => this.bytesToBigint(b.subarray(from, to)) + + /** Number to 32b. Must be 0 <= num < 2²⁵⁶. validate, pad, to bytes. */ + static bigintTo32Bytes (num: bigint): Bytes { return this.hexToBytes(this - .arange(num, 0n, this.B256) + .arange(num, 0n, 2n ** 256n) // secp256k1 is weierstrass curve. Equation is x³ + ax + b. .toString(16) .padStart(this.L2, '0') ) } /** Normalize private key to scalar (bigint). Verifies scalar is in range 1 { - const num = this.bytesToNumBE(this.abytes(secretKey, this.L, 'secret key')) + const num = this.bytesToBigint(this.abytes(secretKey, this.L, 'secret key')) return this.arange(num, 1n, this.N, 'invalid secret key: outside of range') } /** For Signature malleability, validates sig.s is bigger than N/2. */ @@ -527,7 +537,7 @@ export class Secp256k1 { return this.highS(s) }, toBytes: (format: ECDSASignatureFormat = this.SIG_COMPACT): Bytes => { - const res = this.concatBytes(this.numTo32b(r), this.numTo32b(s)) + const res = this.concatBytes(this.bigintTo32Bytes(r), this.bigintTo32Bytes(s)) if (format === this.SIG_RECOVERED) { this.assertRecoveryBit(recovery) return this.concatBytes(Uint8Array.of(recovery!), res) @@ -558,7 +568,7 @@ export class Secp256k1 { 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.bytesToNumBE(bytes) + const num = this.bytesToBigint(bytes) return delta > 0 ? num >> BigInt(delta) : num } /** int2octets can't be used; pads small msgs with 0: BAD for truncation as per RFC vectors */ @@ -589,15 +599,15 @@ export class Secp256k1 { return async_ ? this.hashes.sha256Async(msg) : this.callHash('sha256')(msg) } - static NULL = this.u8n(0) + static NULL: Bytes = new Uint8Array(0) static byte0 = this.u8of(0x00) static byte1 = this.u8of(0x01) 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 => { - let v = this.u8n(this.L) // Steps B, C of RFC6979 3.2: set hashLen - let k = this.u8n(this.L) // In our case, it's always equal to L + 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 const reset = () => { v.fill(1) @@ -629,8 +639,8 @@ export class Secp256k1 { // Identical to hmacDrbg, but async: uses built-in WebCrypto static hmacDrbgAsync = async (seed: Bytes, pred: Pred): Promise => { - let v = this.u8n(this.L) // Steps B, C of RFC6979 3.2: set hashLen - let k = this.u8n(this.L) // In our case, it's always equal to L + 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 const reset = () => { v.fill(1) @@ -670,7 +680,7 @@ export class Secp256k1 { ): T => { let { lowS, extraEntropy } = opts // generates low-s sigs by default // RFC6979 3.2: we skip step A - const int2octets = this.numTo32b // int to octets + const int2octets = this.bigintTo32Bytes // int to octets const h1i = this.bits2int_modN(messageHash) // msg bigint const h1o = int2octets(h1i) // msg octets const d = this.secretKeyToScalar(secretKey) // validate private key, convert to bigint @@ -730,7 +740,7 @@ export class Secp256k1 { const is = this.invert(s, this.N) // s^-1 const u1 = this.modN(h * is) // u1 = hs^-1 mod n const u2 = this.modN(r * is) // u2 = rs^-1 mod n - const R = this.doubleScalarMulUns(P, u1, u2).toAffine() // R = u1⋅G + u2⋅P + const R = this.doubleScalarMultiplyUnsafe(P, u1, u2).toAffine() // R = u1⋅G + u2⋅P // Stop if R is identity / zero point. Check is done inside `doubleScalarMulUns` const v = this.modN(R.x) // R.x must be in N's field, not P's return v === r // mod(R.x, n) == r @@ -852,12 +862,12 @@ export class Secp256k1 { const radj = recovery === 2 || recovery === 3 ? r + this.N : r this.FpIsValidNot0(radj) // ensure q.x is still a field element const head = this.getPrefix(BigInt(recovery!)) // head is 0x02 or 0x03 - const Rb = this.concatBytes(head, this.numTo32b(radj)) // concat head + r + const Rb = this.concatBytes(head, this.bigintTo32Bytes(radj)) // concat head + r const R = this.pointFromBytes(Rb) const ir = this.invert(radj, this.N) // r^-1 const u1 = this.modN(-h * ir) // -hr^-1 const u2 = this.modN(s * ir) // sr^-1 - const point = this.doubleScalarMulUns(R, u1, u2) // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) + const point = this.doubleScalarMultiplyUnsafe(R, u1, u2) // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) return point.toBytes() } @@ -894,8 +904,8 @@ export class Secp256k1 { static randomSecretKey = (seed = this.randomBytes(this.lengths.seed)) => { this.abytes(seed) if (seed.length < this.lengths.seed || seed.length > 1024) this.err('expected 40-1024b') - const num = this.M(this.bytesToNumBE(seed), this.N - 1n) - return this.numTo32b(num + 1n) + const num = this.M(this.bytesToBigint(seed), this.N - 1n) + return this.bigintTo32Bytes(num + 1n) } static createKeygen = (getPublicKey: (secretKey: Bytes) => Bytes) => (seed?: Bytes): KeysSecPub => { @@ -909,8 +919,8 @@ export class Secp256k1 { hexToBytes: this.hexToBytes as (hex: string) => Bytes, bytesToHex: this.bytesToHex as (bytes: Bytes) => string, concatBytes: this.concatBytes as (...arrs: Bytes[]) => Bytes, - bytesToNumberBE: this.bytesToNumBE as (a: Bytes) => bigint, - numberToBytesBE: this.numTo32b as (n: bigint) => Bytes, + bytesToNumberBE: this.bytesToBigint as (a: Bytes) => bigint, + numberToBytesBE: this.bigintTo32Bytes as (n: bigint) => Bytes, mod: this.M as (a: bigint, md?: bigint) => bigint, invert: this.invert as (num: bigint, md?: bigint) => bigint, // math utilities randomBytes: this.randomBytes as (len?: number) => Bytes, @@ -949,11 +959,11 @@ export class Secp256k1 { 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 const d = this.isEven(y) ? d_ : this.modN(-d_) - const px = this.numTo32b(x) + const px = this.bigintTo32Bytes(x) return { d, px } } - static bytesModN = (bytes: Bytes) => this.modN(this.bytesToNumBE(bytes)) + static bytesModN = (bytes: Bytes) => this.modN(this.bytesToBigint(bytes)) static challenge = (...args: Bytes[]): bigint => this.bytesModN(this.taggedHash(this.T_CHALLENGE, ...args)) static challengeAsync = async (...args: Bytes[]): Promise => this.bytesModN(await this.taggedHashAsync(this.T_CHALLENGE, ...args)) @@ -976,13 +986,13 @@ export class Secp256k1 { 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.numTo32b(k_)) // Let R = k'⋅G. + const { px, d } = this.extpubSchnorr(this.bigintTo32Bytes(k_)) // Let R = k'⋅G. return { rx: px, k: d } } // Common signature creation helper static createSigSchnorr = (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes => { - return this.concatBytes(px, this.numTo32b(this.modN(k + e * d))) + return this.concatBytes(px, this.bigintTo32Bytes(this.modN(k + e * d))) } static E_INVSIG = 'invalid signature produced' @@ -994,7 +1004,7 @@ export class Secp256k1 { const { m, px, d, a } = this.prepSigSchnorr(message, secretKey, auxRand) const aux = this.taggedHash(this.T_AUX, a) // Let t be the byte-wise xor of bytes(d) and hash/aux(a) - const t = this.numTo32b(d ^ this.bytesToNumBE(aux)) + const t = this.bigintTo32Bytes(d ^ this.bytesToBigint(aux)) // Let rand = hash/nonce(t || bytes(P) || m) const rand = this.taggedHash(this.T_NONCE, t, px, m) const { rx, k } = this.extractK(rand) @@ -1014,7 +1024,7 @@ export class Secp256k1 { const { m, px, d, a } = this.prepSigSchnorr(message, secretKey, auxRand) const aux = await this.taggedHashAsync(this.T_AUX, a) // Let t be the byte-wise xor of bytes(d) and hash/aux(a) - const t = this.numTo32b(d ^ this.bytesToNumBE(aux)) + const t = this.bigintTo32Bytes(d ^ this.bytesToBigint(aux)) // Let rand = hash/nonce(t || bytes(P) || m) const rand = await this.taggedHashAsync(this.T_NONCE, t, px, m) const { rx, k } = this.extractK(rand) @@ -1044,22 +1054,22 @@ export class Secp256k1 { try { // lift_x from BIP340. Convert 32-byte x coordinate to elliptic curve point. // Fail if x ≥ p. Let c = x³ + 7 mod p. - const x = this.bytesToNumBE(pub) + const x = this.bytesToBigint(pub) const y = this.lift_x(x) // Let y = c^(p+1)/4 mod p. 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_ = this.Point(x, y_, 1n).assertValidity() - const px = this.numTo32b(P_.toAffine().x) + const px = this.bigintTo32Bytes(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. this.arange(r, 1n, this.P) const s = this.sliceBytesNumBE(sig, this.L, this.L2) // Let s = int(sig[32:64]); fail if s ≥ n. this.arange(s, 1n, this.N) - const i = this.concatBytes(this.numTo32b(r), px, msg) + const i = this.concatBytes(this.bigintTo32Bytes(r), px, msg) // int(challenge(bytes(r)||bytes(P)||m))%n return this.callSyncAsyncFn(challengeFn(i), (e) => { - const { x, y } = this.doubleScalarMulUns(P_, s, this.modN(-e)).toAffine() // R = s⋅G - e⋅P + const { x, y } = this.doubleScalarMultiplyUnsafe(P_, s, this.modN(-e)).toAffine() // R = s⋅G - e⋅P if (!this.isEven(y) || x !== r) return false // -eP == (n-e)P return true // Fail if is_infinite(R) / not has_even_y(R) / x(R) ≠ r. }) -- 2.47.3