+++ /dev/null
-//! 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')
- }
-}