/*! scure-bip32 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */ import { hmac } from '@noble/hashes/hmac'; import { ripemd160 } from '@noble/hashes/ripemd160'; import { sha256 } from '@noble/hashes/sha256'; import { sha512 } from '@noble/hashes/sha512'; import { bytes as assertBytes } from '@noble/hashes/_assert'; import { bytesToHex, concatBytes, createView, hexToBytes, utf8ToBytes } from '@noble/hashes/utils'; import { secp256k1 as secp } from '@noble/curves/secp256k1'; import { mod } from '@noble/curves/abstract/modular'; import { createBase58check } from '@scure/base'; const Point = secp.ProjectivePoint; const base58check = createBase58check(sha256); function bytesToNumber(bytes: Uint8Array): bigint { return BigInt(`0x${bytesToHex(bytes)}`); } function numberToBytes(num: bigint): Uint8Array { return hexToBytes(num.toString(16).padStart(64, '0')); } const MASTER_SECRET = utf8ToBytes('Bitcoin seed'); // Bitcoin hardcoded by default const BITCOIN_VERSIONS: Versions = { private: 0x0488ade4, public: 0x0488b21e }; export const HARDENED_OFFSET: number = 0x80000000; export interface Versions { private: number; public: number; } const hash160 = (data: Uint8Array) => ripemd160(sha256(data)); const fromU32 = (data: Uint8Array) => createView(data).getUint32(0, false); const toU32 = (n: number) => { if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) { throw new Error(`Invalid number=${n}. Should be from 0 to 2 ** 32 - 1`); } const buf = new Uint8Array(4); createView(buf).setUint32(0, n, false); return buf; }; interface HDKeyOpt { versions?: Versions; depth?: number; index?: number; parentFingerprint?: number; chainCode?: Uint8Array; publicKey?: Uint8Array; privateKey?: Uint8Array | bigint; } export class HDKey { get fingerprint(): number { if (!this.pubHash) { throw new Error('No publicKey set!'); } return fromU32(this.pubHash); } get identifier(): Uint8Array | undefined { return this.pubHash; } get pubKeyHash(): Uint8Array | undefined { return this.pubHash; } get privateKey(): Uint8Array | null { return this.privKeyBytes || null; } get publicKey(): Uint8Array | null { return this.pubKey || null; } get privateExtendedKey(): string { const priv = this.privateKey; if (!priv) { throw new Error('No private key'); } return base58check.encode( this.serialize(this.versions.private, concatBytes(new Uint8Array([0]), priv)) ); } get publicExtendedKey(): string { if (!this.pubKey) { throw new Error('No public key'); } return base58check.encode(this.serialize(this.versions.public, this.pubKey)); } public static fromMasterSeed(seed: Uint8Array, versions: Versions = BITCOIN_VERSIONS): HDKey { assertBytes(seed); if (8 * seed.length < 128 || 8 * seed.length > 512) { throw new Error( `HDKey: wrong seed length=${seed.length}. Should be between 128 and 512 bits; 256 bits is advised)` ); } const I = hmac(sha512, MASTER_SECRET, seed); return new HDKey({ versions, chainCode: I.slice(32), privateKey: I.slice(0, 32), }); } public static fromExtendedKey(base58key: string, versions: Versions = BITCOIN_VERSIONS): HDKey { // => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33) const keyBuffer: Uint8Array = base58check.decode(base58key); const keyView = createView(keyBuffer); const version = keyView.getUint32(0, false); const opt = { versions, depth: keyBuffer[4], parentFingerprint: keyView.getUint32(5, false), index: keyView.getUint32(9, false), chainCode: keyBuffer.slice(13, 45), }; const key = keyBuffer.slice(45); const isPriv = key[0] === 0; if (version !== versions[isPriv ? 'private' : 'public']) { throw new Error('Version mismatch'); } if (isPriv) { return new HDKey({ ...opt, privateKey: key.slice(1) }); } else { return new HDKey({ ...opt, publicKey: key }); } } public static fromJSON(json: { xpriv: string }): HDKey { return HDKey.fromExtendedKey(json.xpriv); } public readonly versions: Versions; public readonly depth: number = 0; public readonly index: number = 0; public readonly chainCode: Uint8Array | null = null; public readonly parentFingerprint: number = 0; private privKey?: bigint; private privKeyBytes?: Uint8Array; private pubKey?: Uint8Array; private pubHash: Uint8Array | undefined; constructor(opt: HDKeyOpt) { if (!opt || typeof opt !== 'object') { throw new Error('HDKey.constructor must not be called directly'); } this.versions = opt.versions || BITCOIN_VERSIONS; this.depth = opt.depth || 0; this.chainCode = opt.chainCode || null; this.index = opt.index || 0; this.parentFingerprint = opt.parentFingerprint || 0; if (!this.depth) { if (this.parentFingerprint || this.index) { throw new Error('HDKey: zero depth with non-zero index/parent fingerprint'); } } if (opt.publicKey && opt.privateKey) { throw new Error('HDKey: publicKey and privateKey at same time.'); } if (opt.privateKey) { if (!secp.utils.isValidPrivateKey(opt.privateKey)) { throw new Error('Invalid private key'); } this.privKey = typeof opt.privateKey === 'bigint' ? opt.privateKey : bytesToNumber(opt.privateKey); this.privKeyBytes = numberToBytes(this.privKey); this.pubKey = secp.getPublicKey(opt.privateKey, true); } else if (opt.publicKey) { this.pubKey = Point.fromHex(opt.publicKey).toRawBytes(true); // force compressed point } else { throw new Error('HDKey: no public or private key provided'); } this.pubHash = hash160(this.pubKey); } public derive(path: string): HDKey { if (!/^[mM]'?/.test(path)) { throw new Error('Path must start with "m" or "M"'); } if (/^[mM]'?$/.test(path)) { return this; } const parts = path.replace(/^[mM]'?\//, '').split('/'); // tslint:disable-next-line let child: HDKey = this; for (const c of parts) { const m = /^(\d+)('?)$/.exec(c); const m1 = m && m[1]; if (!m || m.length !== 3 || typeof m1 !== 'string') { throw new Error(`Invalid child index: ${c}`); } let idx = +m1; if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) { throw new Error('Invalid index'); } // hardened key if (m[2] === "'") { idx += HARDENED_OFFSET; } child = child.deriveChild(idx); } return child; } public deriveChild(index: number): HDKey { if (!this.pubKey || !this.chainCode) { throw new Error('No publicKey or chainCode set'); } let data = toU32(index); if (index >= HARDENED_OFFSET) { // Hardened const priv = this.privateKey; if (!priv) { throw new Error('Could not derive hardened child key'); } // Hardened child: 0x00 || ser256(kpar) || ser32(index) data = concatBytes(new Uint8Array([0]), priv, data); } else { // Normal child: serP(point(kpar)) || ser32(index) data = concatBytes(this.pubKey, data); } const I = hmac(sha512, this.chainCode, data); const childTweak = bytesToNumber(I.slice(0, 32)); const chainCode = I.slice(32); if (!secp.utils.isValidPrivateKey(childTweak)) { throw new Error('Tweak bigger than curve order'); } const opt: HDKeyOpt = { versions: this.versions, chainCode, depth: this.depth + 1, parentFingerprint: this.fingerprint, index, }; try { // Private parent key -> private child key if (this.privateKey) { const added = mod(this.privKey! + childTweak, secp.CURVE.n); if (!secp.utils.isValidPrivateKey(added)) { throw new Error('The tweak was out of range or the resulted private key is invalid'); } opt.privateKey = added; } else { const added = Point.fromHex(this.pubKey).add(Point.fromPrivateKey(childTweak)); // Cryptographically impossible: hmac-sha512 preimage would need to be found if (added.equals(Point.ZERO)) { throw new Error('The tweak was equal to negative P, which made the result key invalid'); } opt.publicKey = added.toRawBytes(true); } return new HDKey(opt); } catch (err) { return this.deriveChild(index + 1); } } public sign(hash: Uint8Array): Uint8Array { if (!this.privateKey) { throw new Error('No privateKey set!'); } assertBytes(hash, 32); return secp.sign(hash, this.privKey!).toCompactRawBytes(); } public verify(hash: Uint8Array, signature: Uint8Array): boolean { assertBytes(hash, 32); assertBytes(signature, 64); if (!this.publicKey) { throw new Error('No publicKey set!'); } let sig; try { sig = secp.Signature.fromCompact(signature); } catch (error) { return false; } return secp.verify(sig, hash, this.publicKey); } public wipePrivateData(): this { this.privKey = undefined; if (this.privKeyBytes) { this.privKeyBytes.fill(0); this.privKeyBytes = undefined; } return this; } public toJSON(): { xpriv: string; xpub: string } { return { xpriv: this.privateExtendedKey, xpub: this.publicExtendedKey, }; } private serialize(version: number, key: Uint8Array) { if (!this.chainCode) { throw new Error('No chainCode set'); } assertBytes(key, 33); // version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33) return concatBytes( toU32(version), new Uint8Array([this.depth]), toU32(this.parentFingerprint), toU32(this.index), this.chainCode, key ); } }