From 18ee2c518c252a18fa172a8a7092c58cf6c0b7c5 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Fri, 1 Feb 2019 18:39:50 -0500 Subject: [PATCH] Support for xpub and xpriv derivation and generating extended keys; no fromExtendedKey yet (#405). --- gulpfile.js | 2 +- src.ts/utils/basex.ts | 143 ++++++++++++++++++++++++++++++++ src.ts/utils/hdnode.ts | 168 +++++++++++++++++++++++++------------- src.ts/utils/secp256k1.ts | 6 ++ src.ts/utils/sha2.ts | 4 + thirdparty.d.ts | 8 ++ 6 files changed, 272 insertions(+), 59 deletions(-) create mode 100644 src.ts/utils/basex.ts diff --git a/gulpfile.js b/gulpfile.js index 86100a3b8..45bdaf847 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -111,7 +111,7 @@ function taskBundle(name, options) { "elliptic/package.json" : ellipticPackage, // Remove RIPEMD160 and unneeded hashing algorithms - "hash.js/lib/hash/ripemd.js": "module.exports = {ripemd160: null}", + //"hash.js/lib/hash/ripemd.js": "module.exports = {ripemd160: null}", "hash.js/lib/hash/sha/1.js": empty, "hash.js/lib/hash/sha/224.js": empty, "hash.js/lib/hash/sha/384.js": empty, diff --git a/src.ts/utils/basex.ts b/src.ts/utils/basex.ts new file mode 100644 index 000000000..0b5d8420b --- /dev/null +++ b/src.ts/utils/basex.ts @@ -0,0 +1,143 @@ +/** + * var basex = require('base-x'); + * + * This implementation is heavily based on base-x. The main reason to + * deviate was to prevent the dependency of Buffer. + * + * Contributors: + * + * base-x encoding + * Forked from https://github.com/cryptocoinjs/bs58 + * Originally written by Mike Hearn for BitcoinJ + * Copyright (c) 2011 Google Inc + * Ported to JavaScript by Stefan Thomas + * Merged Buffer refactorings from base58-native by Stephen Pair + * Copyright (c) 2013 BitPay Inc + * + * The MIT License (MIT) + * + * Copyright base-x contributors (c) 2016 + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +import { arrayify, Arrayish } from "./bytes"; +import { defineReadOnly } from "./properties"; + +export class BaseX { + readonly alphabet: string; + readonly base: number; + + private _alphabetMap: { [ character: string ]: number }; + private _leader: string; + + constructor(alphabet: string) { + defineReadOnly(this, "alphabet", alphabet); + defineReadOnly(this, "base", alphabet.length); + + defineReadOnly(this, "_alphabetMap", { }); + defineReadOnly(this, "_leader", alphabet.charAt(0)); + + // pre-compute lookup table + for (let i = 0; i < alphabet.length; i++) { + this._alphabetMap[alphabet.charAt(i)] = i; + } + } + + encode(value: Arrayish | string): string { + let source = arrayify(value); + + if (source.length === 0) { return ''; } + + let digits = [ 0 ] + for (let i = 0; i < source.length; ++i) { + let carry = source[i]; + for (let j = 0; j < digits.length; ++j) { + carry += digits[j] << 8; + digits[j] = carry % this.base; + carry = (carry / this.base) | 0; + } + + while (carry > 0) { + digits.push(carry % this.base); + carry = (carry / this.base) | 0; + } + } + + let string = '' + + // deal with leading zeros + for (let k = 0; source[k] === 0 && k < source.length - 1; ++k) { + string += this._leader; + } + + // convert digits to a string + for (let q = digits.length - 1; q >= 0; --q) { + string += this.alphabet[digits[q]]; + } + + return string; + } + + decode(value: string): Uint8Array { + if (typeof(value) !== 'string') { + throw new TypeError('Expected String'); + } + + let bytes: Array = []; + if (value.length === 0) { return new Uint8Array(bytes); } + + bytes.push(0); + for (let i = 0; i < value.length; i++) { + let byte = this._alphabetMap[value[i]]; + + if (byte === undefined) { + throw new Error('Non-base' + this.base + ' character'); + } + + let carry = byte; + for (let j = 0; j < bytes.length; ++j) { + carry += bytes[j] * this.base; + bytes[j] = carry & 0xff; + carry >>= 8; + } + + while (carry > 0) { + bytes.push(carry & 0xff); + carry >>= 8; + } + } + + // deal with leading zeros + for (let k = 0; value[k] === this._leader && k < value.length - 1; ++k) { + bytes.push(0) + } + + return new Uint8Array(bytes.reverse()) + } +} + +const Base32 = new BaseX("abcdefghijklmnopqrstuvwxyz234567"); +const Base58 = new BaseX("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); + +export { Base32, Base58 }; + +//console.log(Base58.decode("Qmd2V777o5XvJbYMeMb8k2nU5f8d3ciUQ5YpYuWhzv8iDj")) +//console.log(Base58.encode(Base58.decode("Qmd2V777o5XvJbYMeMb8k2nU5f8d3ciUQ5YpYuWhzv8iDj"))) diff --git a/src.ts/utils/hdnode.ts b/src.ts/utils/hdnode.ts index 6984c306f..24792ffe7 100644 --- a/src.ts/utils/hdnode.ts +++ b/src.ts/utils/hdnode.ts @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; // See: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki // See: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki @@ -13,14 +13,15 @@ import { langEn } from '../wordlists/lang-en'; //import { register } from '../wordlists/wordlist'; //register(langEn); -import { arrayify, hexlify } from './bytes'; -import { bigNumberify } from './bignumber'; +import { Base58 } from "./basex"; +import { arrayify, concat, hexDataSlice, hexZeroPad, hexlify } from './bytes'; +import { BigNumber, bigNumberify } from './bignumber'; import { toUtf8Bytes, UnicodeNormalizationForm } from './utf8'; import { pbkdf2 } from './pbkdf2'; import { computeHmac, SupportedAlgorithms } from './hmac'; import { defineReadOnly, isType, setType } from './properties'; import { computeAddress, KeyPair } from './secp256k1'; -import { sha256 } from './sha2'; +import { ripemd160, sha256 } from './sha2'; const N = bigNumberify("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); @@ -30,9 +31,9 @@ import { Wordlist } from './wordlist'; // "Bitcoin seed" -var MasterSecret = toUtf8Bytes('Bitcoin seed'); +const MasterSecret = toUtf8Bytes('Bitcoin seed'); -var HardenedBit = 0x80000000; +const HardenedBit = 0x80000000; // Returns a byte with the MSB bits set function getUpperMask(bits: number): number { @@ -44,16 +45,26 @@ function getLowerMask(bits: number): number { return (1 << bits) - 1; } +function bytes32(value: Arrayish | BigNumber | number): string { + return hexZeroPad(hexlify(value), 32); +} + +function base58check(data: Uint8Array): string { + let checksum = hexDataSlice(sha256(sha256(data)), 0, 4); + return Base58.encode(concat([ data, checksum ])); +} + const _constructorGuard: any = {}; export const defaultPath = "m/44'/60'/0'/0/0"; export class HDNode { - private readonly keyPair: KeyPair; - readonly privateKey: string; readonly publicKey: string; + readonly fingerprint: string; + readonly parentFingerprint: string; + readonly address: string; readonly mnemonic: string; @@ -71,21 +82,28 @@ export class HDNode { * - fromMnemonic * - fromSeed */ - constructor(constructorGuard: any, privateKey: Arrayish, chainCode: Uint8Array, index: number, depth: number, mnemonic: string, path: string) { + constructor(constructorGuard: any, privateKey: string, publicKey: string, parentFingerprint: string, chainCode: string, index: number, depth: number, mnemonic: string, path: string) { errors.checkNew(this, HDNode); if (constructorGuard !== _constructorGuard) { throw new Error('HDNode constructor cannot be called directly'); } - defineReadOnly(this, 'keyPair', new KeyPair(privateKey)); + if (privateKey) { + let keyPair = new KeyPair(privateKey); + defineReadOnly(this, 'privateKey', keyPair.privateKey); + defineReadOnly(this, 'publicKey', keyPair.compressedPublicKey); + } else { + defineReadOnly(this, 'privateKey', null); + defineReadOnly(this, 'publicKey', hexlify(publicKey)); + } - defineReadOnly(this, 'privateKey', this.keyPair.privateKey); - defineReadOnly(this, 'publicKey', this.keyPair.compressedPublicKey); + defineReadOnly(this, 'parentFingerprint', parentFingerprint); + defineReadOnly(this, 'fingerprint', hexDataSlice(ripemd160(sha256(this.publicKey)), 0, 4)); defineReadOnly(this, 'address', computeAddress(this.publicKey)); - defineReadOnly(this, 'chainCode', hexlify(chainCode)); + defineReadOnly(this, 'chainCode', chainCode); defineReadOnly(this, 'index', index); defineReadOnly(this, 'depth', depth); @@ -96,22 +114,43 @@ export class HDNode { setType(this, 'HDNode'); } + get extendedKey(): string { + // We only support the mainnet values for now, but if anyone needs + // testnet values, let me know. I believe current senitment is that + // we should always use mainnet, and use BIP-44 to derive the network + // - Mainnet: public=0x0488B21E, private=0x0488ADE4 + // - Testnet: public=0x043587CF, private=0x04358394 + + if (this.depth >= 256) { throw new Error("Depth too large!"); } + + return base58check(concat([ + ((this.privateKey != null) ? "0x0488ADE4": "0x0488B21E"), + hexlify(this.depth), + this.parentFingerprint, + hexZeroPad(hexlify(this.index), 4), + this.chainCode, + ((this.privateKey != null) ? concat([ "0x00", this.privateKey ]): this.publicKey), + ])); + } + + neuter(): HDNode { + return new HDNode(_constructorGuard, null, this.publicKey, this.parentFingerprint, this.chainCode, this.index, this.depth, null, this.path); + } + private _derive(index: number): HDNode { - - // Public parent key -> public child key - if (!this.privateKey) { - if (index >= HardenedBit) { throw new Error('cannot derive child of neutered node'); } - throw new Error('not implemented'); - } - - var data = new Uint8Array(37); + if (index > 0xffffffff) { throw new Error("invalid index - " + String(index)); } // Base path - var mnemonic = this.mnemonic; - var path = this.path; + let path = this.path; if (path) { path += '/' + (index & ~HardenedBit); } + let data = new Uint8Array(37); + if (index & HardenedBit) { + if (!this.privateKey) { + throw new Error('cannot derive child of neutered node'); + } + // Data = 0x00 || ser_256(k_par) data.set(arrayify(this.privateKey), 1); @@ -120,39 +159,50 @@ export class HDNode { } else { // Data = ser_p(point(k_par)) - data.set(this.keyPair.publicKeyBytes); + data.set(arrayify(this.publicKey)); } // Data += ser_32(i) - for (var i = 24; i >= 0; i -= 8) { data[33 + (i >> 3)] = ((index >> (24 - i)) & 0xff); } + for (let i = 24; i >= 0; i -= 8) { data[33 + (i >> 3)] = ((index >> (24 - i)) & 0xff); } - var I = computeHmac(SupportedAlgorithms.sha512, this.chainCode, data); - var IL = bigNumberify(I.slice(0, 32)); - var IR = I.slice(32); + let I = computeHmac(SupportedAlgorithms.sha512, this.chainCode, data); + let IL = I.slice(0, 32); + let IR = I.slice(32); - var ki = IL.add(this.keyPair.privateKey).mod(N); + // The private key - return new HDNode(_constructorGuard, arrayify(ki), IR, index, this.depth + 1, mnemonic, path); + let ki: string = null + // The public key + let Ki: string = null; + + if (this.privateKey) { + ki = bytes32(bigNumberify(IL).add(this.privateKey).mod(N)); + } else { + let ek = new KeyPair(hexlify(IL)); + Ki = ek._addPoint(this.publicKey); + } + + return new HDNode(_constructorGuard, ki, Ki, this.fingerprint, bytes32(IR), index, this.depth + 1, this.mnemonic, path); } derivePath(path: string): HDNode { - var components = path.split('/'); + let components = path.split('/'); if (components.length === 0 || (components[0] === 'm' && this.depth !== 0)) { - throw new Error('invalid path'); + throw new Error('invalid path - ' + path); } if (components[0] === 'm') { components.shift(); } - var result: HDNode = this; - for (var i = 0; i < components.length; i++) { - var component = components[i]; + let result: HDNode = this; + for (let i = 0; i < components.length; i++) { + let component = components[i]; if (component.match(/^[0-9]+'$/)) { - var index = parseInt(component.substring(0, component.length - 1)); + let index = parseInt(component.substring(0, component.length - 1)); if (index >= HardenedBit) { throw new Error('invalid path index - ' + component); } result = result._derive(HardenedBit + index); } else if (component.match(/^[0-9]+$/)) { - var index = parseInt(component); + let index = parseInt(component); if (index >= HardenedBit) { throw new Error('invalid path index - ' + component); } result = result._derive(index); } else { @@ -166,17 +216,19 @@ export class HDNode { static isHDNode(value: any): value is HDNode { return isType(value, 'HDNode'); } + + static fromExtendedKey(extendedKey: string): HDNode { + return null; + } } - - function _fromSeed(seed: Arrayish, mnemonic: string): HDNode { let seedArray: Uint8Array = arrayify(seed); if (seedArray.length < 16 || seedArray.length > 64) { throw new Error('invalid seed'); } - var I: Uint8Array = arrayify(computeHmac(SupportedAlgorithms.sha512, MasterSecret, seedArray)); + let I: Uint8Array = arrayify(computeHmac(SupportedAlgorithms.sha512, MasterSecret, seedArray)); - return new HDNode(_constructorGuard, I.slice(0, 32), I.slice(32), 0, 0, mnemonic, 'm'); + return new HDNode(_constructorGuard, bytes32(I.slice(0, 32)), null, "0x00000000", bytes32(I.slice(32)), 0, 0, mnemonic, 'm'); } export function fromMnemonic(mnemonic: string, wordlist?: Wordlist): HDNode { @@ -193,7 +245,7 @@ export function fromSeed(seed: Arrayish): HDNode { export function mnemonicToSeed(mnemonic: string, password?: string): string { if (!password) { password = ''; } - var salt = toUtf8Bytes('mnemonic' + password, UnicodeNormalizationForm.NFKD); + let salt = toUtf8Bytes('mnemonic' + password, UnicodeNormalizationForm.NFKD); return hexlify(pbkdf2(toUtf8Bytes(mnemonic, UnicodeNormalizationForm.NFKD), salt, 2048, 64, 'sha512')); } @@ -202,18 +254,18 @@ export function mnemonicToEntropy(mnemonic: string, wordlist?: Wordlist): string if (!wordlist) { wordlist = langEn; } errors.checkNormalize(); - - var words = wordlist.split(mnemonic); + + let words = wordlist.split(mnemonic); if ((words.length % 3) !== 0) { throw new Error('invalid mnemonic'); } - var entropy = arrayify(new Uint8Array(Math.ceil(11 * words.length / 8))); + let entropy = arrayify(new Uint8Array(Math.ceil(11 * words.length / 8))); - var offset = 0; - for (var i = 0; i < words.length; i++) { - var index = wordlist.getWordIndex(words[i].normalize('NFKD')); + let offset = 0; + for (let i = 0; i < words.length; i++) { + let index = wordlist.getWordIndex(words[i].normalize('NFKD')); if (index === -1) { throw new Error('invalid mnemonic'); } - for (var bit = 0; bit < 11; bit++) { + for (let bit = 0; bit < 11; bit++) { if (index & (1 << (10 - bit))) { entropy[offset >> 3] |= (1 << (7 - (offset % 8))); } @@ -221,12 +273,12 @@ export function mnemonicToEntropy(mnemonic: string, wordlist?: Wordlist): string } } - var entropyBits = 32 * words.length / 3; + let entropyBits = 32 * words.length / 3; - var checksumBits = words.length / 3; - var checksumMask = getUpperMask(checksumBits); + let checksumBits = words.length / 3; + let checksumMask = getUpperMask(checksumBits); - var checksum = arrayify(sha256(entropy.slice(0, entropyBits / 8)))[0]; + let checksum = arrayify(sha256(entropy.slice(0, entropyBits / 8)))[0]; checksum &= checksumMask; if (checksum !== (entropy[entropy.length - 1] & checksumMask)) { @@ -243,10 +295,10 @@ export function entropyToMnemonic(entropy: Arrayish, wordlist?: Wordlist): strin throw new Error('invalid entropy'); } - var indices: Array = [ 0 ]; + let indices: Array = [ 0 ]; - var remainingBits = 11; - for (var i = 0; i < entropy.length; i++) { + let remainingBits = 11; + for (let i = 0; i < entropy.length; i++) { // Consume the whole byte (with still more to go) if (remainingBits > 8) { @@ -268,8 +320,8 @@ export function entropyToMnemonic(entropy: Arrayish, wordlist?: Wordlist): strin } // Compute the checksum bits - var checksum = arrayify(sha256(entropy))[0]; - var checksumBits = entropy.length / 4; + let checksum = arrayify(sha256(entropy))[0]; + let checksumBits = entropy.length / 4; checksum &= getUpperMask(checksumBits); // Shift the checksum into the word indices diff --git a/src.ts/utils/secp256k1.ts b/src.ts/utils/secp256k1.ts index 73b49b4c8..7fc10d0aa 100644 --- a/src.ts/utils/secp256k1.ts +++ b/src.ts/utils/secp256k1.ts @@ -62,6 +62,12 @@ export class KeyPair { let otherKeyPair = getCurve().keyFromPublic(arrayify(computePublicKey(otherKey))); return hexZeroPad('0x' + keyPair.derive(otherKeyPair.getPublic()).toString(16), 32); } + + _addPoint(other: Arrayish | string): string { + let p0 = getCurve().keyFromPublic(arrayify(this.publicKey)); + let p1 = getCurve().keyFromPublic(arrayify(other)); + return "0x" + p0.pub.add(p1.pub).encodeCompressed("hex"); + } } export function computePublicKey(key: Arrayish | string, compressed?: boolean): string { diff --git a/src.ts/utils/sha2.ts b/src.ts/utils/sha2.ts index 5ae83222c..3808e3a92 100644 --- a/src.ts/utils/sha2.ts +++ b/src.ts/utils/sha2.ts @@ -7,6 +7,10 @@ import { arrayify } from './bytes'; // Types import { Arrayish } from './bytes'; +export function ripemd160(data: Arrayish): string { + return '0x' + (hash.ripemd160().update(arrayify(data)).digest('hex')); +} + export function sha256(data: Arrayish): string { return '0x' + (hash.sha256().update(arrayify(data)).digest('hex')); } diff --git a/thirdparty.d.ts b/thirdparty.d.ts index 5576bc36d..ea7c53eaa 100644 --- a/thirdparty.d.ts +++ b/thirdparty.d.ts @@ -65,6 +65,11 @@ declare module "elliptic" { recoveryParam: number } + interface Point { + add(point: Point): Point; + encodeCompressed(enc: string): string + } + interface KeyPair { sign(message: Uint8Array, options: { canonical?: boolean }): Signature; getPublic(compressed: boolean, encoding?: string): string; @@ -72,6 +77,7 @@ declare module "elliptic" { getPrivate(encoding?: string): string; encode(encoding: string, compressed: boolean): string; derive(publicKey: BN): BN; + pub: Point; priv: BN; } @@ -83,6 +89,8 @@ declare module "elliptic" { keyFromPublic(publicKey: string | Uint8Array): KeyPair; keyFromPrivate(privateKey: string | Uint8Array): KeyPair; recoverPubKey(data: Uint8Array, signature: BasicSignature, recoveryParam: number): KeyPair; + +// curve: Curve; } }