type MaybePromise<T> = T | Promise<T>
+type EnsureNumber<A extends number> = A extends number ? A : never
+type Length<T extends any[]> = EnsureNumber<T['length']>
+type Tuple<N extends number, T, Accumulator extends T[] = []> = Length<Accumulator> extends N ? Accumulator : Tuple<N, T, [...Accumulator, T]>
+type Add<A extends number, B extends number> = Length<[...Tuple<A, any>, ...Tuple<B, any>]>
+type Subtract<A extends number, B extends number> = Tuple<A, any> extends [...Tuple<B, any>, ...infer Rest,] ? Length<Rest> : never
+type IsLessThanOrEqual<A extends number, B extends number> = Tuple<B, any> extends [...Tuple<A, any>, ...infer _Rest,] ? true : false
+type IsLessThan<A extends number, B extends number> = IsLessThanOrEqual<Add<A, 1>, B>
+type Multiply<A extends number, B extends number> = B extends 0 ? 0 : Add<Multiply<A, Subtract<B, 1>>, A>
+type Quotient<A extends number, B extends number, C extends number = 0> = B extends 0 ? never : B extends 1 ? A : A extends 0 ? 0 : A extends B ? 1 : IsLessThan<A, C> extends true ? never : Multiply<B, C> extends A ? C : Quotient<A, B, Add<C, 1>>
+type Secp256k1Lengths = {
+ publicKey: Add<typeof Secp256k1.L, 1>
+ publicKeyUncompressed: Add<typeof Secp256k1.L2, 1>
+ signature: typeof Secp256k1.L2
+ seed: Add<typeof Secp256k1.L, Quotient<typeof Secp256k1.L, 2>>
+}
+
export class Secp256k1 {
/**
* Curve params. secp256k1 is short weierstrass / koblitz curve. Equation is y² == x³ + ax + b.
Gy: this.Gy,
}
- static L = 32 // field / group byte length
- static L2 = 64
- static lengths = {
- publicKey: this.L + 1,
- publicKeyUncompressed: this.L2 + 1,
- signature: this.L2,
- seed: this.L + this.L / 2,
+ static L: 32 = 32 // field / group byte length
+ static L2: 64 = 64
+ static lengths: Secp256k1Lengths = {
+ publicKey: 33,
+ publicKeyUncompressed: 65,
+ signature: 64,
+ seed: 48,
}
// Helpers and Precomputes sections are reused between libraries
/** Asserts something is Uint8Array. */
static abytes (value: unknown, length?: number, title: string = ''): Bytes {
- function isUint8Array (a: unknown): a is Uint8Array<ArrayBuffer> {
+ 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)
/** Modular inversion using eucledian GCD (non-CT). No negative exponent for now. */
// prettier-ignore
- static invert = (num: bigint, md: bigint): bigint => {
+ static invert (num: bigint, md: bigint): bigint {
if (num === 0n || md <= 0n) this.err('no inverse n=' + num + ' mod=' + md)
let a = this.M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n
while (a !== 0n) {
return b === 1n ? this.M(x, md) : this.err('no inverse') // b is gcd at this point
}
- static callHash = (name: string) => {
+ static callHash (name: string) {
// @ts-ignore
const fn = hashes[name]
if (typeof fn !== 'function') this.err('hashes.' + name + ' not set')
static u8of = (n: number): Bytes => Uint8Array.of(n)
static getPrefix = (y: bigint) => this.u8of(this.isEven(y) ? 0x02 : 0x03)
/** lift_x from BIP340 calculates square root. Validates x, then validates root*root. */
- static lift_x = (x: bigint) => {
+ static lift_x (x: bigint) {
// Let c = x³ + 7 mod p. Fail if x ≥ p. (also fail if x < 1)
const c = this.koblitz(this.FpIsValidNot0(x))
// c = √y
}
/** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */
- static Point = (X: bigint, Y: bigint, Z: bigint): Point => {
+ static Point (X: bigint, Y: bigint, Z: bigint): Point {
const secp256k1 = this
return Object.freeze({
X: this.FpIsValid(X),
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1
},
/** Flip point over y coordinate. */
- negate: (): Point => {
- return this.Point(X, this.M(-Y), Z)
+ negate (): Point {
+ return secp256k1.Point(X, secp256k1.M(-Y), Z)
},
/** Point doubling: P+P, complete formula. */
double (): Point {
*/
// prettier-ignore
add (other: Point): Point {
- const { M, CURVE, Point } = secp256k1
+ const M = (v: bigint): bigint => secp256k1.M(v)
const { X: X1, Y: Y1, Z: Z1 } = { X, Y, Z }
const { X: X2, Y: Y2, Z: Z2 } = other
const a = 0n
- const b = CURVE.b
+ const b = secp256k1.CURVE.b
const b3 = M(b * 3n)
let X3 = 0n, Y3 = 0n, Z3 = 0n
let t0 = M(X1 * X2), t1 = M(Y1 * Y2), t2 = M(Z1 * Z2), t3 = M(X1 + Y1) // step 1
t0 = M(t5 * t4) // step 35
X3 = M(t3 * X3); X3 = M(X3 - t0); t0 = M(t3 * t1); Z3 = M(t5 * Z3)
Z3 = M(Z3 + t0) // step 40
- return Point(X3, Y3, Z3)
+ return secp256k1.Point(X3, Y3, Z3)
},
subtract (other: Point): Point {
return this.add(other.negate())
)
}
/** Normalize private key to scalar (bigint). Verifies scalar is in range 1<s<N */
- static secretKeyToScalar = (secretKey: Bytes): bigint => {
+ static secretKeyToScalar (secretKey: Bytes): bigint {
const num = this.bytesToBigint(this.abytes(secretKey, this.L, 'secret key'))
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
/** Creates 33/65-byte public key from 32-byte private key. */
- static getPublicKey = (privKey: Bytes, isCompressed = true): Bytes => {
+ static getPublicKey (privKey: Bytes, isCompressed = true): Bytes {
return this.G.multiply(this.secretKeyToScalar(privKey)).toBytes(isCompressed)
}
- static isValidSecretKey = (secretKey: Bytes): boolean => {
+ static isValidSecretKey (secretKey: Bytes): boolean {
try {
return !!this.secretKeyToScalar(secretKey)
} catch (error) {
return false
}
}
- static isValidPublicKey = (publicKey: Bytes, isCompressed?: boolean): boolean => {
+ static isValidPublicKey (publicKey: Bytes, isCompressed?: boolean): boolean {
const { publicKey: comp, publicKeyUncompressed } = this.lengths
try {
const l = publicKey.length
}
}
- static assertRecoveryBit = (recovery?: number) => {
+ static assertRecoveryBit (recovery?: number) {
if (![0, 1, 2, 3].includes(recovery!)) this.err('recovery id must be valid and present')
}
- static assertSigFormat = (format?: ECDSASignatureFormat) => {
+ static assertSigFormat (format?: ECDSASignatureFormat) {
if (format != null && !this.ALL_SIG.includes(format))
this.err(`Signature format must be one of: ${this.ALL_SIG.join(', ')}`)
if (format === this.SIG_DER) this.err('Signature format "der" is not supported: switch to noble-curves')
}
- static assertSigLength = (sig: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT) => {
+ static assertSigLength (sig: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT) {
this.assertSigFormat(format)
const SL = this.lengths.signature
const RL = SL + 1
static ALL_SIG = [this.SIG_COMPACT, this.SIG_RECOVERED, this.SIG_DER] as const
/** ECDSA Signature class. Supports only compact 64-byte representation, not DER. */
- static Signature = (r: bigint, s: bigint, recovery?: number) => {
+ static Signature (r: bigint, s: bigint, recovery?: number) {
return Object.freeze({
r: this.FnIsValidNot0(r), // 1 <= r < N
s: this.FnIsValidNot0(s), // 1 <= s < N
* FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits,
* which matches bits2int. bits2int can produce res>N.
*/
- static bits2int = (bytes: Bytes): bigint => {
+ 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.bytesToBigint(bytes)
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 => {
+ static hmacDrbg (seed: Bytes, pred: Pred<Bytes>): Bytes {
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
}
// Identical to hmacDrbg, but async: uses built-in WebCrypto
- static hmacDrbgAsync = async (seed: Bytes, pred: Pred<Bytes>): Promise<Bytes> => {
+ static async hmacDrbgAsync (seed: Bytes, pred: Pred<Bytes>): Promise<Bytes> {
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
// RFC6979 signature generation, preparation step.
// Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.2 & RFC6979.
- static _sign = <T> (
- messageHash: Bytes,
- secretKey: Bytes,
- opts: ECDSASignOpts,
- hmacDrbg: (seed: Bytes, pred: Pred<Bytes>) => T
- ): T => {
+ static _sign<T> (messageHash: Bytes, secretKey: Bytes, opts: ECDSASignOpts, hmacDrbg: (seed: Bytes, pred: Pred<Bytes>) => T): T {
let { lowS, extraEntropy } = opts // generates low-s sigs by default
// RFC6979 3.2: we skip step A
const int2octets = this.bigintTo32Bytes // int to octets
}
// Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.4.
- static _verify = (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}) => {
+ static _verify (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): boolean {
const { lowS, format } = opts
if (sig instanceof this.Signature) this.err('Signature must be in Uint8Array, use .toBytes()')
this.assertSigLength(sig, format)
}
}
- static setDefaults = (opts: ECDSASignOpts): Required<ECDSASignOpts> => {
+ static setDefaults (opts: ECDSASignOpts): Required<ECDSASignOpts> {
const res: ECDSASignOpts = {}
Object.keys(this.defaultSignOpts).forEach((k: string) => {
// @ts-ignore
* sign(msg, secretKey, { format: 'recovered' });
* ```
*/
- static sign = (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes => {
+ static sign (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes {
opts = this.setDefaults(opts)
message = this.prepMsg(message, opts, false) as Bytes
return this._sign(message, secretKey, opts, this.hmacDrbg)
* await signAsync(msg, secretKey, { format: 'recovered' });
* ```
*/
- static signAsync = async (
- message: Bytes,
- secretKey: Bytes,
- opts: ECDSASignOpts = {}
- ): Promise<Bytes> => {
+ static async signAsync (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Promise<Bytes> {
opts = this.setDefaults(opts)
message = await this.prepMsg(message, opts, true)
return this._sign(message, secretKey, opts, this.hmacDrbgAsync)
* verify(sigr, msg, publicKey, { format: 'recovered' });
* ```
*/
- static verify = (
- signature: Bytes,
- message: Bytes,
- publicKey: Bytes,
- opts: ECDSAVerifyOpts = {}
- ): boolean => {
+ static verify (signature: Bytes, message: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): boolean {
opts = this.setDefaults(opts)
message = this.prepMsg(message, opts, false) as Bytes
return this._verify(signature, message, publicKey, opts)
* verify(sigr, msg, publicKey, { format: 'recovered' });
* ```
*/
- static verifyAsync = async (
- sig: Bytes,
- message: Bytes,
- publicKey: Bytes,
- opts: ECDSAVerifyOpts = {}
- ): Promise<boolean> => {
+ static async verifyAsync (sig: Bytes, message: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): Promise<boolean> {
opts = this.setDefaults(opts)
message = await this.prepMsg(message, opts, true)
return this._verify(sig, message, publicKey, opts)
}
- static _recover = (signature: Bytes, messageHash: Bytes) => {
+ static _recover (signature: Bytes, messageHash: Bytes): Bytes {
const sig = this.signatureFromBytes(signature, 'recovered')
const { r, s, recovery } = sig
// 0 or 1 recovery id determines sign of "y" coordinate.
* ECDSA public key recovery. Requires msg hash and recovery id.
* Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.6.
*/
- static recoverPublicKey = (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Bytes => {
+ static recoverPublicKey (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Bytes {
message = this.prepMsg(message, this.setDefaults(opts), false) as Bytes
return this._recover(signature, message)
}
- static recoverPublicKeyAsync = async (
- signature: Bytes,
- message: Bytes,
- opts: ECDSARecoverOpts = {}
- ): Promise<Bytes> => {
+ static async recoverPublicKeyAsync (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Promise<Bytes> {
message = await this.prepMsg(message, this.setDefaults(opts), true)
return this._recover(signature, message)
}
* @param isCompressed 33-byte (true) or 65-byte (false) output
* @returns public key C
*/
- static getSharedSecret = (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes => {
+ static getSharedSecret (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes {
return this.pointFromBytes(publicKeyB).multiply(this.secretKeyToScalar(secretKeyA)).toBytes(isCompressed)
}
// 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?: Bytes) {
+ static randomSecretKey (seed?: Bytes): 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')
static T_AUX = 'aux'
static T_NONCE = 'nonce'
static T_CHALLENGE = 'challenge'
- static taggedHash = (tag: string, ...messages: Bytes[]): Bytes => {
+ static taggedHash (tag: string, ...messages: Bytes[]): Bytes {
const fn = this.callHash('sha256')
const tagH = fn(this.getTag(tag))
return fn(this.concatBytes(tagH, tagH, ...messages))
}
- static taggedHashAsync = async (tag: string, ...messages: Bytes[]): Promise<Bytes> => {
+ static async taggedHashAsync (tag: string, ...messages: Bytes[]): Promise<Bytes> {
const fn = this.hashes.sha256Async
const tagH = await fn(this.getTag(tag))
return await fn(this.concatBytes(tagH, tagH, ...messages))
// ECDSA compact points are 33-byte. Schnorr is 32: we strip first byte 0x02 or 0x03
// Calculate point, scalar and bytes
- static extpubSchnorr = (priv: Bytes) => {
+ static extpubSchnorr (priv: Bytes) {
const d_ = this.secretKeyToScalar(priv)
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
/**
* Schnorr public key is just `x` coordinate of Point as per BIP340.
*/
- static pubSchnorr = (secretKey: Bytes): Bytes => {
+ static pubSchnorr (secretKey: Bytes): Bytes {
return this.extpubSchnorr(secretKey).px // d'=int(sk). Fail if d'=0 or d'≥n. Ret bytes(d'⋅G)
}
static keygenSchnorr: KeygenFn = this.createKeygen(this.pubSchnorr)
// Common preparation fn for both sync and async signing
- static prepSigSchnorr = (message: Bytes, secretKey: Bytes, auxRand: Bytes) => {
+ static prepSigSchnorr (message: Bytes, secretKey: Bytes, auxRand: Bytes) {
const { px, d } = this.extpubSchnorr(secretKey)
return { m: this.abytes(message), px, d, a: this.abytes(auxRand, this.L) }
}
- static extractK = (rand: Bytes) => {
+ 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.bigintTo32Bytes(k_)) // Let R = k'⋅G.
}
// Common signature creation helper
- static createSigSchnorr = (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes => {
+ static createSigSchnorr (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes {
return this.concatBytes(px, this.bigintTo32Bytes(this.modN(k + e * d)))
}
* 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): 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)
return res instanceof Promise ? res.then(later) : later(res)
}
- static _verifSchnorr (
- signature: Bytes,
- message: Bytes,
- publicKey: Bytes,
- challengeFn: (...args: Bytes[]) => bigint | Promise<bigint>
- ): boolean | Promise<boolean> {
+ static _verifSchnorr (signature: Bytes, message: Bytes, publicKey: Bytes, challengeFn: (...args: Bytes[]) => bigint | Promise<bigint>): 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')
*
* !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply().
*/
- static wNAF = (n: bigint): { p: Point; f: Point } => {
+ static wNAF (n: bigint): { p: Point; f: Point } {
const comp = this.Gpows || (this.Gpows = this.precompute())
let p = this.I
let f = this.G // f must be G, or could become I in the end