]> git.codecow.com Git - libnemo.git/commitdiff
Replace fork of noble secp256k1 now that we have a reliable way to bundle dependencie...
authorChris Duncan <chris@zoso.dev>
Fri, 3 Apr 2026 22:28:14 +0000 (15:28 -0700)
committerChris Duncan <chris@zoso.dev>
Fri, 3 Apr 2026 22:28:14 +0000 (15:28 -0700)
package-lock.json
package.json
src/lib/crypto/bip44.ts
src/lib/crypto/secp256k1.ts [deleted file]

index d2b6aba21c8c10c722ebf17c572665dc6ba26c30..5c8d1b921ae90aa96a08b570ddb6c0dc9cc2cd2d 100644 (file)
@@ -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"
                        },
                        "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",
index f35312fa6065efd8605f9d36337ba477460f49fc..49e544a9cae30e091a3142a1d6aad79fc7f3fa2c 100644 (file)
@@ -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"
        },
index 010e5819859f6fd0969e8dff9dae342d7b0756f3..38d748b1c493f61fb2b7cd0f667350f25774d3f7 100644 (file)
@@ -1,7 +1,7 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
 //! 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 (file)
index 8ac2a4a..0000000
+++ /dev/null
@@ -1,377 +0,0 @@
-//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
-//! 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<ArrayBuffer>
-
-/** 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<s<N
-        */
-       static secretKeyToScalar (sk: unknown): bigint {
-               if (this.isBytes(sk)) {
-                       if (sk.byteLength !== this.L) {
-                               this.err('secret key byte length', `${this.L}`, `${sk.byteLength}`)
-                       }
-                       sk = this.bytesToBigint(sk)
-               }
-               if (typeof sk !== 'bigint') {
-                       this.err('secret key type', 'bigint or Uint8Array', typeof sk)
-               }
-               return this.bigintInRange(sk, 1n, this.N, 'secret key')
-       }
-
-       /**
-        * Converts an XY-coordinate affine point representing the public key to the
-        * 33-byte compresssed form described by SEC1.
-        */
-       static compressPublicKey (p: AffinePoint): Bytes {
-               const prefix = Number(p.y & 1n) + 2
-               const bytes = this.bigintToLBytes(p.x)
-               const pk = new Uint8Array([prefix, ...bytes])
-               return pk
-       }
-
-       /**
-        * Recalculates y-coordinate from 33-byte SEC1 compressed form of x-coordinate
-        * and checks whether the resulting point is valid and on the curve.
-        */
-       static isValidPublicKey (pk: Bytes): boolean {
-               if (!this.isBytes(pk) || pk.length !== this.C) return false
-               const prefix = pk[0]
-               if (prefix !== 0x02 && prefix !== 0x03) return false
-               const evenH = prefix === 0x02
-               try {
-                       const x = this.bytesToBigint(pk.subarray(1))
-                       // Equation is y² == x³ + ax + b. We calculate y from x.
-                       // y = √y²; there are two solutions: y, -y. Determine proper solution based on SEC1 prefix
-                       let y = this.lift_x(x)
-                       const evenY = this.isEven(y)
-                       if (evenH !== evenY) y = this.M(-y)
-                       const p = this.Point(x, y, 1n)
-                       // Validate point
-                       return !!this.isValidPoint(p)
-               } catch (error) {
-                       return false
-               }
-       }
-
-       /**
-        * Derives the 33-byte SEC1 compressed form of a public key from a 32-byte
-        * private key.
-        */
-       static getPublicKey (sk: bigint | Bytes): Bytes
-       static getPublicKey (sk: unknown): Bytes {
-               const { p } = this.wNAF(this.secretKeyToScalar(sk))
-               const ap = this.Affine(this.isValidPoint(p))
-               const pk = this.compressPublicKey(ap)
-               return this.isValidPublicKey(pk) ? pk : this.err('derived invalid public key from secret key')
-       }
-}