UNPKG

@scure/bip32

Version:

Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets over secp256k1

313 lines 11.9 kB
/** * BIP32 hierarchical deterministic (HD) wallets over secp256k1. * @module * @example * ```js * import { HDKey } from "@scure/bip32"; * import { sha256 } from '@noble/hashes/sha2.js'; * import { randomBytes } from '@noble/hashes/utils.js'; * const seed = randomBytes(32); * const root = HDKey.fromMasterSeed(seed); * const base58key = root.privateExtendedKey; * const restored = HDKey.fromExtendedKey(base58key); * const fromJson = HDKey.fromJSON({ xpriv: base58key }); * const child = fromJson.derive("m/0/2147483647'/1"); * const msgHash = sha256(new TextEncoder().encode('hello scure-bip32')); * * // props * [root.depth, root.index, root.chainCode]; * [restored.privateKey, restored.publicKey]; * const sig = child.sign(msgHash); * child.verify(msgHash, sig); * ``` */ /*! scure-bip32 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */ import { secp256k1 as secp } from '@noble/curves/secp256k1.js'; import { hmac } from '@noble/hashes/hmac.js'; import { ripemd160 } from '@noble/hashes/legacy.js'; import { sha256, sha512 } from '@noble/hashes/sha2.js'; import { abytes, concatBytes, createView } from '@noble/hashes/utils.js'; import { createBase58check } from '@scure/base'; const Point = /* @__PURE__ */ (() => secp.Point)(); const Fn = /* @__PURE__ */ (() => Point.Fn)(); const base58check = /* @__PURE__ */ createBase58check(sha256); const MASTER_SECRET = /* @__PURE__ */ (() => { return Uint8Array.from('Bitcoin seed'.split(''), (char) => char.charCodeAt(0)); })(); const BITCOIN_VERSIONS = { private: 0x0488ade4, public: 0x0488b21e }; /** Hardened child index offset from BIP32. */ export const HARDENED_OFFSET = 0x80000000; const hash160 = (data) => ripemd160(sha256(data)); const fromU32 = (data) => createView(data).getUint32(0, false); const toU32 = (n) => { if (typeof n !== 'number') throw new TypeError('invalid number, should be from 0 to 2**32-1, got ' + n); if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) throw new RangeError('invalid number, should be from 0 to 2**32-1, got ' + n); const buf = new Uint8Array(4); createView(buf).setUint32(0, n, false); return buf; }; /** * HDKey from BIP32 * @param opt - Node fields used to construct one HDKey instance. * @example * ```js * import { HDKey } from '@scure/bip32'; * import { randomBytes } from '@noble/hashes/utils.js'; * * const seed = randomBytes(32); * const root = HDKey.fromMasterSeed(seed); * const account0 = root.derive("m/0/1'"); * account0.publicKey; * ``` */ export class HDKey { get fingerprint() { if (!this.pubHash) { throw new Error('No publicKey set!'); } return fromU32(this.pubHash); } get identifier() { return this.pubHash; } get pubKeyHash() { return this.pubHash; } // Returns the live private key buffer for this instance. // Copy it first if you need an immutable snapshot. get privateKey() { return this._privateKey || null; } get publicKey() { return this._publicKey || null; } get privateExtendedKey() { const priv = this._privateKey; if (!priv) { throw new Error('No private key'); } return base58check.encode(this.serialize(this.versions.private, concatBytes(Uint8Array.of(0), priv))); } get publicExtendedKey() { if (!this._publicKey) { throw new Error('No public key'); } return base58check.encode(this.serialize(this.versions.public, this._publicKey)); } static fromMasterSeed(seed, versions = BITCOIN_VERSIONS) { abytes(seed); if (8 * seed.length < 128 || 8 * seed.length > 512) { throw new RangeError('HDKey: seed length must be between 128 and 512 bits; 256 bits is advised, got ' + seed.length); } const I = hmac(sha512, MASTER_SECRET, seed); const privateKey = I.slice(0, 32); const chainCode = I.slice(32); return new HDKey({ versions, chainCode, privateKey }); } static fromExtendedKey(base58key, versions = BITCOIN_VERSIONS) { // => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33) const keyBuffer = 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 }); } } static fromJSON(json) { return HDKey.fromExtendedKey(json.xpriv); } versions; depth = 0; index = 0; chainCode = null; parentFingerprint = 0; _privateKey; _publicKey; pubHash; constructor(opt) { 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 ? Uint8Array.from(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 (this.depth > 255) { throw new Error('HDKey: depth exceeds the serializable value 255'); } if (opt.publicKey && opt.privateKey) { throw new Error('HDKey: publicKey and privateKey at same time.'); } if (opt.privateKey) { if (!secp.utils.isValidSecretKey(opt.privateKey)) throw new Error('Invalid private key'); // Don't alias caller-owned secret buffers. this._privateKey = Uint8Array.from(opt.privateKey); this._publicKey = secp.getPublicKey(this._privateKey, true); } else if (opt.publicKey) { this._publicKey = Point.fromBytes(opt.publicKey).toBytes(true); // force compressed point } else { throw new Error('HDKey: no public or private key provided'); } this.pubHash = hash160(this._publicKey); } derive(path) { 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 = 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; } /** * @param _I - Test-only override for the 64-byte HMAC-SHA512 output; normal callers must omit it. */ deriveChild(index, _I) { if (!this._publicKey || !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(Uint8Array.of(0), priv, data); } else { // Normal child: serP(point(kpar)) || ser32(index) data = concatBytes(this._publicKey, data); } const out = _I || hmac(sha512, this.chainCode, data); abytes(out, 64); const childTweak = out.slice(0, 32); const chainCode = out.slice(32); const opt = { versions: this.versions, chainCode, depth: this.depth + 1, parentFingerprint: this.fingerprint, index, }; // Fail early instead of re-trying different index if (opt.depth > 255) { throw new Error('HDKey: depth exceeds the serializable value 255'); } try { const ctweak = Fn.fromBytes(childTweak); // BIP-32 private derivation retries only when parse256(I_L) >= n or k_i = 0. // BIP-32 public derivation retries only when parse256(I_L) >= n or K_i is infinity. // So I_L = 0 is valid here; Fn.fromBytes still rejects parse256(I_L) >= n. if (this._privateKey) { const added = Fn.create(Fn.fromBytes(this._privateKey) + ctweak); if (!Fn.isValidNot0(added)) { throw new Error('The tweak was out of range or the resulted private key is invalid'); } opt.privateKey = Fn.toBytes(added); } else { const point = Point.fromBytes(this._publicKey); const added = ctweak === 0n ? point : point.add(Point.BASE.multiply(ctweak)); // 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.toBytes(true); } return new HDKey(opt); } catch (err) { return this.deriveChild(index + 1); } } sign(hash) { if (!this._privateKey) { throw new Error('No privateKey set!'); } abytes(hash, 32); return secp.sign(hash, this._privateKey, { prehash: false }); } verify(hash, signature) { abytes(hash, 32); abytes(signature, 64); if (!this._publicKey) { throw new Error('No publicKey set!'); } return secp.verify(signature, hash, this._publicKey, { prehash: false }); } wipePrivateData() { if (this._privateKey) { this._privateKey.fill(0); this._privateKey = undefined; } return this; } toJSON() { return { xpriv: this.privateExtendedKey, xpub: this.publicExtendedKey, }; } serialize(version, key) { if (!this.chainCode) { throw new Error('No chainCode set'); } abytes(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); } } export const __TESTS = /* @__PURE__ */ Object.freeze({ deriveChildWithI(key, index, I) { // Bytes wrappers widen the exported test seam, but deriveChild still needs concrete inputs. return key.deriveChild(index, I); }, }); //# sourceMappingURL=index.js.map