354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
/**
|
|
* 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, type TArg, type TRet } 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));
|
|
})();
|
|
|
|
/** Network-specific BIP32 version bytes. */
|
|
export interface Versions {
|
|
/** 4-byte version used when serializing private extended keys. */
|
|
private: number;
|
|
/** 4-byte version used when serializing public extended keys. */
|
|
public: number;
|
|
}
|
|
|
|
const BITCOIN_VERSIONS: Versions = { private: 0x0488ade4, public: 0x0488b21e };
|
|
/** Hardened child index offset from BIP32. */
|
|
export const HARDENED_OFFSET: number = 0x80000000;
|
|
|
|
const hash160 = (data: TArg<Uint8Array>) => ripemd160(sha256(data));
|
|
const fromU32 = (data: TArg<Uint8Array>) => createView(data).getUint32(0, false);
|
|
const toU32 = (n: number): TRet<Uint8Array> => {
|
|
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;
|
|
};
|
|
|
|
interface HDKeyOpt {
|
|
versions?: Versions;
|
|
depth?: number;
|
|
index?: number;
|
|
parentFingerprint?: number;
|
|
chainCode?: Uint8Array;
|
|
publicKey?: Uint8Array;
|
|
privateKey?: Uint8Array;
|
|
}
|
|
|
|
/**
|
|
* 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(): 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;
|
|
}
|
|
// Returns the live private key buffer for this instance.
|
|
// Copy it first if you need an immutable snapshot.
|
|
get privateKey(): Uint8Array | null {
|
|
return this._privateKey || null;
|
|
}
|
|
get publicKey(): Uint8Array | null {
|
|
return this._publicKey || 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(Uint8Array.of(0), priv))
|
|
);
|
|
}
|
|
get publicExtendedKey(): string {
|
|
if (!this._publicKey) {
|
|
throw new Error('No public key');
|
|
}
|
|
return base58check.encode(this.serialize(this.versions.public, this._publicKey));
|
|
}
|
|
|
|
static fromMasterSeed(seed: Uint8Array, versions: Versions = BITCOIN_VERSIONS): HDKey {
|
|
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: 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);
|
|
}
|
|
readonly versions: Versions;
|
|
readonly depth: number = 0;
|
|
readonly index: number = 0;
|
|
readonly chainCode: Uint8Array | null = null;
|
|
readonly parentFingerprint: number = 0;
|
|
private _privateKey?: Uint8Array;
|
|
private _publicKey?: 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 ? 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: 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;
|
|
}
|
|
|
|
/**
|
|
* @param _I - Test-only override for the 64-byte HMAC-SHA512 output; normal callers must omit it.
|
|
*/
|
|
deriveChild(index: number, _I?: Uint8Array): HDKey {
|
|
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: HDKeyOpt = {
|
|
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: Uint8Array): Uint8Array {
|
|
if (!this._privateKey) {
|
|
throw new Error('No privateKey set!');
|
|
}
|
|
abytes(hash, 32);
|
|
return secp.sign(hash, this._privateKey, { prehash: false });
|
|
}
|
|
|
|
verify(hash: Uint8Array, signature: Uint8Array): boolean {
|
|
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(): this {
|
|
if (this._privateKey) {
|
|
this._privateKey.fill(0);
|
|
this._privateKey = undefined;
|
|
}
|
|
return this;
|
|
}
|
|
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');
|
|
}
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
type Tests = Readonly<{
|
|
deriveChildWithI(key: TArg<HDKey>, index: number, I: TArg<Uint8Array>): TRet<HDKey>;
|
|
}>;
|
|
|
|
export const __TESTS: TRet<Tests> = /* @__PURE__ */ Object.freeze({
|
|
deriveChildWithI(key: TArg<HDKey>, index: number, I: TArg<Uint8Array>): TRet<HDKey> {
|
|
// Bytes wrappers widen the exported test seam, but deriveChild still needs concrete inputs.
|
|
return (key as HDKey).deriveChild(index, I as Uint8Array) as TRet<HDKey>;
|
|
},
|
|
});
|