]> git.codecow.com Git - libnemo.git/commitdiff
Refactor error handling. Refactor byte array type assertions. Reorganize method order...
authorChris Duncan <chris@zoso.dev>
Sun, 7 Dec 2025 00:42:16 +0000 (16:42 -0800)
committerChris Duncan <chris@zoso.dev>
Tue, 3 Feb 2026 06:05:26 +0000 (22:05 -0800)
src/lib/crypto/secp256k1.ts

index 1bb198d792c070959521b9a6be21dc52a6255ffb..cb78163f3bb8bc9d4dc165388eb44150427c7cd9 100644 (file)
@@ -55,32 +55,22 @@ export class Secp256k1 {
 
        // ## Helpers
        // ----------
-       static err (message = ''): never {
-               const e = new Error(message)
+       static err (message: string, expected?: string, actual?: string): never {
+               const e = new Error(message, { cause: { expected, actual } })
                Error.captureStackTrace?.(e, this.err)
                throw e
        }
 
-       /** Asserts something is Uint8Array. */
-       static abytes (value: unknown, length?: number, title: string = ''): Bytes {
-               function isUint8Array (a: unknown): a is Bytes {
-                       return (a instanceof Uint8Array && a.buffer instanceof ArrayBuffer) || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array')
-               }
-               const isBytes = isUint8Array(value)
-               const needsLen = length !== undefined
-               if (!isBytes || (needsLen && value.length !== length)) {
-                       const prefix = title && `"${title}" `
-                       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
+       /** 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 typeof n === 'bigint' && min <= n && n < max
-                       ? n
-                       : this.err(msg ?? 'bigint out of range')
+               return n < min || max <= n
+                       ? this.err(`${msg ?? 'bigint'} out of range`, `between ${min} and ${max - 1n}`, `${n}`)
+                       : n
        }
 
        /** modular division */
@@ -222,6 +212,15 @@ export class Secp256k1 {
                })
        }
 
+       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++) {
@@ -240,48 +239,6 @@ export class Secp256k1 {
                return this.M(y * y) === this.koblitz(x) ? p : this.err('bad point: not on curve')
        }
 
-       /** Normalize private key to scalar (bigint). Verifies scalar is in range 1<s<N */
-       static secretKeyToScalar (sk: Bytes): bigint {
-               const num = this.bytesToBigint(this.abytes(sk, this.L, 'secret key'))
-               return this.bigintInRange(num, 1n, this.N, 'invalid secret key: outside of range')
-       }
-
-       /** Derives a compressed 33-byte public key from a 32-byte private key. */
-       static getPublicKey (sk: Bytes): Bytes {
-               const p = this.wNAF(this.secretKeyToScalar(sk)).p
-               let { x, y } = this.Affine(this.isValidPoint(p))
-               const len = this.C
-               const pk = new Uint8Array(len)
-               for (let i = len - 1; i >= 1; i--) {
-                       pk[i] = Number(x & 0xffn)
-                       x >>= 8n
-               }
-               pk[0] = this.isEven(y) ? 0x02 : 0x03
-               return this.isValidPublicKey(pk) ? pk : this.err('derived invalid public key from secret key')
-       }
-
-       /** Converts point to 33-byte Uint8Array and checks validity. */
-       static isValidPublicKey (pk: Bytes): boolean {
-               if (pk.length !== this.C) return false
-               const prefix = pk[0]
-               if (prefix !== 0x02 && prefix !== 0x03) return false
-               const evenH = prefix === 0x02
-               try {
-                       this.abytes(pk)
-                       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
-               }
-       }
-
        /**
         * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by
         * caching multiples of G (base point). Cache is stored in 32MB of RAM.
@@ -352,4 +309,68 @@ export class Secp256k1 {
                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')
+       }
 }