get x (): bigint
get y (): bigint
equals: (other: Point) => boolean
- is0: () => boolean
negate: () => Point
double: () => Point
add: (other: Point) => Point
// ## 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
}
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 => {
}
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]
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)
/** 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))
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)
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. */
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)
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 */
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)
// 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)
): 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
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
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()
}
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 => {
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,
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))
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'
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)
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)
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.
})