@safeheron/crypto-bip32
Version:
HDKey in js(embrace bip32-secp256k1, bip32-ed25519)
318 lines • 13.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Secp256k1HDKey = void 0;
const bs58check = require("bs58check");
const BN = require("bn.js");
const cryptoJS = require("crypto-js");
const elliptic = require("elliptic");
const Secp256k1 = elliptic.ec('secp256k1');
const crypto_utils_1 = require("@safeheron/crypto-utils");
const assert = require("assert");
const MASTER_SECRET = cryptoJS.enc.Utf8.parse('Bitcoin seed');
const HARDENED_OFFSET = 0x80000000;
const LEN = 78;
const ZERO = new BN('0', 16);
// Bitcoin hardcoded by default, can use package `coininfo` for others
const BITCOIN_VERSIONS = { private: 0x0488ADE4, public: 0x0488B21E };
function Buffer2CryptoJSArray(buffer) {
const hexStr = buffer.toString('hex');
return crypto_utils_1.Hex.toCryptoJSBytes(crypto_utils_1.Hex.pad64(hexStr));
}
class Secp256k1HDKey {
constructor(versions) {
this.versions = versions || BITCOIN_VERSIONS;
this.depth = 0;
this.index = 0;
this._privateKey = null; // BN
this._publicKey = null; // elliptic point
this.chainCode = null;
this._fingerprint = 0;
this.parentFingerprint = 0;
this._identifier = null;
}
get fingerprint() {
return this._fingerprint;
}
get identifier() {
return this._identifier;
}
get pubKeyHash() {
return this._identifier;
}
get privateKey() {
return this._privateKey;
}
set privateKey(value) {
if (typeof value === 'string') {
if (value.length !== 64)
throw 'Private key must be 32 bytes.';
var valueNum = new BN(value, 16);
if (valueNum.gt(Secp256k1.n) || valueNum.eq(0))
throw 'Invalid private key';
this._privateKey = valueNum;
}
else {
if (value.gt(Secp256k1.n) || value.eq(0))
throw 'Invalid private key';
this._privateKey = value;
}
this._publicKey = Secp256k1.g.mul(this._privateKey);
this._identifier = Secp256k1HDKey.hash160(cryptoJS.enc.Hex.parse(this._publicKey.encodeCompressed('hex')));
this._fingerprint = parseInt(cryptoJS.enc.Hex.stringify(this._identifier).substr(0, 4 * 2), 16);
}
get publicKey() {
return this._publicKey;
}
set publicKey(value) {
if (typeof value === 'string') {
if (value.length !== 33 * 2)
throw 'Public key must be 33 bytes(compressed public key).';
this._publicKey = Secp256k1.curve.decodePoint(value, 'hex');
}
else {
if (!Secp256k1.curve.validate(value))
throw 'Invalid public key';
this._publicKey = value;
}
this._identifier = Secp256k1HDKey.hash160(cryptoJS.enc.Hex.parse(this._publicKey.encodeCompressed('hex')));
this._fingerprint = parseInt(cryptoJS.enc.Hex.stringify(this._identifier).substr(0, 4 * 2), 16);
this._privateKey = null;
}
get xprv() {
if (this._privateKey) {
// with prefix
let keyBuff = '00' + crypto_utils_1.Hex.pad64(this._privateKey.toString(16));
return bs58check.encode(Secp256k1HDKey.serialize(this, this.versions.private, keyBuff));
}
else
return null;
}
get xpub() {
return bs58check.encode(Secp256k1HDKey.serialize(this, this.versions.public, this._publicKey.encodeCompressed('hex')));
}
derive(path) {
if (path === 'm' || path === 'M' || path === "m'" || path === "M'") {
return this;
}
let entries = path.split('/');
let hdkey = this;
entries.forEach(function (c, i) {
if (i === 0) {
assert(/^[mM]{1}/.test(c), 'Path must start with "m" or "M"');
return;
}
let hardened = (c.length > 1) && (c[c.length - 1] === "'");
if (c[c.length - 1] === "'") {
c = c.substring(0, c.length - 1);
}
if (/^\d+$/.test(c)) {
let childIndex = parseInt(c, 10); // & (HARDENED_OFFSET - 1)
assert((childIndex < HARDENED_OFFSET), 'Invalid index');
if (hardened)
childIndex += HARDENED_OFFSET;
// @ts-ignore
hdkey = hdkey.deriveChild(childIndex);
}
else {
throw "Invalid index";
}
});
return hdkey;
}
deriveChild(index) {
let isHardened = index >= HARDENED_OFFSET;
let data = "";
if (isHardened) { // Hardened child
assert(this.privateKey, 'Could not derive hardened child key');
// data = 0x00 || ser256(kpar) || ser32(index)
data = '00' + crypto_utils_1.Hex.pad64(this._privateKey.toString(16)) + crypto_utils_1.Hex.pad8(index.toString(16));
}
else { // Normal child
// data = serP(point(kpar)) || ser32(index)
// = serP(Kpar) || ser32(index)
data = this._publicKey.encodeCompressed('hex') + crypto_utils_1.Hex.pad8(index.toString(16));
}
let chainCodeWordArray = Buffer2CryptoJSArray(this.chainCode);
let dataWordArray = cryptoJS.enc.Hex.parse(data);
let I = cryptoJS.enc.Hex.stringify(cryptoJS.HmacSHA512(dataWordArray, chainCodeWordArray));
let IL = new BN(I.substr(0, 64), 16);
let IR = new BN(I.substr(64), 16);
let hd = new Secp256k1HDKey(this.versions);
// Private parent key -> private child key
if (this.privateKey) {
// ki = parse256(IL) + kpar (mod n)
try {
let _privateKey = this._privateKey.add(IL).umod(Secp256k1.n);
// throw if IL >= n || (privateKey + IL) === 0
if (IL.gt(Secp256k1.n) || _privateKey.eqn(0))
throw "Invalid child private key!";
hd.privateKey = _privateKey;
}
catch (err) {
// In case parse256(IL) >= n or ki == 0, one should proceed with the next value for i
return this.deriveChild(index + 1);
}
// Public parent key -> public child key
}
else {
// Ki = point(parse256(IL)) + Kpar
// = G*IL + Kpar
try {
let _publicKey = this._publicKey.add(Secp256k1.g.mul(IL));
// throw if IL >= n || (g**IL + publicKey) is infinity
if (IL.gt(Secp256k1.n) || _publicKey.isInfinity())
throw "Invalid child public key!";
hd.publicKey = _publicKey;
}
catch (err) {
// In case parse256(IL) >= n or Ki is the point at infinity, one should proceed with the next value for i
return this.deriveChild(index + 1);
}
}
hd.chainCode = IR;
hd.depth = this.depth + 1;
hd.parentFingerprint = this.fingerprint; // .readUInt32BE(0)
hd.index = index;
return hd;
}
publicDerive(path) {
let delta = ZERO;
if (path === 'm' || path === 'M' || path === "m'" || path === "M'") {
return [this, new BN(0)];
}
if (path.indexOf("'") != -1) {
throw "Could not derive hardened child key!";
}
let entries = path.split('/');
let hdkey = this;
entries.forEach(function (c, i) {
if (i === 0) {
assert(/^[mM]{1}/.test(c), 'Path must start with "m" or "M"');
return;
}
// let hardened = (c.length > 1) && (c[c.length - 1] === "'")
if (/^\d+$/.test(c)) {
let childIndex = parseInt(c, 10); // & (HARDENED_OFFSET - 1)
assert(childIndex < HARDENED_OFFSET, 'Invalid index');
// if (hardened) childIndex += HARDENED_OFFSET
const [_hdkey, _delta] = hdkey.publicDeriveChild(childIndex);
delta = delta.add(_delta).umod(Secp256k1.n);
// @ts-ignore
hdkey = _hdkey;
}
else {
throw "Invalid index";
}
});
return [hdkey, delta];
}
publicDeriveChild(index) {
//let isHardened = index >= HARDENED_OFFSET
// Normal child
// data = serP(point(kpar)) || ser32(index)
// = serP(Kpar) || ser32(index)
let data = this._publicKey.encodeCompressed('hex') + crypto_utils_1.Hex.pad8(index.toString(16));
let chainCodeWordArray = Buffer2CryptoJSArray(this.chainCode);
let dataWordArray = cryptoJS.enc.Hex.parse(data);
let I = cryptoJS.enc.Hex.stringify(cryptoJS.HmacSHA512(dataWordArray, chainCodeWordArray));
let IL = new BN(I.substr(0, 64), 16);
let IR = new BN(I.substr(64), 16);
let hd = new Secp256k1HDKey(this.versions);
// Public parent key -> public child key
// Ki = point(parse256(IL)) + Kpar
// = G*IL + Kpar
try {
let _publicKey = this._publicKey.add(Secp256k1.g.mul(IL));
// throw if IL >= n || (g**IL + publicKey) is infinity
if (IL.gt(Secp256k1.n) || _publicKey.isInfinity())
throw "Invalid child public key!";
hd.publicKey = _publicKey;
}
catch (err) {
// In case parse256(IL) >= n or Ki is the point at infinity, one should proceed with the next value for i
return this.publicDeriveChild(index + 1);
}
hd.chainCode = IR;
hd.depth = this.depth + 1;
hd.parentFingerprint = this.fingerprint; // .readUInt32BE(0)
hd.index = index;
let delta = IL;
return [hd, delta];
}
sign(hash) {
throw "Not implemented!";
}
verify(hash, signature) {
throw "Not implemented!";
}
static fromMasterSeed(seedWordArray) {
let I = cryptoJS.enc.Hex.stringify(cryptoJS.HmacSHA512(seedWordArray, MASTER_SECRET));
let IL = new BN(I.substr(0, 64), 16);
let IR = new BN(I.substr(64), 16);
if ((IL.gt(Secp256k1.n)) || (IL.eqn(0))) {
throw "Invalid Master Key!";
}
let hdkey = new Secp256k1HDKey(BITCOIN_VERSIONS);
hdkey.privateKey = IL;
hdkey.chainCode = IR;
return hdkey;
}
static fromMasterSeedHex(seedHex) {
return Secp256k1HDKey.fromMasterSeed(cryptoJS.enc.Hex.parse(seedHex));
}
static fromPublicKeyAndChainCode(publicKey, chainCode) {
let hdkey = new Secp256k1HDKey(BITCOIN_VERSIONS);
hdkey.publicKey = publicKey;
hdkey.chainCode = chainCode;
return hdkey;
}
static fromPrivateKeyAndChainCode(privateKey, chainCode) {
let hdkey = new Secp256k1HDKey(BITCOIN_VERSIONS);
hdkey.privateKey = privateKey;
hdkey.chainCode = chainCode;
return hdkey;
}
static fromExtendedKey(base58key) {
// => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
let versions = BITCOIN_VERSIONS;
let hdkey = new Secp256k1HDKey(versions);
let keyBuffer = bs58check.decode(base58key);
let keyBufferHex = keyBuffer.toString('hex');
let version = parseInt(keyBufferHex.substr(0, 4 * 2), 16);
assert(version === versions.private || version === versions.public, 'Version mismatch: does not match private or public');
hdkey.depth = parseInt(keyBufferHex.substr(4 * 2, 2), 16);
hdkey.parentFingerprint = parseInt(keyBufferHex.substr(5 * 2, 4 * 2), 16);
hdkey.index = parseInt(keyBufferHex.substr(9 * 2, 4 * 2), 16);
hdkey.chainCode = new BN(keyBufferHex.substr(13 * 2, 32 * 2), 16);
let prefix = parseInt(keyBufferHex.substr(45 * 2, 2), 16);
if (prefix === 0) { // private
assert(version === versions.private, 'Version mismatch: version does not match private');
hdkey.privateKey = new BN(keyBufferHex.substr(46 * 2, 32 * 2), 16); // cut off first 0x0 byte
}
else {
assert(version === versions.public, 'Version mismatch: version does not match public');
hdkey.publicKey = Secp256k1.curve.decodePoint(keyBufferHex.substr(45 * 2, 33 * 2), 'hex');
}
return hdkey;
}
static serialize(hdkey, version, key) {
// => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
let buffer = Buffer.allocUnsafe(LEN);
buffer.writeUInt32BE(version, 0);
buffer.writeUInt8(hdkey.depth, 4);
let fingerprint = hdkey.depth ? hdkey.parentFingerprint : 0x00000000;
buffer.writeUInt32BE(fingerprint, 5);
buffer.writeUInt32BE(hdkey.index, 9);
let chainCodeBuffer = Buffer.from(crypto_utils_1.Hex.pad64(hdkey.chainCode.toString(16)), "hex");
chainCodeBuffer.copy(buffer, 13);
Buffer.from(key, 'hex').copy(buffer, 45);
return buffer;
}
static hash160(wordArray) {
let sha = cryptoJS.SHA256(wordArray);
return cryptoJS.RIPEMD160(sha);
}
}
exports.Secp256k1HDKey = Secp256k1HDKey;
Secp256k1HDKey.HARDENED_OFFSET = HARDENED_OFFSET;
//# sourceMappingURL=Secp256k1HDKey.js.map