From: Chris Duncan Date: Mon, 24 Nov 2025 08:52:57 +0000 (-0800) Subject: Start forking noble secp256k1 for worker embedding. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=e0cfc027703e544ee77ba9a6cb930c0424efc81f;p=libnemo.git Start forking noble secp256k1 for worker embedding. --- diff --git a/src/lib/crypto/bip44.ts b/src/lib/crypto/bip44.ts index 1007f17..3e25626 100644 --- a/src/lib/crypto/bip44.ts +++ b/src/lib/crypto/bip44.ts @@ -1,7 +1,7 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { getPublicKey } from '@noble/secp256k1' +import * as secp256k1 from '.' type ExtendedKey = { privateKey: ArrayBuffer @@ -71,9 +71,9 @@ export class Bip44 { } static CKDpriv (curve: Curve, { privateKey, chainCode }: ExtendedKey, index?: number): Promise { - console.log(index) - console.log([...(new Uint8Array(privateKey))].map(v => v.toString(16).padStart(2, '0')).join('')) - console.log([...(new Uint8Array(chainCode))].map(v => v.toString(16).padStart(2, '0')).join('')) + // console.log(index) + // console.log([...(new Uint8Array(privateKey))].map(v => v.toString(16).padStart(2, '0')).join('')) + // console.log([...(new Uint8Array(chainCode))].map(v => v.toString(16).padStart(2, '0')).join('')) if (index === undefined) { return Promise.resolve({ privateKey, chainCode }) } @@ -87,7 +87,7 @@ export class Bip44 { } else if (curve === 'ed25519 seed') { throw new RangeError('Only hardened child keys are supported for ed25519') } else { - data.set(getPublicKey(pk)) + data.set(secp256k1.getPublicKey(pk)) data.set(this.ser32(index), 33) } return this.hmac(key, data) diff --git a/src/lib/crypto/index.ts b/src/lib/crypto/index.ts index 6c61c9f..c0f3d8c 100644 --- a/src/lib/crypto/index.ts +++ b/src/lib/crypto/index.ts @@ -5,6 +5,7 @@ import { Bip39 } from './bip39' import { Bip44 } from './bip44' import { Blake2b } from './blake2b' import { NanoNaCl } from './nano-nacl' +import { Secp256k1 } from './secp256k1' import { WalletAesGcm } from './wallet-aes-gcm' -export { Bip39, Bip44, Blake2b, NanoNaCl, WalletAesGcm } +export { Bip39, Bip44, Blake2b, NanoNaCl, Secp256k1, WalletAesGcm } diff --git a/src/lib/crypto/secp256k1.ts b/src/lib/crypto/secp256k1.ts new file mode 100644 index 0000000..8e615f9 --- /dev/null +++ b/src/lib/crypto/secp256k1.ts @@ -0,0 +1,1185 @@ +/*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ + +import { Point, Signature, hashes, signAsync, verifyAsync, recoverPublicKey, recoverPublicKeyAsync, getSharedSecret, getPublicKey, etc, utils } from "@noble/secp256k1" +import { randomBytes, sign, verify } from "crypto" +import { format } from "path" +import { _verify } from "../wallet/verify" + +/** + * 5KB JS implementation of secp256k1 ECDSA / Schnorr signatures & ECDH. + * Compliant with RFC6979 & BIP340. + * @module + */ + +/** + * Fork rereleased under GPLv3. + * Functionality wrapped in a class for embedding into a Web Worker. + */ + +/** Alias to Uint8Array. */ +export type Bytes = Uint8Array +/** Signature instance, which allows recovering pubkey from it. */ +export type RecoveredSignature = Secp256k1['Signature'] & { recovery: number } +/** Weierstrass elliptic curve options. */ +export type WeierstrassOpts = Readonly<{ + p: bigint + n: bigint + h: bigint + a: T + b: T + Gx: T + Gy: T +}> +declare const globalThis: Record | undefined // Typescript symbol present in browsers +/** Point in 2d xy affine coordinates. */ +export type AffinePoint = { + x: bigint + y: bigint +} +export type ECDSAExtraEntropy = boolean | Bytes +/** + * - `compact` is the default format + * - `recovered` is the same as compact, but with an extra byte indicating recovery byte + * - `der` is not supported; and provided for consistency. + * Switch to noble-curves if you need der. + */ +export type ECDSASignatureFormat = 'compact' | 'recovered' | 'der' +/** + * - `prehash`: (default: true) indicates whether to do sha256(message). + * When a custom hash is used, it must be set to `false`. + */ +export type ECDSARecoverOpts = { + prehash?: boolean +} +/** + * - `prehash`: (default: true) indicates whether to do sha256(message). + * When a custom hash is used, it must be set to `false`. + * - `lowS`: (default: true) prohibits signatures which have (sig.s >= CURVE.n/2n). + * Compatible with BTC/ETH. Setting `lowS: false` allows to create malleable signatures, + * which is default openssl behavior. + * Non-malleable signatures can still be successfully verified in openssl. + * - `format`: (default: 'compact') 'compact' or 'recovered' with recovery byte + */ +export type ECDSAVerifyOpts = { + prehash?: boolean + lowS?: boolean + format?: ECDSASignatureFormat +} +/** + * - `prehash`: (default: true) indicates whether to do sha256(message). + * When a custom hash is used, it must be set to `false`. + * - `lowS`: (default: true) prohibits signatures which have (sig.s >= CURVE.n/2n). + * Compatible with BTC/ETH. Setting `lowS: false` allows to create malleable signatures, + * which is default openssl behavior. + * Non-malleable signatures can still be successfully verified in openssl. + * - `format`: (default: 'compact') 'compact' or 'recovered' with recovery byte + * - `extraEntropy`: (default: false) creates sigs with increased security, see {@link ECDSAExtraEntropy} + */ +export type ECDSASignOpts = { + prehash?: boolean + lowS?: boolean + format?: ECDSASignatureFormat + extraEntropy?: ECDSAExtraEntropy +} +type Pred = (v: Bytes) => T | undefined + +export class Secp256k1 { + /** + * Curve params. secp256k1 is short weierstrass / koblitz curve. Equation is y² == x³ + ax + b. + * * P = `2n**256n-2n**32n-2n**977n` // field over which calculations are done + * * N = `2n**256n - 0x14551231950b75fc4402da1732fc9bebfn` // group order, amount of curve points + * * h = `1n` // cofactor + * * a = `0n` // equation param + * * b = `7n` // equation param + * * Gx, Gy are coordinates of Generator / base point + */ + static P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn + static N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n + static h = 1n + static a = 0n + static _b = 7n + static Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n + static Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n + + static secp256k1_CURVE: WeierstrassOpts = { + p: this.P, + n: this.N, + h: 1n, + a: 0n, + b: this._b, + Gx: this.Gx, + 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, + } + + // Helpers and Precomputes sections are reused between libraries + + // ## Helpers + // ---------- + static captureTrace = (...args: Parameters): void => { + if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(...args) + } + } + static err = (message = ''): never => { + const e = new Error(message) + this.captureTrace(e, this.err) + throw e + } + static isBig = (n: unknown): n is bigint => typeof n === 'bigint' // is big integer + static isStr = (s: unknown): s is string => typeof s === 'string' // is string + static isBytes = (a: unknown): a is Uint8Array => + a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array') + /** Asserts something is Uint8Array. */ + static abytes = (value: Bytes, length?: number, title: string = ''): Bytes => { + const bytes = this.isBytes(value) + const len = value?.length + const needsLen = length !== undefined + if (!bytes || (needsLen && len !== 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) + } + return value + } + /** create Uint8Array */ + static u8n = (len: number): Bytes => new Uint8Array(len) + static padh = (n: number | bigint, pad: number) => n.toString(16).padStart(pad, '0') + static bytesToHex = (b: Bytes): string => + Array.from(this.abytes(b)) + .map((e) => this.padh(e, 2)) + .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 => { + 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 => { + const e = 'hex invalid' + if (!this.isStr(hex)) return this.err(e) + const hl = hex.length + const al = hl / 2 + if (hl % 2) return this.err(e) + const array = this.u8n(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 + if (n1 === undefined || n2 === undefined) return this.err(e) + array[ai] = n1 * 16 + n2 // example: 'A9' => 10*16 + 9 + } + return array + } + static cr = () => globalThis?.crypto // WebCrypto is available in all modern environments + static subtle = () => this.cr()?.subtle ?? this.err('crypto.subtle must be defined, consider polyfill') + // 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 + 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 => { + const c = this.cr() + return c.getRandomValues(this.u8n(len)) + } + static big = BigInt + static arange = (n: bigint, min: bigint, max: bigint, msg = 'bad number: out of range'): bigint => + this.isBig(n) && min <= n && n < max ? n : this.err(msg) + /** modular division */ + static M = (a: bigint, b: bigint = this.P) => { + 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 => { + 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) { + const q = b / a, r = b % a + const m = x - u * q, n = y - v * q + b = a, a = r, x = u, y = v, u = m, v = n + } + 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] + if (typeof fn !== 'function') this.err('hashes.' + name + ' not set') + return fn + } + static hash = (msg: Bytes): Bytes => this.callHash('sha256')(msg) + static apoint = (p: unknown) => (p instanceof this.Point ? p : this.err('Point expected')) + // ## End of Helpers + // ----------------- + + /** 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) + /** assert is element of field mod P (excl. 0) */ + static FpIsValidNot0 = (n: bigint) => this.arange(n, 1n, this.P) + /** assert is element of field mod N (excl. 0) */ + static FnIsValidNot0 = (n: bigint) => this.arange(n, 1n, this.N) + static isEven = (y: bigint) => (y & 1n) === 0n + /** create Uint8Array of byte n */ + 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) => { + // Let c = x³ + 7 mod p. Fail if x ≥ p. (also fail if x < 1) + const c = this.koblitz(this.FpIsValidNot0(x)) + // c = √y + // y = c^((p+1)/4) mod p + // This formula works for fields p = 3 mod 4 -- a special, fast case. + // Paper: "Square Roots from 1;24,51,10 to Dan Shanks". + let r = 1n + for (let num = c, e = (this.P + 1n) / 4n; e > 0n; e >>= 1n) { + // powMod: modular exponentiation. + if (e & 1n) r = (r * num) % this.P // Uses exponentiation by squaring. + num = (num * num) % this.P // Not constant-time. + } + return this.M(r * r) === c ? r : this.err('sqrt invalid') // check if result is valid + } + + /** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */ + static Point = class { + static BASE: Secp256k1['Point'] + static ZERO: Secp256k1['Point'] + readonly X: bigint + readonly Y: bigint + readonly Z: bigint + constructor (X: bigint, Y: bigint, Z: bigint) { + this.X = Secp256k1.FpIsValid(X) + this.Y = Secp256k1.FpIsValidNot0(Y) // Y can't be 0 in Projective + this.Z = Secp256k1.FpIsValid(Z) + Object.freeze(this) + } + static CURVE (): WeierstrassOpts { + return Secp256k1.secp256k1_CURVE + } + /** Create 3d xyz point from 2d xy. (0, 0) => (0, 1, 0), not (0, 0, 1) */ + static fromAffine (ap: AffinePoint): Point { + const { x, y } = ap + return x === 0n && y === 0n ? Secp256k1.I : new Secp256k1.Point(x, y, 1n) + } + /** Convert Uint8Array or hex string to Point. */ + static fromBytes (bytes: Bytes): Point { + Secp256k1.abytes(bytes) + const { publicKey: comp, publicKeyUncompressed: uncomp } = Secp256k1.lengths // e.g. for 32-byte: 33, 65 + let p: Point | undefined = undefined + const length = bytes.length + const head = bytes[0] + const tail = bytes.subarray(1) + const x = Secp256k1.sliceBytesNumBE(tail, 0, Secp256k1.L) + // No actual validation is done here: use .assertValidity() + if (length === comp && (head === 0x02 || head === 0x03)) { + // Equation is y² == x³ + ax + b. We calculate y from x. + // y = √y²; there are two solutions: y, -y. Determine proper solution based on prefix + let y = Secp256k1.lift_x(x) + const evenY = Secp256k1.isEven(y) + const evenH = Secp256k1.isEven(Secp256k1.big(head)) + if (evenH !== evenY) y = Secp256k1.M(-y) + p = new Point(x, y, 1n) + } + // Uncompressed 65-byte point, 0x04 prefix + if (length === uncomp && head === 0x04) p = new Point(x, Secp256k1.sliceBytesNumBE(tail, Secp256k1.L, Secp256k1.L2), 1n) + // Validate point + return p ? p.assertValidity() : Secp256k1.err('bad point: not on curve') + } + static fromHex (hex: string): Point { + return Secp256k1.Point.fromBytes(Secp256k1.hexToBytes(hex)) + } + get x (): bigint { + return this.toAffine().x + } + get y (): bigint { + return this.toAffine().y + } + /** Equality check: compare points P&Q. */ + equals (other: Point): boolean { + const { X: X1, Y: Y1, Z: Z1 } = this + const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other) // checks class equality + const X1Z2 = Secp256k1.M(X1 * Z2) + const X2Z1 = Secp256k1.M(X2 * Z1) + const Y1Z2 = Secp256k1.M(Y1 * Z2) + const Y2Z1 = Secp256k1.M(Y2 * Z1) + return X1Z2 === X2Z1 && Y1Z2 === Y2Z1 + } + is0 (): boolean { + return this.equals(I) + } + /** Flip point over y coordinate. */ + negate (): Point { + return new Point(this.X, Secp256k1.M(-this.Y), this.Z) + } + /** Point doubling: P+P, complete formula. */ + double (): Point { + return this.add(this) + } + /** + * Point addition: P+Q, complete, exception-free formula + * (Renes-Costello-Batina, algo 1 of [2015/1060](https://eprint.iacr.org/2015/1060)). + * Cost: `12M + 0S + 3*a + 3*b3 + 23add`. + */ + // prettier-ignore + add (other: Point): Point { + const { X: X1, Y: Y1, Z: Z1 } = this + const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other) + const a = 0n + const b = Secp256k1._b + let X3 = 0n, Y3 = 0n, Z3 = 0n + const b3 = Secp256k1.M(b * 3n) + let t0 = Secp256k1.M(X1 * X2), t1 = Secp256k1.M(Y1 * Y2), t2 = Secp256k1.M(Z1 * Z2), t3 = Secp256k1.M(X1 + Y1) // step 1 + let t4 = Secp256k1.M(X2 + Y2) // step 5 + t3 = Secp256k1.M(t3 * t4); t4 = Secp256k1.M(t0 + t1); t3 = Secp256k1.M(t3 - t4); t4 = Secp256k1.M(X1 + Z1) + let t5 = Secp256k1.M(X2 + Z2) // step 10 + t4 = Secp256k1.M(t4 * t5); t5 = Secp256k1.M(t0 + t2); t4 = Secp256k1.M(t4 - t5); t5 = Secp256k1.M(Y1 + Z1) + X3 = Secp256k1.M(Y2 + Z2) // step 15 + t5 = Secp256k1.M(t5 * X3); X3 = Secp256k1.M(t1 + t2); t5 = Secp256k1.M(t5 - X3); Z3 = Secp256k1.M(a * t4) + X3 = Secp256k1.M(b3 * t2) // step 20 + Z3 = Secp256k1.M(X3 + Z3); X3 = Secp256k1.M(t1 - Z3); Z3 = Secp256k1.M(t1 + Z3); Y3 = Secp256k1.M(X3 * Z3) + t1 = Secp256k1.M(t0 + t0) // step 25 + t1 = Secp256k1.M(t1 + t0); t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(b3 * t4); t1 = Secp256k1.M(t1 + t2) + t2 = Secp256k1.M(t0 - t2) // step 30 + t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(t4 + t2); t0 = Secp256k1.M(t1 * t4); Y3 = Secp256k1.M(Y3 + t0) + t0 = Secp256k1.M(t5 * t4) // step 35 + X3 = Secp256k1.M(t3 * X3); X3 = Secp256k1.M(X3 - t0); t0 = Secp256k1.M(t3 * t1); Z3 = Secp256k1.M(t5 * Z3) + Z3 = Secp256k1.M(Z3 + t0) // step 40 + return new Point(X3, Y3, Z3) + } + subtract (other: Point): Point { + return this.add(Secp256k1.apoint(other).negate()) + } + /** + * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n. + * Uses {@link wNAF} for base point. + * Uses fake point to mitigate side-channel leakage. + * @param n scalar by which point is multiplied + * @param safe safe mode guards against timing attacks; unsafe mode is faster + */ + multiply (n: bigint, safe = true): Point { + if (!safe && n === 0n) return Secp256k1.I + Secp256k1.FnIsValidNot0(n) + if (n === 1n) return this + if (this.equals(Secp256k1.G)) return wNAF(n).p + // init result point & fake point + let p = Secp256k1.I + let f = Secp256k1.G + for (let d: Point = this; n > 0n; d = d.double(), n >>= 1n) { + // if bit is present, add to point + // if not present, add to fake, for timing safety + if (n & 1n) p = p.add(d) + else if (safe) f = f.add(d) + } + return p + } + multiplyUnsafe (scalar: bigint): Point { + return this.multiply(scalar, false) + } + /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */ + toAffine (): AffinePoint { + const { X: x, Y: y, Z: z } = this + // fast-paths for ZERO point OR Z=1 + if (this.equals(I)) return { x: 0n, y: 0n } + if (z === 1n) return { x, y } + const iz = Secp256k1.invert(z, Secp256k1.P) + // (Z * Z^-1) must be 1, otherwise bad math + if (Secp256k1.M(z * iz) !== 1n) Secp256k1.err('inverse invalid') + // x = X*Z^-1; y = Y*Z^-1 + return { x: Secp256k1.M(x * iz), y: Secp256k1.M(y * iz) } + } + /** Checks if the point is valid and on-curve. */ + assertValidity (): Point { + const { x, y } = this.toAffine() // convert to 2d xy affine point. + Secp256k1.FpIsValidNot0(x) // must be in range 1 <= x,y < P + Secp256k1.FpIsValidNot0(y) + // y² == x³ + ax + b, equation sides must be equal + return Secp256k1.M(y * y) === Secp256k1.koblitz(x) ? this : Secp256k1.err('bad point: not on curve') + } + /** Converts point to 33/65-byte Uint8Array. */ + toBytes (isCompressed = true): Bytes { + const { x, y } = this.assertValidity().toAffine() + const x32b = Secp256k1.numTo32b(x) + if (isCompressed) return Secp256k1.concatBytes(Secp256k1.getPrefix(y), x32b) + return Secp256k1.concatBytes(Secp256k1.u8of(0x04), x32b, Secp256k1.numTo32b(y)) + } + + toHex (isCompressed?: boolean): string { + return Secp256k1.bytesToHex(this.toBytes(isCompressed)) + } + } + /** Generator / base point */ + static G: Point = new Point(this.Gx, this.Gy, 1n) + /** Identity / zero point */ + static I: Point = new Point(0n, 1n, 0n) + // Static aliases + static { + this.Point.BASE = this.G + this.Point.ZERO = this.I + } + /** `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 => this.big('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 => this.hexToBytes(this.padh(this.arange(num, 0n, this.B256), this.L2)) + /** Normalize private key to scalar (bigint). Verifies scalar is in range 1 { + const num = this.bytesToNumBE(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. */ + 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 => { + return this.G.multiply(this.secretKeyToScalar(privKey)).toBytes(isCompressed) + } + + static isValidSecretKey = (secretKey: Bytes): boolean => { + try { + return !!this.secretKeyToScalar(secretKey) + } catch (error) { + return false + } + } + static isValidPublicKey = (publicKey: Bytes, isCompressed?: boolean): boolean => { + const { publicKey: comp, publicKeyUncompressed } = this.lengths + try { + const l = publicKey.length + if (isCompressed === true && l !== comp) return false + if (isCompressed === false && l !== publicKeyUncompressed) return false + return !!this.Point.fromBytes(publicKey) + } catch (error) { + return false + } + } + + static assertRecoveryBit = (recovery?: number) => { + if (![0, 1, 2, 3].includes(recovery!)) this.err('recovery id must be valid and present') + } + static assertSigFormat = (format?: ECDSASignatureFormat) => { + if (format != null && !ALL_SIG.includes(format)) + err(`Signature format must be one of: ${ALL_SIG.join(', ')}`) + if (format === SIG_DER) this.err('Signature format "der" is not supported: switch to noble-curves') + } + static assertSigLength = (sig: Bytes, format: ECDSASignatureFormat = SIG_COMPACT) => { + this.assertSigFormat(format) + const SL = this.lengths.signature + const RL = SL + 1 + let msg = `Signature format "${format}" expects Uint8Array with length ` + if (format === SIG_COMPACT && sig.length !== SL) this.err(msg + SL) + if (format === SIG_RECOVERED && sig.length !== RL) this.err(msg + RL) + } + /** ECDSA Signature class. Supports only compact 64-byte representation, not DER. */ + static Signature = class { + readonly r: bigint + readonly s: bigint + readonly recovery?: number + constructor (r: bigint, s: bigint, recovery?: number) { + this.r = Secp256k1.FnIsValidNot0(r) // 1 <= r < N + this.s = Secp256k1.FnIsValidNot0(s) // 1 <= s < N + if (recovery != null) this.recovery = recovery + Object.freeze(this) + } + static fromBytes (b: Bytes, format: ECDSASignatureFormat = SIG_COMPACT): Secp256k1['Signature'] { + Secp256k1.assertSigLength(b, format) + let rec: number | undefined + if (format === SIG_RECOVERED) { + rec = b[0] + b = b.subarray(1) + } + const r = Secp256k1.sliceBytesNumBE(b, 0, Secp256k1.L) + const s = Secp256k1.sliceBytesNumBE(b, Secp256k1.L, Secp256k1.L2) + return new Signature(r, s, rec) + } + addRecoveryBit (bit: number): RecoveredSignature { + return new Signature(this.r, this.s, bit) as RecoveredSignature + } + hasHighS (): boolean { + return Secp256k1.highS(this.s) + } + toBytes (format: ECDSASignatureFormat = SIG_COMPACT): Bytes { + const { r, s, recovery } = this + const res = Secp256k1.concatBytes(Secp256k1.numTo32b(r), Secp256k1.numTo32b(s)) + if (format === SIG_RECOVERED) { + Secp256k1.assertRecoveryBit(recovery) + return Secp256k1.concatBytes(Uint8Array.of(recovery!), res) + } + return res + } + } + + /** + * RFC6979: ensure ECDSA msg is X bytes, convert to BigInt. + * RFC suggests optional truncating via bits2octets. + * 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 => { + 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) + return delta > 0 ? num >> this.big(delta) : num + } + /** int2octets can't be used; pads small msgs with 0: BAD for truncation as per RFC vectors */ + static bits2int_modN = (bytes: Bytes): bigint => this.modN(this.bits2int(this.abytes(bytes))) + /** + * Option to enable hedged signatures with improved security. + * + * * Randomly generated k is bad, because broken CSPRNG would leak private keys. + * * Deterministic k (RFC6979) is better; but is suspectible to fault attacks. + * + * We allow using technique described in RFC6979 3.6: additional k', a.k.a. adding randomness + * to deterministic sig. If CSPRNG is broken & randomness is weak, it would STILL be as secure + * as ordinary sig without ExtraEntropy. + * + * * `true` means "fetch data, from CSPRNG, incorporate it into k generation" + * * `false` means "disable extra entropy, use purely deterministic k" + * * `Uint8Array` passed means "incorporate following data into k generation" + * + * https://paulmillr.com/posts/deterministic-signatures/ + */ + // todo: better name + static SIG_COMPACT: ECDSASignatureFormat = 'compact' + static SIG_RECOVERED = 'recovered' + static SIG_DER = 'der' + static ALL_SIG = [this.SIG_COMPACT, this.SIG_RECOVERED, this.SIG_DER] as const + + static defaultSignOpts: ECDSASignOpts = { + lowS: true, + prehash: true, + format: this.SIG_COMPACT, + extraEntropy: false, + } + + static _sha = 'SHA-256' + static hashes = { + hmacSha256Async: async (key: Bytes, message: Bytes): Promise => { + const s = this.subtle() + const name = 'HMAC' + const k = await s.importKey('raw', key, { name, hash: { name: this._sha } }, false, ['sign']) + return this.u8n(await s.sign(name, k, message)) + }, + hmacSha256: undefined as undefined | ((key: Bytes, message: Bytes) => Bytes), + sha256Async: async (msg: Bytes): Promise => this.u8n(await sthis.ubtle().digest(this._sha, msg)), + sha256: undefined as undefined | ((message: Bytes) => Bytes), + } + + static prepMsg = (msg: Bytes, opts: ECDSARecoverOpts, async_: boolean): Bytes | Promise => { + this.abytes(msg, undefined, 'message') + if (!opts.prehash) return msg + return async_ ? this.hashes.sha256Async(msg) : this.callHash('sha256')(msg) + } + + static NULL = this.u8n(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 => { + let v = u8n(L) // Steps B, C of RFC6979 3.2: set hashLen + let k = u8n(L) // In our case, it's always equal to L + let i = 0 // Iterations counter, will throw when over max + static reset = () => { + v.fill(1) + k.fill(0) + } + // h = hmac(K || V || ...) + static h = (...b: Bytes[]) => callHash('hmacSha256')(k, concatBytes(v, ...b)) + static reseed = (seed = NULL) => { + // HMAC-DRBG reseed() function. Steps D-G + k = h(byte0, seed) // k = hmac(k || v || 0x00 || seed) + v = h() // v = hmac(k || v) + if (seed.length === 0) return + k = h(byte1, seed) // k = hmac(k || v || 0x01 || seed) + v = h() // v = hmac(k || v) + } + // HMAC-DRBG generate() function + static gen = () => { + if (i++ >= _maxDrbgIters) err(_drbgErr) + v = h() // v = hmac(k || v) + return v // this diverges from noble-curves: we don't allow arbitrary output len! + } + reset () + reseed (seed) // Steps D-G + let res: Bytes | undefined = undefined // Step H: grind until k is in [1..n-1] +while (!(res = pred(gen()))) reseed() // test predicate until it returns ok +reset() +return res! +} + +// Identical to hmacDrbg, but async: uses built-in WebCrypto +static hmacDrbgAsync = async (seed: Bytes, pred: Pred): Promise => { + let v = u8n(L) // Steps B, C of RFC6979 3.2: set hashLen + let k = u8n(L) // In our case, it's always equal to L + let i = 0 // Iterations counter, will throw when over max + static reset = () => { + v.fill(1) + k.fill(0) + } + // h = hmac(K || V || ...) + static h = (...b: Bytes[]) => hashes.hmacSha256Async(k, concatBytes(v, ...b)) + static reseed = async (seed = NULL) => { + // HMAC-DRBG reseed() function. Steps D-G + k = await h(byte0, seed) // k = hmac(K || V || 0x00 || seed) + v = await h() // v = hmac(K || V) + if (seed.length === 0) return + k = await h(byte1, seed) // k = hmac(K || V || 0x01 || seed) + v = await h() // v = hmac(K || V) + } + // HMAC-DRBG generate() function + static gen = async () => { + if (i++ >= _maxDrbgIters) err(_drbgErr) + v = await h() // v = hmac(K || V) + return v // this diverges from noble-curves: we don't allow arbitrary output len! + } + reset() + await reseed(seed) // Steps D-G + let res: Bytes | undefined = undefined // Step H: grind until k is in [1..n-1] + while (!(res = pred(await gen()))) await reseed() // test predicate until it returns ok + reset() + return res! +} + +// RFC6979 signature generation, preparation step. +// Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.2 & RFC6979. +static _sign = ( + messageHash: Bytes, + secretKey: Bytes, + opts: ECDSASignOpts, + hmacDrbg: (seed: Bytes, pred: Pred) => T +): T => { + let { lowS, extraEntropy } = opts // generates low-s sigs by default + // RFC6979 3.2: we skip step A + static int2octets = numTo32b // int to octets + static h1i = bits2int_modN(messageHash) // msg bigint + static h1o = int2octets(h1i) // msg octets + static d = secretKeyToScalar(secretKey) // validate private key, convert to bigint + static seedArgs = [int2octets(d), h1o] // Step D of RFC6979 3.2 + /** RFC6979 3.6: additional k' (optional). See {@link ECDSAExtraEntropy}. */ + 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 ? randomBytes(L) : extraEntropy + seedArgs.push(abytes(e, undefined, 'extraEntropy')) // check for being bytes + } + static seed = concatBytes(...seedArgs) + static m = h1i // convert msg to bigint + // Converts signature params into point w r/s, checks result for validity. + // To transform k => Signature: + // q = k⋅G + // r = q.x mod n + // s = k^-1(m + rd) mod n + // Can use scalar blinding b^-1(bm + bdr) where b ∈ [1,q−1] according to + // https://tches.iacr.org/index.php/TCHES/article/view/7337/6509. We've decided against it: + // a) dependency on CSPRNG b) 15% slowdown c) doesn't really help since bigints are not CT + static k2sig = (kBytes: Bytes): Bytes | undefined => { + // RFC 6979 Section 3.2, step 3: k = bits2int(T) + // Important: all mod() calls here must be done over N + const k = bits2int(kBytes) + if (!(1n <= k && k < N)) return // Valid scalars (including k) must be in 1..N-1 + const ik = invert(k, N) // k^-1 mod n + const q = G.multiply(k).toAffine() // q = k⋅G + const r = modN(q.x) // r = q.x mod n + if (r === 0n) return + const s = modN(ik * modN(m + r * d)) // s = k^-1(m + rd) mod n + if (s === 0n) return + let recovery = (q.x === r ? 0 : 2) | Number(q.y & 1n) // recovery bit (2 or 3, when q.x > n) + let normS = s // normalized S + if (lowS && highS(s)) { + // if lowS was passed, ensure s is always + normS = modN(-s) // in the bottom half of CURVE.n + recovery ^= 1 + } + const sig = new Signature(r, normS, recovery) as RecoveredSignature // use normS, not s + return sig.toBytes(opts.format) + } + return hmacDrbg(seed, k2sig) +} + +// Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.4. +static _verify = (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}) => { + static { lowS, format } = opts + if (sig instanceof Signature) err('Signature must be in Uint8Array, use .toBytes()') + assertSigLength(sig, format) + abytes(publicKey, undefined, 'publicKey') + try { + const { r, s } = Signature.fromBytes(sig, format) + const h = bits2int_modN(messageHash) // Truncate hash + const P = Point.fromBytes(publicKey) // Validate public key + if (lowS && highS(s)) return false // lowS bans sig.s >= CURVE.n/2 + const is = invert(s, N) // s^-1 + const u1 = modN(h * is) // u1 = hs^-1 mod n + const u2 = modN(r * is) // u2 = rs^-1 mod n + const R = doubleScalarMulUns(P, u1, u2).toAffine() // R = u1⋅G + u2⋅P + // Stop if R is identity / zero point. Check is done inside `doubleScalarMulUns` + const v = modN(R.x) // R.x must be in N's field, not P's + return v === r // mod(R.x, n) == r + } catch (error) { + return false + } +} + +static setDefaults = (opts: ECDSASignOpts): Required => { + static res: ECDSASignOpts = {} + Object.keys(defaultSignOpts).forEach((k: string) => { + // @ts-ignore + res[k] = opts[k] ?? defaultSignOpts[k] + }) + return res as Required +} + +/** + * Sign a message using secp256k1. Sync: uses `hashes.sha256` and `hashes.hmacSha256`. + * Prehashes message with sha256, disable using `prehash: false`. + * @param opts - see {@link ECDSASignOpts} for details. Enabling {@link ECDSAExtraEntropy} will improve security. + * @example + * ```js + * const msg = new TextEncoder().encode('hello noble'); + * sign(msg, secretKey); + * sign(keccak256(msg), secretKey, { prehash: false }); + * sign(msg, secretKey, { extraEntropy: true }); + * sign(msg, secretKey, { format: 'recovered' }); + * ``` + */ +static sign = (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes => { + opts = setDefaults(opts) + message = prepMsg(message, opts, false) as Bytes + return _sign(message, secretKey, opts, hmacDrbg) +} + +/** + * Sign a message using secp256k1. Async: uses built-in WebCrypto hashes. + * Prehashes message with sha256, disable using `prehash: false`. + * @param opts - see {@link ECDSASignOpts} for details. Enabling {@link ECDSAExtraEntropy} will improve security. + * @example + * ```js + * const msg = new TextEncoder().encode('hello noble'); + * await signAsync(msg, secretKey); + * await signAsync(keccak256(msg), secretKey, { prehash: false }); + * await signAsync(msg, secretKey, { extraEntropy: true }); + * await signAsync(msg, secretKey, { format: 'recovered' }); + * ``` + */ +static signAsync = async ( + message: Bytes, + secretKey: Bytes, + opts: ECDSASignOpts = {} +): Promise => { + opts = setDefaults(opts) + message = await prepMsg(message, opts, true) + return _sign(message, secretKey, opts, hmacDrbgAsync) +} + +/** + * Verify a signature using secp256k1. Sync: uses `hashes.sha256` and `hashes.hmacSha256`. + * @param signature - default is 64-byte 'compact' format, also see {@link ECDSASignatureFormat} + * @param message - message which was signed. Keep in mind `prehash` from opts. + * @param publicKey - public key which + * @param opts - see {@link ECDSAVerifyOpts} for details. + * @example + * ```js + * const msg = new TextEncoder().encode('hello noble'); + * verify(sig, msg, publicKey); + * verify(sig, keccak256(msg), publicKey, { prehash: false }); + * verify(sig, msg, publicKey, { lowS: false }); + * verify(sigr, msg, publicKey, { format: 'recovered' }); + * ``` + */ +static verify = ( + signature: Bytes, + message: Bytes, + publicKey: Bytes, + opts: ECDSAVerifyOpts = {} +): boolean => { + opts = setDefaults(opts) + message = prepMsg(message, opts, false) as Bytes + return _verify(signature, message, publicKey, opts) +} + +/** + * Verify a signature using secp256k1. Async: uses built-in WebCrypto hashes. + * @param signature - default is 64-byte 'compact' format, also see {@link ECDSASignatureFormat} + * @param message - message which was signed. Keep in mind `prehash` from opts. + * @param publicKey - public key which + * @param opts - see {@link ECDSAVerifyOpts} for details. + * @example + * ```js + * const msg = new TextEncoder().encode('hello noble'); + * verify(sig, msg, publicKey); + * verify(sig, keccak256(msg), publicKey, { prehash: false }); + * verify(sig, msg, publicKey, { lowS: false }); + * verify(sigr, msg, publicKey, { format: 'recovered' }); + * ``` + */ +static verifyAsync = async ( + sig: Bytes, + message: Bytes, + publicKey: Bytes, + opts: ECDSAVerifyOpts = {} +): Promise => { + opts = setDefaults(opts) + message = await prepMsg(message, opts, true) + return _verify(sig, message, publicKey, opts) +} + +static _recover = (signature: Bytes, messageHash: Bytes) => { + static sig = Signature.fromBytes(signature, 'recovered') + static { r, s, recovery } = sig + // 0 or 1 recovery id determines sign of "y" coordinate. + // 2 or 3 means q.x was >N. + assertRecoveryBit(recovery) + static h = bits2int_modN(abytes(messageHash, L)) // Truncate hash + static radj = recovery === 2 || recovery === 3 ? r + N : r + FpIsValidNot0(radj) // ensure q.x is still a field element + static head = getPrefix(big(recovery!)) // head is 0x02 or 0x03 + static Rb = concatBytes(head, numTo32b(radj)) // concat head + r + static R = Point.fromBytes(Rb) + static ir = invert(radj, N) // r^-1 + static u1 = modN(-h * ir) // -hr^-1 + static u2 = modN(s * ir) // sr^-1 + static point = doubleScalarMulUns(R, u1, u2) // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) + return point.toBytes() +} + +/** + * 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 => { + message = prepMsg(message, setDefaults(opts), false) as Bytes + return _recover(signature, message) +} + +static recoverPublicKeyAsync = async ( + signature: Bytes, + message: Bytes, + opts: ECDSARecoverOpts = {} +): Promise => { + message = await prepMsg(message, setDefaults(opts), true) + return _recover(signature, message) +} + +/** + * Elliptic Curve Diffie-Hellman (ECDH) on secp256k1. + * Result is **NOT hashed**. Use hash or KDF on it if you need. + * @param isCompressed 33-byte (true) or 65-byte (false) output + * @returns public key C + */ +static getSharedSecret = (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes => { + return Point.fromBytes(publicKeyB).multiply(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 = randomBytes(lengths.seed)) => { + abytes(seed) + if (seed.length < lengths.seed || seed.length > 1024) err('expected 40-1024b') + static num = M(bytesToNumBE(seed), N - 1n) + return numTo32b(num + 1n) +} + +type KeysSecPub = { secretKey: Bytes; publicKey: Bytes } +type KeygenFn = (seed?: Bytes) => KeysSecPub +static createKeygen = (getPublicKey: (secretKey: Bytes) => Bytes) => (seed?: Bytes): KeysSecPub => { + static secretKey = randomSecretKey(seed) + return { secretKey, publicKey: getPublicKey(secretKey) } +} +static keygen: KeygenFn = createKeygen(getPublicKey) + +/** Math, hex, byte helpers. Not in `utils` because utils share API with noble-curves. */ +static etc = { + hexToBytes: hexToBytes as (hex: string) => Bytes, + bytesToHex: bytesToHex as (bytes: Bytes) => string, + concatBytes: concatBytes as (...arrs: Bytes[]) => Bytes, + bytesToNumberBE: bytesToNumBE as (a: Bytes) => bigint, + numberToBytesBE: numTo32b as (n: bigint) => Bytes, + mod: M as (a: bigint, md?: bigint) => bigint, + invert: invert as (num: bigint, md?: bigint) => bigint, // math utilities + randomBytes: randomBytes as (len?: number) => Bytes, + secretKeyToScalar: secretKeyToScalar as typeof secretKeyToScalar, + abytes: abytes as typeof abytes, +} + +/** Curve-specific utilities for private keys. */ +static utils = { + isValidSecretKey: isValidSecretKey as typeof isValidSecretKey, + isValidPublicKey: isValidPublicKey as typeof isValidPublicKey, + randomSecretKey: randomSecretKey as () => Bytes, +} + +// Schnorr signatures are superior to ECDSA from above. Below is Schnorr-specific BIP0340 code. +// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki +static getTag = (tag: string) => Uint8Array.from('BIP0340/' + tag, (c) => c.charCodeAt(0)) +static T_AUX = 'aux' +static T_NONCE = 'nonce' +static T_CHALLENGE = 'challenge' +static taggedHash = (tag: string, ...messages: Bytes[]): Bytes => { + static fn = callHash('sha256') + static tagH = fn(getTag(tag)) + return fn(concatBytes(tagH, tagH, ...messages)) +} +static taggedHashAsync = async (tag: string, ...messages: Bytes[]): Promise => { + static fn = hashes.sha256Async + static tagH = await fn(getTag(tag)) + return await fn(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 d_ = secretKeyToScalar(priv) + static p = G.multiply(d_) // P = d'⋅G; 0 < d' < n check is done inside + static { x, y } = p.assertValidity().toAffine() // validate Point is not at infinity + static d = isEven(y) ? d_ : modN(-d_) + static px = numTo32b(x) + return { d, px } +} + +static bytesModN = (bytes: Bytes) => modN(bytesToNumBE(bytes)) +static challenge = (...args: Bytes[]): bigint => bytesModN(taggedHash(T_CHALLENGE, ...args)) +static challengeAsync = async (...args: Bytes[]): Promise => + bytesModN(await taggedHashAsync(T_CHALLENGE, ...args)) + +/** + * Schnorr public key is just `x` coordinate of Point as per BIP340. + */ +static pubSchnorr = (secretKey: Bytes): Bytes => { + return extpubSchnorr(secretKey).px // d'=int(sk). Fail if d'=0 or d'≥n. Ret bytes(d'⋅G) +} + +static keygenSchnorr: KeygenFn = createKeygen(pubSchnorr) + +// Common preparation fn for both sync and async signing +static prepSigSchnorr = (message: Bytes, secretKey: Bytes, auxRand: Bytes) => { + static { px, d } = extpubSchnorr(secretKey) + return { m: abytes(message), px, d, a: abytes(auxRand, L) } +} + +static extractK = (rand: Bytes) => { + static k_ = bytesModN(rand) // Let k' = int(rand) mod n + if (k_ === 0n) err('sign failed: k is zero') // Fail if k' = 0. + static { px, d } = extpubSchnorr(numTo32b(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 concatBytes(px, numTo32b(modN(k + e * d))) +} + +static E_INVSIG = 'invalid signature produced' +/** + * 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 = randomBytes(L)): Bytes => { + static { m, px, d, a } = prepSigSchnorr(message, secretKey, auxRand) + static aux = taggedHash(T_AUX, a) + // Let t be the byte-wise xor of bytes(d) and hash/aux(a) + static t = numTo32b(d ^ bytesToNumBE(aux)) + // Let rand = hash/nonce(t || bytes(P) || m) + static rand = taggedHash(T_NONCE, t, px, m) + static { rx, k } = extractK(rand) + // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n. + static e = challenge(rx, px, m) + static sig = createSigSchnorr(k, rx, e, d) + // If Verify(bytes(P), m, sig) (see below) returns failure, abort + if (!verifySchnorr(sig, m, px)) err(E_INVSIG) + return sig +} + +static signSchnorrAsync = async ( + message: Bytes, + secretKey: Bytes, + auxRand: Bytes = randomBytes(L) +): Promise => { + static { m, px, d, a } = prepSigSchnorr(message, secretKey, auxRand) + static aux = await taggedHashAsync(T_AUX, a) + // Let t be the byte-wise xor of bytes(d) and hash/aux(a) + static t = numTo32b(d ^ bytesToNumBE(aux)) + // Let rand = hash/nonce(t || bytes(P) || m) + static rand = await taggedHashAsync(T_NONCE, t, px, m) + static { rx, k } = extractK(rand) + // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n. + static e = await challengeAsync(rx, px, m) + static sig = createSigSchnorr(k, rx, e, d) + // If Verify(bytes(P), m, sig) (see below) returns failure, abort + if (!(await verifySchnorrAsync(sig, m, px))) err(E_INVSIG) + return sig +} + +// const finishVerif = (P_: Point, r: bigint, s: bigint, e: bigint) => {}; + +type MaybePromise = T | Promise +static callSyncAsyncFn = (res: MaybePromise, later: (res2: T) => O) => { + return res instanceof Promise ? res.then(later) : later(res) +} + +static _verifSchnorr = ( + signature: Bytes, + message: Bytes, + publicKey: Bytes, + challengeFn: (...args: Bytes[]) => bigint | Promise +): boolean | Promise => { + static sig = abytes(signature, L2, 'signature') + static msg = abytes(message, undefined, 'message') + static pub = abytes(publicKey, L, 'publicKey') + 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 = bytesToNumBE(pub) + const y = lift_x(x) // Let y = c^(p+1)/4 mod p. + const y_ = isEven(y) ? y : 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_ = new Point(x, y_, 1n).assertValidity() + const px = numTo32b(P_.toAffine().x) + // P = lift_x(int(pk)); fail if that fails + const r = sliceBytesNumBE(sig, 0, L) // Let r = int(sig[0:32]); fail if r ≥ p. + arange(r, 1n, P) + const s = sliceBytesNumBE(sig, L, L2) // Let s = int(sig[32:64]); fail if s ≥ n. + arange(s, 1n, N) + const i = concatBytes(numTo32b(r), px, msg) + // int(challenge(bytes(r)||bytes(P)||m))%n + return callSyncAsyncFn(challengeFn(i), (e) => { + const { x, y } = doubleScalarMulUns(P_, s, modN(-e)).toAffine() // R = s⋅G - e⋅P + if (!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. + }) + } catch (error) { + return false + } +} + +/** + * Verifies Schnorr signature. + * Will swallow errors & return false except for initial type validation of arguments. + */ +static verifySchnorr = (s: Bytes, m: Bytes, p: Bytes): boolean => + _verifSchnorr(s, m, p, challenge) as boolean +static verifySchnorrAsync = async (s: Bytes, m: Bytes, p: Bytes): Promise => + _verifSchnorr(s, m, p, challengeAsync) as Promise + +static schnorr: { + keygen: typeof keygenSchnorr, + getPublicKey: typeof pubSchnorr + sign: typeof signSchnorr + verify: typeof verifySchnorr + signAsync: typeof signSchnorrAsync, + verifyAsync: typeof verifySchnorrAsync +} = { + keygen: keygenSchnorr, + getPublicKey: pubSchnorr, + sign: signSchnorr, + verify: verifySchnorr, + signAsync: signSchnorrAsync, + verifyAsync: verifySchnorrAsync, +} + +// ## Precomputes +// -------------- + +static W = 8 // W is window size +static scalarBits = 256 +static pwindows = Math.ceil(scalarBits / W) + 1 // 33 for W=8, NOT 32 - see wNAF loop +static pwindowSize = 2 ** (W - 1) // 128 for W=8 +static precompute = () => { + static points: Point[] = [] + let p = G + let b = p + for (let w = 0; w < pwindows; w++) { + b = p + points.push(b) + for (let i = 1; i < pwindowSize; i++) { + b = b.add(p) + points.push(b) + } // i=1, bc we skip 0 + p = b.double() + } + return points +} +let Gpows: Point[] | undefined = undefined // precomputes for base point G +// const-time negate +static ctneg = (cnd: boolean, p: Point) => { + static n = p.negate() + return cnd ? n : p +} + +/** + * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by + * caching multiples of G (base point). Cache is stored in 32MB of RAM. + * Any time `G.multiply` is done, precomputes are used. + * Not used for getSharedSecret, which instead multiplies random pubkey `P.multiply`. + * + * w-ary non-adjacent form (wNAF) precomputation method is 10% slower than windowed method, + * but takes 2x less RAM. RAM reduction is possible by utilizing `.subtract`. + * + * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply(). + */ +static wNAF = (n: bigint): { p: Point; f: Point } => { + static comp = Gpows || (Gpows = precompute()) + let p = I + let f = G // f must be G, or could become I in the end + static pow_2_w = 2 ** W // 256 for W=8 + static maxNum = pow_2_w // 256 for W=8 + static mask = big(pow_2_w - 1) // 255 for W=8 == mask 0b11111111 + static shiftBy = big(W) // 8 for W=8 + for (let w = 0; w < pwindows; w++) { + let wbits = Number(n & mask) // extract W bits. + n >>= shiftBy // shift number by W bits. + // We use negative indexes to reduce size of precomputed table by 2x. + // Instead of needing precomputes 0..256, we only calculate them for 0..128. + // If an index > 128 is found, we do (256-index) - where 256 is next window. + // Naive: index +127 => 127, +224 => 224 + // Optimized: index +127 => 127, +224 => 256-32 + if (wbits > pwindowSize) { + wbits -= maxNum + n += 1n + } + const off = w * pwindowSize + const offF = off // offsets, evaluate both + const offP = off + 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(ctneg(isEven, comp[offF])) // bits are 0: add garbage to fake point + } else { + p = p.add(ctneg(isNeg, comp[offP])) // bits are 1: add to result point + } + } + if (n !== 0n) err('invalid wnaf') + return { p, f } // return both real and fake points for JIT +} +} + +// !! Remove the export below to easily use in REPL / browser console +// export { +// etc, +// getPublicKey, getSharedSecret, +// hash, hashes, +// keygen, +// Point, recoverPublicKey, recoverPublicKeyAsync, schnorr, sign, signAsync, + +// Signature, utils, verify, verifyAsync +// } diff --git a/src/lib/crypto/wallet-aes-gcm.ts b/src/lib/crypto/wallet-aes-gcm.ts index 33575d8..0880fc9 100644 --- a/src/lib/crypto/wallet-aes-gcm.ts +++ b/src/lib/crypto/wallet-aes-gcm.ts @@ -7,7 +7,7 @@ export class WalletAesGcm { static encoder: TextEncoder = new TextEncoder() static decrypt (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<{ [key: string]: ArrayBuffer }> { - const seedLength = type === 'BIP-44' ? 64 : 32 + const seedLength = type === 'BLAKE2b' ? 32 : 64 const additionalData = this.encoder.encode(type) return crypto.subtle .decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted) diff --git a/src/lib/vault/index.ts b/src/lib/vault/index.ts index dcd7dec..710deb0 100644 --- a/src/lib/vault/index.ts +++ b/src/lib/vault/index.ts @@ -3,7 +3,7 @@ import { Worker as NodeWorker } from 'node:worker_threads' import { default as CONSTANTS } from '../constants' -import { Bip39, Bip44, Blake2b, NanoNaCl, WalletAesGcm } from '../crypto' +import { Bip39, Bip44, Blake2b, NanoNaCl, Secp256k1, WalletAesGcm } from '../crypto' import { Data } from '../database' import { Passkey } from './passkey' import { VaultTimer } from './vault-timer' @@ -124,6 +124,7 @@ export class Vault { const blob = ` ${CONSTANTS} + const ${Secp256k1.name} = ${Secp256k1} const ${Bip39.name} = ${Bip39} const ${Bip44.name} = ${Bip44} const ${Blake2b.name} = ${Blake2b} @@ -132,6 +133,7 @@ const blob = ` const ${Passkey.name} = ${Passkey} const ${VaultTimer.name} = ${VaultTimer} const ${VaultWorker.name} = ${VaultWorker} + ${Secp256k1.name === 'Secp256k1' ? '' : `const Secp256k1 = ${Secp256k1.name}`} ${Bip39.name === 'Bip39' ? '' : `const Bip39 = ${Bip39.name}`} ${Bip44.name === 'Bip44' ? '' : `const Bip44 = ${Bip44.name}`} ${Blake2b.name === 'Blake2b' ? '' : `const Blake2b = ${Blake2b.name}`} diff --git a/src/lib/vault/vault-worker.ts b/src/lib/vault/vault-worker.ts index 5be96d5..66436e0 100644 --- a/src/lib/vault/vault-worker.ts +++ b/src/lib/vault/vault-worker.ts @@ -187,9 +187,11 @@ export class VaultWorker { if (typeof index !== 'number') { throw new Error('Invalid wallet account index') } - const derive = this.#type === 'BLAKE2b' - ? Blake2b.ckd(this.#seed, index) - : Bip44.ckd(this.#type === 'Exodus' ? 'Bitcoin seed' : 'ed25519 seed', this.#seed, BIP44_COIN_NANO, index) + const derive = this.#type === 'BIP-44' + ? Bip44.ckd('ed25519 seed', this.#seed, BIP44_COIN_NANO, index) + : this.#type === 'Exodus' + ? Bip44.ckd('Bitcoin seed', this.#seed, 0x100, index, 0, 0) + : Blake2b.ckd(this.#seed, index) return derive.then(prv => { const pub = NanoNaCl.convert(new Uint8Array(prv)) this.#timer = new VaultTimer(() => this.lock(), this.#timeout) @@ -207,7 +209,7 @@ export class VaultWorker { * vector, salt, and encrypted data representing the wallet in a locked state. */ load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { - if (type !== 'BIP-44' && type !== 'BLAKE2b') { + if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') { throw new TypeError('Unsupported software wallet algorithm', { cause: type }) } return this.#load(type, key, keySalt, secret, mnemonicSalt) @@ -373,6 +375,7 @@ export class VaultWorker { isVerified = diff === 0 } if (mnemonicPhrase != null) { + console.log(new TextDecoder().decode((new Uint8Array(this.#mnemonic ?? [])))) let diff = 0 const userMnemonic = this.#encoder.encode(mnemonicPhrase) const thisMnemonic = new Uint8Array(this.#mnemonic ?? []) @@ -491,7 +494,7 @@ export class VaultWorker { * Encrypts an existing seed or mnemonic+salt and returns the initialization * vector, salt, and encrypted data representing the wallet in a locked state. */ - #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { + #load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { try { if (!this.#locked) { throw new Error('Wallet is in use') @@ -502,7 +505,7 @@ export class VaultWorker { if (type == null) { throw new TypeError('Wallet type is required') } - if (type !== 'BIP-44' && type !== 'BLAKE2b') { + if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') { throw new TypeError('Invalid wallet type') } if (secret == null) { @@ -521,6 +524,14 @@ export class VaultWorker { throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes') } } + if (type === 'Exodus') { + if (typeof secret === 'string' && secret.split(' ').length !== 12) { + throw new RangeError('Mnemonic for Exodus wallet must be 12 words') + } + if (secret instanceof ArrayBuffer && secret.byteLength !== 64) { + throw new RangeError('Seed for Exodus wallet must be 64 bytes') + } + } this.#type = type let seed: Promise if (secret instanceof ArrayBuffer) { @@ -537,9 +548,9 @@ export class VaultWorker { seed = Bip39.fromPhrase(secret) .then(bip39 => { this.#mnemonic = new Uint8Array(this.#encoder.encode(bip39.phrase ?? '')).buffer - const derive = type === 'BIP-44' - ? bip39.toBip39Seed(mnemonicSalt ?? '') - : Promise.resolve(bip39.toBlake2bSeed()) + const derive = type === 'BLAKE2b' + ? Promise.resolve(bip39.toBlake2bSeed()) + : bip39.toBip39Seed(mnemonicSalt ?? '') return derive.then(s => s.buffer) }) } diff --git a/src/lib/wallet/backup.ts b/src/lib/wallet/backup.ts index bb8992f..f39cb41 100644 --- a/src/lib/wallet/backup.ts +++ b/src/lib/wallet/backup.ts @@ -12,7 +12,7 @@ export async function _backup () { if (typeof id !== 'string') { throw new TypeError('Retrieved invalid ID', { cause: id }) } - if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') { + if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus' && type !== 'Ledger') { throw new TypeError('Retrieved invalid type', { cause: type }) } if (!(iv instanceof ArrayBuffer)) { diff --git a/src/lib/wallet/get.ts b/src/lib/wallet/get.ts index 412befd..de7f2c9 100644 --- a/src/lib/wallet/get.ts +++ b/src/lib/wallet/get.ts @@ -11,7 +11,7 @@ export async function _get (recordId: string) { if (typeof id !== 'string') { throw new TypeError('Retrieved invalid ID', { cause: id }) } - if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') { + if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus' && type !== 'Ledger') { throw new TypeError('Retrieved invalid type', { cause: type }) } if (!(iv instanceof ArrayBuffer)) { diff --git a/src/lib/wallet/verify.ts b/src/lib/wallet/verify.ts index f364e7a..02d7baa 100644 --- a/src/lib/wallet/verify.ts +++ b/src/lib/wallet/verify.ts @@ -1,10 +1,10 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +import { WalletType } from '.' import { hex } from '../convert' import { Ledger } from '../ledger' import { Vault } from '../vault' -import { WalletType } from '.' export async function _verify (type: WalletType, vault: Vault, secret: string): Promise export async function _verify (type: WalletType, vault: Vault, secret: unknown): Promise { diff --git a/test/VECTORS.mjs b/test/VECTORS.mjs index 43d0db1..0f709ff 100644 --- a/test/VECTORS.mjs +++ b/test/VECTORS.mjs @@ -44,21 +44,6 @@ export const NANO_TEST_VECTORS = Object.freeze({ PUBLIC_2: 'A46DA51986E25A14D82E32D765DCEE69B9EECCD4405411430D91DDB61B717566', ADDRESS_2: 'nano_3b5fnnerfrkt4me4wepqeqggwtfsxu8fai4n473iu6gxprfq4xd8pk9gh1dg', - SHORT_MNEMONIC: 'edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur', - SHORT_BIP39_SEED: '924A962CAE64448812BE28A514093EBFEEED537D61A44318EB35F902961D21B2FCCD30008D33C8D1D5327A34B9B73281C4B27A0A3D004C1C2E85E8DBB234CBA8', - - SHORT_PRIVATE_0: '6f73d61ca0b56fcdb79d69d437f102348ad75ca971433eb92b2b003f8c99b48d', - SHORT_PUBLIC_0: '134d938215f68bcaa3a0e574fde325fc4b1abad9bd3d698bfef95633b54ffb57', - SHORT_ADDRESS_0: 'nano_16tfkg33dxndscjt3sdnzqjkdz4d5cxfmhbxf87zxycp8gtnzytqmcosi3zr', - - SHORT_PRIVATE_1: '7e104389811a0967ef574af1f3f423f23cbf7b614be17844f67fb6fd315f9a7e', - SHORT_PUBLIC_1: '71e6caac915affe836c3e822be6a5b3464f40c74bd2e5459d4e74205c6a7c0df', - SHORT_ADDRESS_1: 'nano_1wh8scpb4pqzx1ue9t34qso7pf56yi89bhbgcjexbst41q5chi8zqtwb74ih', - - SHORT_PRIVATE_2: '8b7250869207a277ac37068dbe32782c2ab9fc6a5342f0deabbfdfae1285196a', - SHORT_PUBLIC_2: 'fcebc6554853ed01c242817abf1b5050b887002f8de8f55d00c7c6b5fe01075d', - SHORT_ADDRESS_2: 'nano_3z9drscninzf193671dtqwfo1n7riw14z5hayogi3jy8pqz143txaghe4gbk', - // from nano.org transaction examples SEND_BLOCK: { account: "nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx", @@ -280,6 +265,15 @@ export const CUSTOM_TEST_VECTORS = Object.freeze({ link: "71E9336634A47E557EDAAC16243E019BCABFD745A4679291C11C91FF89DFF219", hash: "F3893E3568C64F9554C1A89CF3FABB16C428D08CFE18C6271630CDB964120E70", signature: "583624F09D1108B50CFB219F13F075AC03CD5C5450DE055B0963D8E814324C9B0A858BD34AF06E5290C436CC86872CBB13C7FE89972E77E6836249DCD2DAE40A" + }, + + // mnemonic from nano.org, other values calculated by importing to exodus app + EXODUS: { + MNEMONIC: 'company public remove bread fashion tortoise ahead shrimp onion prefer waste blade', + BIP39_SEED: '924A962CAE64448812BE28A514093EBFEEED537D61A44318EB35F902961D21B2FCCD30008D33C8D1D5327A34B9B73281C4B27A0A3D004C1C2E85E8DBB234CBA8', + PRIVATE_0: 'CBB312CA6C48B2A2656723C7D4FDE774479D9FCC53EE23F46B877E3AA6D041BF', + PUBLIC_0: 'A8225A8F80079FB62E4A1BC577A684A2A93DB7303A4FD8CA5D0A90C5150F1182', + ADDRESS_0: 'nano_3c34dc9r13wzprq6n8y7gymabaob9pum1gkhu577t4nirnciy6e43g96m6ym', } }) diff --git a/test/main.test.mjs b/test/main.test.mjs index d59be6e..957a612 100644 --- a/test/main.test.mjs +++ b/test/main.test.mjs @@ -4,18 +4,18 @@ import { failures, passes } from './GLOBALS.mjs' import './test.runner-check.mjs' -import './test.blake2b.mjs' -import './test.blocks.mjs' -import './test.calculate-pow.mjs' -import './test.create-wallet.mjs' -import './test.derive-accounts.mjs' +// import './test.blake2b.mjs' +// import './test.blocks.mjs' +// import './test.calculate-pow.mjs' +// import './test.create-wallet.mjs' +// import './test.derive-accounts.mjs' import './test.import-wallet.mjs' -import './test.ledger.mjs' -import './test.lock-unlock.mjs' -import './test.manage-rolodex.mjs' -import './test.refresh-accounts.mjs' -import './test.tools.mjs' -import './test.wallet-sign.mjs' +// import './test.ledger.mjs' +// import './test.lock-unlock.mjs' +// import './test.manage-rolodex.mjs' +// import './test.refresh-accounts.mjs' +// import './test.tools.mjs' +// import './test.wallet-sign.mjs' console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold') console.log('%cPASS: ', 'color:green;font-weight:bold', passes.length) diff --git a/test/test.import-wallet.mjs b/test/test.import-wallet.mjs index 7615964..8f3b94a 100644 --- a/test/test.import-wallet.mjs +++ b/test/test.import-wallet.mjs @@ -10,320 +10,320 @@ import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST await Promise.all([ suite('Import wallets', async () => { - await test('nano.org BIP-44 test vector mnemonic', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - - assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) - assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) - assert.ok(account instanceof Account) - assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) - assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) - - await assert.resolves(wallet.destroy()) - }) - - await test('nano.org BIP-44 test vector seed with no mnemonic', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - - assert.ok('mnemonic' in wallet) - assert.ok('seed' in wallet) - assert.ok(account instanceof Account) - assert.ok(wallet.mnemonic === undefined) - assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) - assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) - assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) - - await assert.resolves(wallet.destroy()) - }) - - await test('Trezor-derived BIP-44 entropy for 12-word mnemonic', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_0) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_0)) - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_0)) - assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_0) - assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_0) - - await assert.resolves(wallet.destroy()) - }) - - await test('Trezor-derived BIP-44 entropy for 15-word mnemonic', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_1) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_1)) - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_1)) - assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_1) - assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_1) - - await assert.resolves(wallet.destroy()) - }) - - await test('Trezor-derived BIP-44 entropy for 18-word mnemonic', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_2) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_2)) - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_2)) - assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_2) - assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_2) - - await assert.resolves(wallet.destroy()) - }) - - await test('Trezor-derived BIP-44 entropy for 21-word mnemonic', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_3) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_3)) - assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_3)) - assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_3) - assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_3) - - await assert.resolves(wallet.destroy()) - }) - - await test('BIP-44 zero-string entropy', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0, TREZOR_TEST_VECTORS.PASSWORD) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts(0, 3) - - assert.ok('mnemonic' in wallet) - assert.ok('seed' in wallet) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.SEED_0)) - assert.equal(accounts.size, 4) - - for (let i = 0; i < accounts.size; i++) { - const account = accounts.get(i) - assert.exists(account) - assert.exists(account.address) - assert.exists(account.publicKey) - } - - await assert.resolves(wallet.destroy()) - }) - - await test('BLAKE2b zero-string seed', async () => { - const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts(0, 3) - - assert.ok('mnemonic' in wallet) - assert.ok('seed' in wallet) - assert.ok(wallet.mnemonic === undefined) - assert.ok(wallet.seed === undefined) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_0)) - assert.equal(accounts.size, 4) - - for (let i = 0; i < accounts.size; i++) { - const account = accounts.get(i) - assert.exists(account) - assert.exists(account.address) - assert.exists(account.publicKey) - } + // await test('nano.org BIP-44 test vector mnemonic', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + + // assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) + // assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) + // assert.ok(account instanceof Account) + // assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) + // assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('nano.org BIP-44 test vector seed with no mnemonic', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + + // assert.ok('mnemonic' in wallet) + // assert.ok('seed' in wallet) + // assert.ok(account instanceof Account) + // assert.ok(wallet.mnemonic === undefined) + // assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) + // assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) + // assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('Trezor-derived BIP-44 entropy for 12-word mnemonic', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_0) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_0)) + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_0)) + // assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_0) + // assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_0) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('Trezor-derived BIP-44 entropy for 15-word mnemonic', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_1) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_1)) + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_1)) + // assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_1) + // assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_1) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('Trezor-derived BIP-44 entropy for 18-word mnemonic', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_2) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_2)) + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_2)) + // assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_2) + // assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_2) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('Trezor-derived BIP-44 entropy for 21-word mnemonic', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_3) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_3)) + // assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_3)) + // assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_3) + // assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_3) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('BIP-44 zero-string entropy', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0, TREZOR_TEST_VECTORS.PASSWORD) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const accounts = await wallet.accounts(0, 3) + + // assert.ok('mnemonic' in wallet) + // assert.ok('seed' in wallet) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.SEED_0)) + // assert.equal(accounts.size, 4) + + // for (let i = 0; i < accounts.size; i++) { + // const account = accounts.get(i) + // assert.exists(account) + // assert.exists(account.address) + // assert.exists(account.publicKey) + // } + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('BLAKE2b zero-string seed', async () => { + // const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const accounts = await wallet.accounts(0, 3) + + // assert.ok('mnemonic' in wallet) + // assert.ok('seed' in wallet) + // assert.ok(wallet.mnemonic === undefined) + // assert.ok(wallet.seed === undefined) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_0)) + // assert.equal(accounts.size, 4) + + // for (let i = 0; i < accounts.size; i++) { + // const account = accounts.get(i) + // assert.exists(account) + // assert.exists(account.address) + // assert.exists(account.publicKey) + // } + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('Trezor-derived BLAKE2b test vectors verified with third-party libraries', async () => { + // const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const accounts = await wallet.accounts(0, 1) + + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_1)) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_1)) + + // const account0 = accounts.get(0) + // assert.exists(account0) + // assert.ok(account0 instanceof Account) + // assert.equal(account0.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_0) + // assert.equal(account0.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_0) + + // const account1 = accounts.get(1) + // assert.exists(account1) + // assert.ok(account1 instanceof Account) + // assert.equal(account1.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_1) + // assert.equal(account1.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_1) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('BLAKE2b seed creates identical wallet as its derived mnemonic', async () => { + // const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const walletAccount = await wallet.account() + + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_2)) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_2)) + + // const imported = await Wallet.load('BLAKE2b', TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_2) + // await imported.unlock(TREZOR_TEST_VECTORS.PASSWORD) + // const importedAccount = await imported.account() + + // assert.ok(await imported.verify(TREZOR_TEST_VECTORS.MNEMONIC_2)) + // assert.ok(await imported.verify(TREZOR_TEST_VECTORS.ENTROPY_2)) + // assert.equal(importedAccount.publicKey, walletAccount.publicKey) + + // await assert.resolves(wallet.destroy()) + // await assert.resolves(imported.destroy()) + // }) + + // await test('BLAKE2b mnemonic for maximum seed value', async () => { + // const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + + // assert.ok(account instanceof Account) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_3)) + // assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_3)) + // assert.equal(account.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0) + // assert.equal(account.address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0) + + // await assert.resolves(wallet.destroy()) + // }) + + // await test('Reject invalid seed', async () => { + // await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C797')) + // await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C79701')) + // await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0.replaceAll(/./g, 'x'))) + // }) + + // await test('Reject invalid length seed', async () => { + // await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED + 'f'), + // `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length + 1}-character string.`) + // await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED.slice(0, -1)), + // `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length - 1}-character string.`) + // }) + + // await test('Reject seed containing non-hex characters', async () => { + // await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.SEED_0.replace(/./, 'g')), + // 'Seed contains invalid hexadecimal characters.') + // await assert.rejects(Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1.replace(/./, 'g')), + // 'Seed contains invalid hexadecimal characters.') + // }) + + // await test('Import BIP-44 wallet from storage using a wallet-generated ID', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + // const restored = await Wallet.restore(wallet.id) + + // assert.ok('mnemonic' in restored) + // assert.ok('seed' in restored) + // assert.ok(restored.mnemonic === undefined) + // assert.ok(restored.seed === undefined) + // await assert.rejects(restored.verify(NANO_TEST_VECTORS.MNEMONIC)) + // await assert.rejects(restored.verify(NANO_TEST_VECTORS.BIP39_SEED)) + + // await restored.unlock(NANO_TEST_VECTORS.PASSWORD) + + // assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC)) + // assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED)) + + // await assert.resolves(restored.destroy()) + // await assert.resolves(wallet.destroy()) + // }) - await assert.resolves(wallet.destroy()) - }) - - await test('Trezor-derived BLAKE2b test vectors verified with third-party libraries', async () => { - const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts(0, 1) - - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_1)) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_1)) - - const account0 = accounts.get(0) - assert.exists(account0) - assert.ok(account0 instanceof Account) - assert.equal(account0.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_0) - assert.equal(account0.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_0) - - const account1 = accounts.get(1) - assert.exists(account1) - assert.ok(account1 instanceof Account) - assert.equal(account1.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_1) - assert.equal(account1.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_1) - - await assert.resolves(wallet.destroy()) - }) - - await test('BLAKE2b seed creates identical wallet as its derived mnemonic', async () => { - const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const walletAccount = await wallet.account() - - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_2)) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_2)) - - const imported = await Wallet.load('BLAKE2b', TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_2) - await imported.unlock(TREZOR_TEST_VECTORS.PASSWORD) - const importedAccount = await imported.account() - - assert.ok(await imported.verify(TREZOR_TEST_VECTORS.MNEMONIC_2)) - assert.ok(await imported.verify(TREZOR_TEST_VECTORS.ENTROPY_2)) - assert.equal(importedAccount.publicKey, walletAccount.publicKey) - - await assert.resolves(wallet.destroy()) - await assert.resolves(imported.destroy()) - }) - - await test('BLAKE2b mnemonic for maximum seed value', async () => { - const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - - assert.ok(account instanceof Account) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_3)) - assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_3)) - assert.equal(account.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0) - assert.equal(account.address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0) - - await assert.resolves(wallet.destroy()) - }) + // await test('Import BLAKE2B wallet from storage using a wallet-generated ID', async () => { + // const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0) + // const restored = await Wallet.restore(wallet.id) + + // assert.ok('mnemonic' in restored) + // assert.ok('seed' in restored) + // assert.ok(restored.mnemonic === undefined) + // assert.ok(restored.seed === undefined) + // await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) + // await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0)) - await test('Reject invalid seed', async () => { - await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C797')) - await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C79701')) - await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0.replaceAll(/./g, 'x'))) - }) - - await test('Reject invalid length seed', async () => { - await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED + 'f'), - `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length + 1}-character string.`) - await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED.slice(0, -1)), - `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length - 1}-character string.`) - }) - - await test('Reject seed containing non-hex characters', async () => { - await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.SEED_0.replace(/./, 'g')), - 'Seed contains invalid hexadecimal characters.') - await assert.rejects(Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1.replace(/./, 'g')), - 'Seed contains invalid hexadecimal characters.') - }) - - await test('Import BIP-44 wallet from storage using a wallet-generated ID', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) - const restored = await Wallet.restore(wallet.id) - - assert.ok('mnemonic' in restored) - assert.ok('seed' in restored) - assert.ok(restored.mnemonic === undefined) - assert.ok(restored.seed === undefined) - await assert.rejects(restored.verify(NANO_TEST_VECTORS.MNEMONIC)) - await assert.rejects(restored.verify(NANO_TEST_VECTORS.BIP39_SEED)) - - await restored.unlock(NANO_TEST_VECTORS.PASSWORD) - - assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC)) - assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED)) - - await assert.resolves(restored.destroy()) - await assert.resolves(wallet.destroy()) - }) - - await test('Import BLAKE2B wallet from storage using a wallet-generated ID', async () => { - const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0) - const restored = await Wallet.restore(wallet.id) + // await restored.unlock(NANO_TEST_VECTORS.PASSWORD) + + // assert.ok(await restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) + // assert.ok(await restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0)) - assert.ok('mnemonic' in restored) - assert.ok('seed' in restored) - assert.ok(restored.mnemonic === undefined) - assert.ok(restored.seed === undefined) - await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) - await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0)) + // await assert.resolves(restored.destroy()) + // await assert.resolves(wallet.destroy()) + // }) - await restored.unlock(NANO_TEST_VECTORS.PASSWORD) + // await test('export wallet IDs from storage and reimport them', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + // const backups = await Wallet.backup() - assert.ok(await restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0)) - assert.ok(await restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0)) + // assert.ok(backups.some(record => record.id === wallet.id)) - await assert.resolves(restored.destroy()) - await assert.resolves(wallet.destroy()) - }) + // const restored = await Wallet.restore(wallet.id) - await test('export wallet IDs from storage and reimport them', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) - const backups = await Wallet.backup() + // assert.ok('mnemonic' in restored) + // assert.ok('seed' in restored) + // assert.ok(restored.mnemonic === undefined) + // assert.ok(restored.seed === undefined) - assert.ok(backups.some(record => record.id === wallet.id)) + // await restored.unlock(NANO_TEST_VECTORS.PASSWORD) - const restored = await Wallet.restore(wallet.id) + // assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC)) + // assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED)) - assert.ok('mnemonic' in restored) - assert.ok('seed' in restored) - assert.ok(restored.mnemonic === undefined) - assert.ok(restored.seed === undefined) + // await assert.resolves(wallet.destroy()) + // await assert.resolves(restored.destroy()) + // }) - await restored.unlock(NANO_TEST_VECTORS.PASSWORD) + // await test('restore wallets without referencing IDs', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + // const backups = await Wallet.backup() - assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC)) - assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED)) + // assert.ok(backups.some(record => record.id === wallet.id)) - await assert.resolves(wallet.destroy()) - await assert.resolves(restored.destroy()) - }) + // const restored = await Wallet.restore() + // const restoredWallet = restored.find(restoredWallet => restoredWallet.id === wallet.id) - await test('restore wallets without referencing IDs', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) - const backups = await Wallet.backup() + // assert.exists(restoredWallet) + // assert.ok('mnemonic' in restoredWallet) + // assert.ok('seed' in restoredWallet) + // assert.ok(restoredWallet.mnemonic === undefined) + // assert.ok(restoredWallet.seed === undefined) - assert.ok(backups.some(record => record.id === wallet.id)) + // await restoredWallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const restored = await Wallet.restore() - const restoredWallet = restored.find(restoredWallet => restoredWallet.id === wallet.id) + // assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.MNEMONIC)) + // assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) - assert.exists(restoredWallet) - assert.ok('mnemonic' in restoredWallet) - assert.ok('seed' in restoredWallet) - assert.ok(restoredWallet.mnemonic === undefined) - assert.ok(restoredWallet.seed === undefined) + // for (const restoredWallet of restored) { + // await assert.resolves(restoredWallet.destroy()) + // } + // await assert.resolves(wallet.destroy()) + // }) - await restoredWallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // await test('load account from legacy address', async () => { + // const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + // await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + // const account = await wallet.account() + // const legacy = Account.load(NANO_TEST_VECTORS.ADDRESS_0.replace('nano_', 'xrb_')) - assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.MNEMONIC)) - assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) - - for (const restoredWallet of restored) { - await assert.resolves(restoredWallet.destroy()) - } - await assert.resolves(wallet.destroy()) - }) + // assert.equal(account.address, legacy.address) + // assert.equal(NANO_TEST_VECTORS.ADDRESS_0, legacy.address) + // assert.equal(NANO_TEST_VECTORS.PUBLIC_0, legacy.publicKey) - await test('load account from legacy address', async () => { - const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const account = await wallet.account() - const legacy = Account.load(NANO_TEST_VECTORS.ADDRESS_0.replace('nano_', 'xrb_')) - - assert.equal(account.address, legacy.address) - assert.equal(NANO_TEST_VECTORS.ADDRESS_0, legacy.address) - assert.equal(NANO_TEST_VECTORS.PUBLIC_0, legacy.publicKey) - - await assert.resolves(wallet.destroy()) - }) + // await assert.resolves(wallet.destroy()) + // }) await test('nano.org Exodus test vector mnemonic', async () => { - const wallet = await Wallet.load('Exodus', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.SHORT_MNEMONIC) + const wallet = await Wallet.load('Exodus', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.EXODUS.MNEMONIC) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - assert.ok(await wallet.verify(NANO_TEST_VECTORS.SHORT_MNEMONIC)) - assert.ok(await wallet.verify(NANO_TEST_VECTORS.SHORT_BIP39_SEED)) + assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.EXODUS.MNEMONIC)) + assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.EXODUS.BIP39_SEED)) assert.ok(account instanceof Account) - assert.equal(account.publicKey, NANO_TEST_VECTORS.SHORT_PUBLIC_0) - assert.equal(account.address, NANO_TEST_VECTORS.SHORT_ADDRESS_0) + assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.EXODUS.PUBLIC_0) + assert.equal(account.address, CUSTOM_TEST_VECTORS.EXODUS.ADDRESS_0) await assert.resolves(wallet.destroy()) })