]> git.codecow.com Git - libnemo.git/commitdiff
Adjust more names and function signatures. Deprecate CSPRNG wrapper and use API directly.
authorChris Duncan <chris@zoso.dev>
Tue, 2 Dec 2025 23:14:43 +0000 (15:14 -0800)
committerChris Duncan <chris@zoso.dev>
Tue, 2 Dec 2025 23:14:43 +0000 (15:14 -0800)
src/lib/crypto/secp256k1.ts

index d468540911b5bc40eb494021ce7569ab4917b023..64c83c2a6e471fb5eb39b71db54c318a9d72429b 100644 (file)
@@ -173,10 +173,10 @@ export class Secp256k1 {
        // 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)
+       static _char (char: number): number | undefined {
+               if (char >= this.C._0 && char <= this.C._9) return char - this.C._0 // '2' => 50-48
+               if (char >= this.C.A && char <= this.C.F) return char - (this.C.A - 10) // 'B' => 66-(65-10)
+               if (char >= this.C.a && char <= this.C.f) return char - (this.C.a - 10) // 'b' => 98-(97-10)
                return
        }
 
@@ -189,8 +189,8 @@ export class Secp256k1 {
                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
-                       const n2 = this._ch(hex.charCodeAt(hi + 1)) // parse second char
+                       const n1 = this._char(hex.charCodeAt(hi)) // parse first char, multiply it by 16
+                       const n2 = this._char(hex.charCodeAt(hi + 1)) // parse second char
                        if (n1 === undefined || n2 === undefined) return this.err(e)
                        array[ai] = n1 * 16 + n2 // example: 'A9' => 10*16 + 9
                }
@@ -205,15 +205,10 @@ export class Secp256k1 {
                return r
        }
 
-       /** WebCrypto OS-level CSPRNG (random number generator). Will throw when not available. */
-       static randomBytes = (len: number = this.L): Bytes => {
-               return crypto.getRandomValues(new Uint8Array(len))
-       }
-
-       static arange (n: bigint, min: bigint, max: bigint, msg = 'bad number: out of range'): bigint {
+       static bigintInRange (n: bigint, min: bigint, max: bigint, msg?: string): bigint {
                return typeof n === 'bigint' && min <= n && n < max
                        ? n
-                       : this.err(msg)
+                       : this.err(msg ?? 'bigint out of range')
        }
 
        /** modular division */
@@ -249,11 +244,11 @@ export class Secp256k1 {
        /** secp256k1 formula. Koblitz curves are subclass of weierstrass curves with a=0, making it x³+b */
        static koblitz = (x: bigint) => this.M(this.M(x * x) * x + this._b)
        /** assert is element of field mod P (incl. 0) */
-       static FpIsValid = (n: bigint) => this.arange(n, 0n, this.P)
+       static FpIsValid = (n: bigint) => this.bigintInRange(n, 0n, this.P)
        /** assert is element of field mod P (excl. 0) */
-       static FpIsValidNot0 = (n: bigint) => this.arange(n, 1n, this.P)
+       static FpIsValidNot0 = (n: bigint) => this.bigintInRange(n, 1n, this.P)
        /** assert is element of field mod N (excl. 0) */
-       static FnIsValidNot0 = (n: bigint) => this.arange(n, 1n, this.N)
+       static FnIsValidNot0 = (n: bigint) => this.bigintInRange(n, 1n, this.N)
        static isEven = (y: bigint) => (y & 1n) === 0n
        /** create Uint8Array of byte n */
        static u8of = (n: number): Bytes => Uint8Array.of(n)
@@ -449,7 +444,7 @@ export class Secp256k1 {
        /** Number to 32b. Must be 0 <= num < 2²⁵⁶. validate, pad, to bytes. */
        static bigintTo32Bytes (num: bigint): Bytes {
                return this.hexToBytes(this
-                       .arange(num, 0n, 2n ** 256n) // secp256k1 is weierstrass curve. Equation is x³ + ax + b.
+                       .bigintInRange(num, 0n, 2n ** 256n) // secp256k1 is weierstrass curve. Equation is x³ + ax + b.
                        .toString(16)
                        .padStart(this.L2, '0')
                )
@@ -457,7 +452,7 @@ export class Secp256k1 {
        /** Normalize private key to scalar (bigint). Verifies scalar is in range 1<s<N */
        static secretKeyToScalar = (secretKey: Bytes): bigint => {
                const num = this.bytesToBigint(this.abytes(secretKey, this.L, 'secret key'))
-               return this.arange(num, 1n, this.N, 'invalid secret key: outside of range')
+               return this.bigintInRange(num, 1n, this.N, 'invalid secret key: outside of range')
        }
        /** For Signature malleability, validates sig.s is bigger than N/2. */
        static highS = (n: bigint): boolean => n > this.N >> 1n
@@ -549,14 +544,14 @@ export class Secp256k1 {
 
        static signatureFromBytes (b: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT): Signature {
                this.assertSigLength(b, format)
-               let rec: number | undefined
+               let recoveryBit: number | undefined
                if (format === this.SIG_RECOVERED) {
-                       rec = b[0]
+                       recoveryBit = b[0]
                        b = b.subarray(1)
                }
                const r = this.sliceBytesNumBE(b, 0, this.L)
                const s = this.sliceBytesNumBE(b, this.L, this.L2)
-               return this.Signature(r, s, rec)
+               return this.Signature(r, s, recoveryBit)
        }
 
        /**
@@ -581,19 +576,18 @@ export class Secp256k1 {
                extraEntropy: false,
        }
 
-       static _sha = 'SHA-256'
        static hashes = {
                hmacSha256Async: async (key: Bytes, message: Bytes): Promise<Bytes> => {
                        const name = 'HMAC'
-                       const k = await crypto.subtle.importKey('raw', key, { name, hash: { name: this._sha } }, false, ['sign'])
+                       const k = await crypto.subtle.importKey('raw', key, { name, hash: { name: 'SHA-256' } }, false, ['sign'])
                        return new Uint8Array(await crypto.subtle.sign(name, k, message))
                },
                hmacSha256: undefined as undefined | ((key: Bytes, message: Bytes) => Bytes),
-               sha256Async: async (msg: Bytes): Promise<Bytes> => new Uint8Array(await crypto.subtle.digest(this._sha, msg)),
+               sha256Async: async (msg: Bytes): Promise<Bytes> => new Uint8Array(await crypto.subtle.digest('SHA-256', msg)),
                sha256: undefined as undefined | ((message: Bytes) => Bytes),
        }
 
-       static prepMsg = (msg: Bytes, opts: ECDSARecoverOpts, async_: boolean): Bytes | Promise<Bytes> => {
+       static prepMsg (msg: Bytes, opts: ECDSARecoverOpts, async_: boolean): Bytes | Promise<Bytes> {
                this.abytes(msg, undefined, 'message')
                if (!opts.prehash) return msg
                return async_ ? this.hashes.sha256Async(msg) : this.callHash('sha256')(msg)
@@ -689,7 +683,7 @@ export class Secp256k1 {
                if (extraEntropy != null && extraEntropy !== false) {
                        // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
                        // gen random bytes OR pass as-is
-                       const e = extraEntropy === true ? this.randomBytes(this.L) : extraEntropy
+                       const e = extraEntropy === true ? crypto.getRandomValues(new Uint8Array(this.L)) : extraEntropy
                        seedArgs.push(this.abytes(e, undefined, 'extraEntropy')) // check for being bytes
                }
                const seed = this.concatBytes(...seedArgs)
@@ -901,7 +895,8 @@ export class Secp256k1 {
 
        // FIPS 186 B.4.1 compliant key generation produces private keys
        // with modulo bias being neglible. takes >N+16 bytes, returns (hash mod n-1)+1
-       static randomSecretKey = (seed = this.randomBytes(this.lengths.seed)) => {
+       static randomSecretKey (seed?: Bytes) {
+               seed ??= crypto.getRandomValues(new Uint8Array(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.bytesToBigint(seed), this.N - 1n)
@@ -923,7 +918,6 @@ export class Secp256k1 {
                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,
                secretKeyToScalar: this.secretKeyToScalar as typeof this.secretKeyToScalar,
                abytes: this.abytes as typeof this.abytes,
        }
@@ -1000,7 +994,8 @@ export class Secp256k1 {
         * Creates Schnorr signature as per BIP340. Verifies itself before returning anything.
         * auxRand is optional and is not the sole source of k generation: bad CSPRNG won't be dangerous.
         */
-       static signSchnorr = (message: Bytes, secretKey: Bytes, auxRand: Bytes = this.randomBytes(this.L)): Bytes => {
+       static signSchnorr = (message: Bytes, secretKey: Bytes, auxRand?: Bytes): Bytes => {
+               auxRand ??= crypto.getRandomValues(new Uint8Array(this.L))
                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)
@@ -1016,11 +1011,8 @@ export class Secp256k1 {
                return sig
        }
 
-       static signSchnorrAsync = async (
-               message: Bytes,
-               secretKey: Bytes,
-               auxRand: Bytes = this.randomBytes(this.L)
-       ): Promise<Bytes> => {
+       static async signSchnorrAsync (message: Bytes, secretKey: Bytes, auxRand?: Bytes): Promise<Bytes> {
+               auxRand ??= crypto.getRandomValues(new Uint8Array(this.L))
                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)
@@ -1042,12 +1034,12 @@ export class Secp256k1 {
                return res instanceof Promise ? res.then(later) : later(res)
        }
 
-       static _verifSchnorr (
+       static _verifSchnorr (
                signature: Bytes,
                message: Bytes,
                publicKey: Bytes,
                challengeFn: (...args: Bytes[]) => bigint | Promise<bigint>
-       ): boolean | Promise<boolean> => {
+       ): boolean | Promise<boolean> {
                const sig = this.abytes(signature, this.L2, 'signature')
                const msg = this.abytes(message, undefined, 'message')
                const pub = this.abytes(publicKey, this.L, 'publicKey')
@@ -1063,9 +1055,9 @@ export class Secp256k1 {
                        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)
+                       this.bigintInRange(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)
+                       this.bigintInRange(s, 1n, this.N)
                        const i = this.concatBytes(this.bigintTo32Bytes(r), px, msg)
                        // int(challenge(bytes(r)||bytes(P)||m))%n
                        return this.callSyncAsyncFn(challengeFn(i), (e) => {
@@ -1103,7 +1095,7 @@ export class Secp256k1 {
        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 = () => {
+       static precompute () {
                const points: Point[] = []
                let p = this.G
                let b = p
@@ -1120,7 +1112,7 @@ export class Secp256k1 {
        }
        static Gpows: Point[] | undefined = undefined // precomputes for base point G
        // const-time negate
-       static ctneg = (cnd: boolean, p: Point) => {
+       static ctneg (cnd: boolean, p: Point) {
                const n = p.negate()
                return cnd ? n : p
        }
@@ -1156,16 +1148,16 @@ export class Secp256k1 {
                                wbits -= maxNum
                                n += 1n
                        }
-                       const off = w * this.pwindowSize
-                       const offF = off // offsets, evaluate both
-                       const offP = off + Math.abs(wbits) - 1
+                       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 = f.add(this.ctneg(isEven, comp[offF])) // bits are 0: add garbage to fake point
+                               f = f.add(this.ctneg(isEven, comp[offsetF])) // bits are 0: add garbage to fake point
                        } else {
-                               p = p.add(this.ctneg(isNeg, comp[offP])) // bits are 1: add to result point
+                               p = p.add(this.ctneg(isNeg, comp[offsetP])) // bits are 1: add to result point
                        }
                }
                if (n !== 0n) this.err('invalid wnaf')