mirror of https://github.com/lukechilds/hdkey.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
231 lines
6.8 KiB
231 lines
6.8 KiB
var assert = require('assert')
|
|
var crypto = require('crypto')
|
|
var BigInteger = require('bigi')
|
|
var cs = require('coinstring')
|
|
var ecurve = require('ecurve')
|
|
var curve = ecurve.getCurveByName('secp256k1')
|
|
var Point = ecurve.Point
|
|
|
|
module.exports = HDKey
|
|
|
|
var MASTER_SECRET = new Buffer('Bitcoin seed')
|
|
var HARDENED_OFFSET = 0x80000000
|
|
var LEN = 78
|
|
|
|
// Bitcoin hardcoded by default, can use package `coininfo` for others
|
|
var BITCOIN_VERSIONS = {private: 0x0488ADE4, public: 0x0488B21E}
|
|
|
|
function HDKey (versions) {
|
|
this.versions = versions || BITCOIN_VERSIONS
|
|
this.depth = 0
|
|
this.index = 0
|
|
this._privateKey = null
|
|
this._publicKey = null
|
|
this.chainCode = null
|
|
this._fingerprint = 0
|
|
this.parentFingerprint = 0
|
|
}
|
|
|
|
Object.defineProperty(HDKey.prototype, 'fingerprint', { get: function () { return this._fingerprint } })
|
|
Object.defineProperty(HDKey.prototype, 'identifier', {get: function () { return this._identifier } })
|
|
Object.defineProperty(HDKey.prototype, 'pubKeyHash', {get: function () { return this.identifier }})
|
|
|
|
Object.defineProperty(HDKey.prototype, 'privateKey', {
|
|
get: function () {
|
|
return this._privateKey
|
|
},
|
|
set: function (value) {
|
|
assert.equal(value.length, 32, 'Private key must be 32 bytes.')
|
|
this._privateKey = value
|
|
this._publicPoint = curve.G.multiply(BigInteger.fromBuffer(this._privateKey))
|
|
this._publicKey = this._publicPoint.getEncoded(true) // force compressed point
|
|
this._identifier = hash160(this.publicKey)
|
|
this._fingerprint = this._identifier.slice(0, 4).readUInt32BE(0)
|
|
}
|
|
})
|
|
|
|
Object.defineProperty(HDKey.prototype, 'publicKey', {
|
|
get: function () {
|
|
return this._publicKey
|
|
},
|
|
set: function (value) {
|
|
assert(value.length === 33 || value.length === 65, 'Public key must be 33 or 65 bytes.')
|
|
this._publicPoint = Point.decodeFrom(curve, value)
|
|
this._publicKey = this._publicPoint.getEncoded(true) // force compressed point
|
|
this._identifier = hash160(this.publicKey)
|
|
this._fingerprint = this._identifier.slice(0, 4).readUInt32BE(0)
|
|
this._privateKey = null
|
|
}
|
|
})
|
|
|
|
Object.defineProperty(HDKey.prototype, 'privateExtendedKey', {
|
|
get: function () {
|
|
return cs.encode(serialize(this, this.versions.private, Buffer.concat([new Buffer([0]), this.privateKey])))
|
|
}
|
|
})
|
|
|
|
Object.defineProperty(HDKey.prototype, 'publicExtendedKey', {
|
|
get: function () {
|
|
return cs.encode(serialize(this, this.versions.public, this.publicKey))
|
|
}
|
|
})
|
|
|
|
HDKey.prototype.derive = function (path) {
|
|
if (path === 'm' || path === 'M' || path === "m'" || path === "M'") {
|
|
return this
|
|
}
|
|
|
|
var entries = path.split('/')
|
|
var hdkey = this
|
|
entries.forEach(function (c, i) {
|
|
if (i === 0) {
|
|
assert(c, 'm', 'Invalid path')
|
|
return
|
|
}
|
|
|
|
var hardened = (c.length > 1) && (c[c.length - 1] === "'")
|
|
var childIndex = parseInt(c, 10) // & (HARDENED_OFFSET - 1)
|
|
assert(childIndex < HARDENED_OFFSET, 'Invalid index')
|
|
if (hardened) childIndex += HARDENED_OFFSET
|
|
|
|
hdkey = hdkey.deriveChild(childIndex)
|
|
})
|
|
|
|
return hdkey
|
|
}
|
|
|
|
HDKey.prototype.deriveChild = function (index) {
|
|
var isHardened = index >= HARDENED_OFFSET
|
|
var indexBuffer = new Buffer(4)
|
|
indexBuffer.writeUInt32BE(index, 0)
|
|
|
|
var data
|
|
|
|
if (isHardened) { // Hardened child
|
|
assert(this.privateKey, 'Could not derive hardened child key')
|
|
|
|
var pk = this.privateKey
|
|
var zb = new Buffer([0])
|
|
pk = Buffer.concat([zb, pk])
|
|
|
|
// data = 0x00 || ser256(kpar) || ser32(index)
|
|
data = Buffer.concat([pk, indexBuffer])
|
|
} else { // Normal child
|
|
// data = serP(point(kpar)) || ser32(index)
|
|
// = serP(Kpar) || ser32(index)
|
|
data = Buffer.concat([this.publicKey, indexBuffer])
|
|
}
|
|
|
|
var I = crypto.createHmac('sha512', this.chainCode).update(data).digest()
|
|
var IL = I.slice(0, 32)
|
|
var IR = I.slice(32)
|
|
|
|
var hd = new HDKey(this.versions)
|
|
var pIL = BigInteger.fromBuffer(IL)
|
|
|
|
// Private parent key -> private child key
|
|
if (this.privateKey) {
|
|
// ki = parse256(IL) + kpar (mod n)
|
|
var ki = pIL.add(BigInteger.fromBuffer(this.privateKey)).mod(curve.n)
|
|
|
|
// In case parse256(IL) >= n or ki == 0, one should proceed with the next value for i
|
|
if (pIL.compareTo(curve.n) >= 0 || ki.signum() === 0) {
|
|
return this.derive(index + 1)
|
|
}
|
|
|
|
// if less than 32 bytes, pad with 0's
|
|
hd.privateKey = ki.toBuffer(32)
|
|
|
|
// Public parent key -> public child key
|
|
} else {
|
|
// Ki = point(parse256(IL)) + Kpar
|
|
// = G*IL + Kpar
|
|
var Ki = curve.G.multiply(pIL).add(this._publicPoint)
|
|
|
|
// In case parse256(IL) >= n or Ki is the point at infinity, one should proceed with the next value for i
|
|
if (pIL.compareTo(curve.n) >= 0 || curve.isInfinity(Ki)) {
|
|
return this.derive(index + 1)
|
|
}
|
|
|
|
hd.publicKey = Ki.getEncoded(true)
|
|
}
|
|
|
|
hd.chainCode = IR
|
|
hd.depth = this.depth + 1
|
|
hd.parentFingerprint = this.fingerprint// .readUInt32BE(0)
|
|
hd.index = index
|
|
|
|
return hd
|
|
}
|
|
|
|
HDKey.prototype.toJSON = function () {
|
|
return {
|
|
xpriv: this.privateExtendedKey,
|
|
xpub: this.publicExtendedKey
|
|
}
|
|
}
|
|
|
|
HDKey.fromMasterSeed = function (seedBuffer, versions) {
|
|
var I = crypto.createHmac('sha512', MASTER_SECRET).update(seedBuffer).digest()
|
|
var IL = I.slice(0, 32)
|
|
var IR = I.slice(32)
|
|
|
|
var hdkey = new HDKey(versions)
|
|
hdkey.chainCode = IR
|
|
hdkey.privateKey = IL
|
|
|
|
return hdkey
|
|
}
|
|
|
|
HDKey.fromExtendedKey = function (base58key, versions) {
|
|
// => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
|
|
versions = versions || BITCOIN_VERSIONS
|
|
var hdkey = new HDKey(versions)
|
|
|
|
var keyBuffer = cs.decode(base58key)
|
|
|
|
var version = keyBuffer.readUInt32BE(0)
|
|
assert(version === versions.private || version === versions.public, 'Version mismatch: does not match private or public')
|
|
|
|
hdkey.depth = keyBuffer.readUInt8(4)
|
|
hdkey.parentFingerprint = keyBuffer.readUInt32BE(5)
|
|
hdkey.index = keyBuffer.readUInt32BE(9)
|
|
hdkey.chainCode = keyBuffer.slice(13, 45)
|
|
|
|
var key = keyBuffer.slice(45)
|
|
if (key.readUInt8(0) === 0) { // private
|
|
assert(version === versions.private, 'Version mismatch: version does not match private')
|
|
hdkey.privateKey = key.slice(1) // cut off first 0x0 byte
|
|
} else {
|
|
assert(version === versions.public, 'Version mismatch: version does not match public')
|
|
hdkey.publicKey = key
|
|
}
|
|
|
|
return hdkey
|
|
}
|
|
|
|
HDKey.fromJSON = function (obj) {
|
|
return HDKey.fromExtendedKey(obj.xpriv)
|
|
}
|
|
|
|
function serialize (hdkey, version, key) {
|
|
// => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
|
|
var buffer = new Buffer(LEN)
|
|
|
|
buffer.writeUInt32BE(version, 0)
|
|
buffer.writeUInt8(hdkey.depth, 4)
|
|
|
|
var fingerprint = hdkey.depth ? hdkey.parentFingerprint : 0x00000000
|
|
buffer.writeUInt32BE(fingerprint, 5)
|
|
buffer.writeUInt32BE(hdkey.index, 9)
|
|
|
|
hdkey.chainCode.copy(buffer, 13)
|
|
key.copy(buffer, 45)
|
|
|
|
return buffer
|
|
}
|
|
|
|
function hash160 (buf) {
|
|
var sha = crypto.createHash('sha256').update(buf).digest()
|
|
return crypto.createHash('rmd160').update(sha).digest()
|
|
}
|
|
|