]> git.codecow.com Git - libnemo.git/commitdiff
Remove unused point function. Consolidate error helpers. Consolidate byte assertion...
authorChris Duncan <chris@zoso.dev>
Tue, 2 Dec 2025 22:40:02 +0000 (14:40 -0800)
committerChris Duncan <chris@zoso.dev>
Tue, 2 Dec 2025 22:40:02 +0000 (14:40 -0800)
src/lib/crypto/secp256k1.ts

index 4f55d806d729fb14628a3cbcced8d926564ae342..d468540911b5bc40eb494021ce7569ab4917b023 100644 (file)
@@ -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<typeof Error.captureStackTrace>): 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<ArrayBuffer> {
+                       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<s<N */
        static secretKeyToScalar = (secretKey: Bytes): bigint => {
-               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>): 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<Bytes>): Promise<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)
@@ -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<bigint> =>
                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.
                        })