// ## 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 */
})
}
+ 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++) {
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.
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')
+ }
}