From 6534b7d24877f0a376f2fce12494c359f8bc95b1 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 3 Apr 2026 15:28:14 -0700 Subject: [PATCH] Replace fork of noble secp256k1 now that we have a reliable way to bundle dependencies in the vault worker. --- package-lock.json | 10 + package.json | 1 + src/lib/crypto/bip44.ts | 8 +- src/lib/crypto/secp256k1.ts | 377 ------------------------------------ 4 files changed, 15 insertions(+), 381 deletions(-) delete mode 100644 src/lib/crypto/secp256k1.ts diff --git a/package-lock.json b/package-lock.json index d2b6aba..5c8d1b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ledgerhq/hw-transport-web-ble": "^6.33.1", "@ledgerhq/hw-transport-webhid": "^6.34.0", "@ledgerhq/hw-transport-webusb": "^6.33.0", + "@noble/secp256k1": "^3.0.0", "nano-pow": "^5.1.11", "nano25519": "^1.0.1" }, @@ -570,6 +571,15 @@ "integrity": "sha512-v/PLfb1dq1En35kkpbfRWp8jLYgbPUXxGhmd4pmvPSIe0nRGkNTomsZASmWQAv6pRonVGqHIBVlte7j1MBbOww==", "license": "Apache-2.0" }, + "node_modules/@noble/secp256k1": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz", + "integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@puppeteer/browsers": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", diff --git a/package.json b/package.json index f35312f..49e544a 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@ledgerhq/hw-transport-web-ble": "^6.33.1", "@ledgerhq/hw-transport-webhid": "^6.34.0", "@ledgerhq/hw-transport-webusb": "^6.33.0", + "@noble/secp256k1": "^3.0.0", "nano-pow": "^5.1.11", "nano25519": "^1.0.1" }, diff --git a/src/lib/crypto/bip44.ts b/src/lib/crypto/bip44.ts index 010e581..38d748b 100644 --- a/src/lib/crypto/bip44.ts +++ b/src/lib/crypto/bip44.ts @@ -1,7 +1,7 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { Secp256k1 } from '.' +import { Point, getPublicKey as secp256k1_getPublicKey } from '@noble/secp256k1' type ExtendedKey = { privateKey: ArrayBuffer @@ -81,7 +81,7 @@ export class Bip44 { } else if (curve === 'ed25519 seed') { throw new RangeError('Only hardened child keys are supported for ed25519') } else { - data.set(Secp256k1.getPublicKey(pk)) + data.set(secp256k1_getPublicKey(pk)) data.set(this.ser32(index), 33) } return this.hmac(key, data) @@ -92,11 +92,11 @@ export class Bip44 { return ({ privateKey: IL, chainCode: IR }) } else { const ILparsed = this.parse256(new Uint8Array(IL)) - if (ILparsed >= Secp256k1.N) { + if (ILparsed >= Point.CURVE().n) { throw new Error('Invalid child key is greater than the order of the curve') } const pkParsed = this.parse256(pk) - const childKey = (ILparsed + pkParsed) % Secp256k1.N + const childKey = (ILparsed + pkParsed) % Point.CURVE().n if (childKey === 0n) { throw new Error('Invalid child key is zero') } diff --git a/src/lib/crypto/secp256k1.ts b/src/lib/crypto/secp256k1.ts deleted file mode 100644 index 8ac2a4a..0000000 --- a/src/lib/crypto/secp256k1.ts +++ /dev/null @@ -1,377 +0,0 @@ -//! SPDX-FileCopyrightText: 2025 Chris Duncan -//! SPDX-License-Identifier: GPL-3.0-or-later - -/*! - * Fork of `noble-secp256k1` in order to wrap functionality in a class for - * embedding into a Web Worker and to prune methods down to only public key - * derivation for supporting Nano wallet seed functionality from the Exodus app. - */ - -/*! - * noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) - */ - -/** Alias to Uint8Array. */ -type Bytes = Uint8Array - -/** Point in 3d xyz coordinates. */ -type Point = { - X: bigint - Y: bigint - Z: bigint -} - -/** Point in 2d xy affine coordinates. */ -type AffinePoint = { - x: bigint - y: bigint -} - -export class Secp256k1 { - /** - * Curve params. secp256k1 is short weierstrass / koblitz curve. Equation is y² == x³ + ax + b. - * * P = `2n**256n - 2n**32n - 977n` // field over which calculations are done - * * N = `2n**256n - 0x14551231950b75fc4402da1732fc9bebfn` // group order, amount of curve points - * * h = `1n` // cofactor - * * a = `0n` // equation param - * * b = `7n` // equation param - * * Gx, Gy are coordinates of Generator / base point - */ - static P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn - static N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n - static h = 1n - static a = 0n - static b = 7n - static Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n - static Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n - - static L: 32 = 32 // field / group byte length - static C: 33 = 33 // byte length of SEC1 compressed form of coordinate pair - - /** Generator / base point */ - static G: Point = this.Point(this.Gx, this.Gy, 1n) - /** Identity / zero point */ - static I: Point = this.Point(0n, 1n, 0n) - - // ## Helpers - // ---------- - static err (message: string, expected?: string, actual?: string): never { - const e = new Error(message, { cause: { expected, actual } }) - Error.captureStackTrace?.(e, this.err) - throw e - } - - /** Typechecks if a value is a Uint8Array. */ - static isBytes (value: unknown): value is Bytes { - return (value instanceof Uint8Array && value.buffer instanceof ArrayBuffer) - || (ArrayBuffer.isView(value) && value.constructor.name === 'Uint8Array') - } - - static bigintInRange (n: bigint, min: bigint, max: bigint, msg?: string): bigint { - return n < min || max <= n - ? this.err(`${msg ?? 'bigint'} out of range`, `between ${min} and ${max - 1n}`, `${n}`) - : n - } - - /** modular division */ - static M (a: bigint): bigint { - const p = this.P, r = a % p - return r >= 0n ? r : p + r - } - - /** - * Modular inversion using euclidean GCD (non-constant-time). - * No negative exponent for now. - */ - static invert (num: bigint): bigint { - if (num === 0n || this.P <= 0n) this.err(`no inverse n=${num} mod=${this.P}`) - let a = this.M(num), b = this.P, x = 0n, y = 1n, u = 1n, v = 0n - while (a !== 0n) { - const q = b / a, r = b % a - const m = x - u * q, n = y - v * q - b = a, a = r, x = u, y = v, u = m, v = n - } - return b === 1n ? this.M(x) : this.err('no inverse') // b is gcd at this point - } - - // ## End of Helpers - - /** secp256k1 formula. Koblitz curves are subclass of weierstrass curves with a=0, making it x³+b */ - static koblitz = (x: bigint): bigint => this.M(this.M(x * x) * x + this.b) - static isEven = (y: bigint): boolean => (y & 1n) === 0n - /** lift_x from BIP340 calculates square root. Validates x, then validates root*root. */ - static lift_x (x: bigint): bigint { - // Let c = x³ + 7 mod p. Fail if x ≥ p. (also fail if x < 1) - const c = this.koblitz(this.bigintInRange(x, 1n, this.P)) - // c = √y - // y = c^((p+1)/4) mod p - // This formula works for fields p = 3 mod 4 -- a special, fast case. - // Paper: "Square Roots from 1;24,51,10 to Dan Shanks". - let r = 1n - for (let num = c, e = (this.P + 1n) / 4n; e > 0n; e >>= 1n) { - // powMod: modular exponentiation. - if (e & 1n) r = (r * num) % this.P // Uses exponentiation by squaring. - num = (num * num) % this.P // Not constant-time. - } - return this.M(r * r) === c ? r : this.err('sqrt invalid') // check if result is valid - } - - /** - * 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`. - */ - static Add (p: Point, q: Point): Point { - const { X: X1, Y: Y1, Z: Z1 } = p - const { X: X2, Y: Y2, Z: Z2 } = q - const a = this.a - const b3 = this.M(this.b * 3n) - let X3 = 0n, Y3 = 0n, Z3 = 0n - let t0 = this.M(X1 * X2) // step 1 - let t1 = this.M(Y1 * Y2) - let t2 = this.M(Z1 * Z2) - let t3 = this.M(X1 + Y1) - 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) - } - - /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */ - static Affine (p: Point): AffinePoint { - const { X: x, Y: y, Z: z } = p - // fast-paths for ZERO point OR Z=1 - if (this.Equals(p, this.I)) return { x: 0n, y: 0n } - if (z === 1n) return { x, y } - const iz = this.invert(z) - // (Z * Z^-1) must be 1, otherwise bad math - if (this.M(z * iz) !== 1n) this.err('inverse invalid') - // x = X*Z^-1; y = Y*Z^-1 - return { x: this.M(x * iz), y: this.M(y * iz) } - } - - /** Point doubling: P+P */ - static Double (p: Point) { - return this.Add(p, p) - } - - /** Equality check: compare points P&Q. */ - static Equals (p: Point, q: Point): boolean { - const { X: X1, Y: Y1, Z: Z1 } = p - const { X: X2, Y: Y2, Z: Z2 } = q - const X1Z2 = this.M(X1 * Z2) - const X2Z1 = this.M(X2 * Z1) - const Y1Z2 = this.M(Y1 * Z2) - const Y2Z1 = this.M(Y2 * Z1) - return X1Z2 === X2Z1 && Y1Z2 === Y2Z1 - } - - /** Flip point over y coordinate. */ - static Negate (p: Point) { - return this.Point(p.X, this.M(-p.Y), p.Z) - } - - /** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */ - static Point (X: bigint, Y: bigint, Z: bigint): Point { - return Object.freeze({ - X: this.bigintInRange(X, 0n, this.P), - Y: this.bigintInRange(Y, 1n, this.P), // Y can't be 0 in Projective - Z: this.bigintInRange(Z, 0n, this.P), - }) - } - - static bigintToLBytes (b: bigint): Bytes { - const bytes = new Uint8Array(this.L) - for (let i = bytes.byteLength - 1; i >= 0; i--) { - bytes[i] = Number(b & 0xffn) - b >>= 8n - } - return bytes - } - - static bytesToBigint (b: Bytes): bigint { - let int = BigInt(b[0]), len = b.length - for (let i = 1; i < len; i++) { - int <<= 8n - int |= BigInt(b[i]) - } - return int - } - - /** Checks if the point is valid and on-curve. */ - static isValidPoint (p: Point): Point { - const { x, y } = this.Affine(p) // convert to 2d xy affine point. - this.bigintInRange(x, 1n, this.P) // must be in range 1 <= x,y < P - this.bigintInRange(y, 1n, this.P) - // y² == x³ + ax + b, equation sides must be equal - return this.M(y * y) === this.koblitz(x) ? p : this.err('bad point: not on curve') - } - - /** - * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by - * caching multiples of G (base point). Cache is stored in 32MB of RAM. - * Any time `G.multiply` is done, precomputes are used. - * - * w-ary non-adjacent form (wNAF) precomputation method is 10% slower than windowed method, - * but takes 2x less RAM. RAM reduction is possible by utilizing subtraction. - * - * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply(). - */ - static Gpows: Point[] | undefined = undefined // precomputes for base point G - static W = 8 // W is window size - static scalarBits = 256 - static pwindows = Math.ceil(this.scalarBits / this.W) + 1 // 33 for W=8, NOT 32 - see wNAF loop - static pwindowSize = 2 ** (this.W - 1) // 128 for W=8 - static precompute () { - const points: Point[] = [] - let p = this.G - let b = p - for (let w = 0; w < this.pwindows; w++) { - b = p - points.push(b) - for (let i = 1; i < this.pwindowSize; i++) { - b = this.Add(b, p) - points.push(b) - } // i=1, bc we skip 0 - p = this.Double(b) - } - return points - } - // const-time negate - static ctneg (cnd: boolean, p: Point): Point { - const n = this.Negate(p) - return cnd ? n : p - } - 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 - const pow_2_w = 2 ** this.W // 256 for W=8 - const maxNum = pow_2_w // 256 for W=8 - const mask = BigInt(pow_2_w - 1) // 255 for W=8 == mask 0b11111111 - const shiftBy = BigInt(this.W) // 8 for W=8 - for (let w = 0; w < this.pwindows; w++) { - let wbits = Number(n & mask) // extract W bits. - n >>= shiftBy // shift number by W bits. - // We use negative indexes to reduce size of precomputed table by 2x. - // Instead of needing precomputes 0..256, we only calculate them for 0..128. - // If an index > 128 is found, we do (256-index) - where 256 is next window. - // Naive: index +127 => 127, +224 => 224 - // Optimized: index +127 => 127, +224 => 256-32 - if (wbits > this.pwindowSize) { - wbits -= maxNum - n += 1n - } - const offset = w * this.pwindowSize - const offsetF = offset // offsets, evaluate both - const offsetP = offset + Math.abs(wbits) - 1 - const isEven = w % 2 !== 0 // conditions, evaluate both - const isNeg = wbits < 0 - if (wbits === 0) { - // off == I: can't add it. Adding random offF instead. - f = this.Add(f, this.ctneg(isEven, comp[offsetF])) // bits are 0: add garbage to fake point - } else { - p = this.Add(p, this.ctneg(isNeg, comp[offsetP])) // bits are 1: add to result point - } - } - if (n !== 0n) this.err('invalid wnaf') - return { p, f } // return both real and fake points for JIT - } - - /** - * Normalize private key to scalar (bigint). - * Verifies scalar is in range 0