UNPKG

cipher-ethereum

Version:

An Ethereum library used by Cipher Browser, a mobile Ethereum client

318 lines (278 loc) 8.83 kB
import * as bs58 from 'bs58' import * as crypto from 'crypto' import BN from 'bn.js' import { ec as EC } from 'elliptic' const HARDENED_KEY_OFFSET = 0x80000000 const secp256k1 = new EC('secp256k1') export interface VersionBytes { bip32: { public: number private: number } public: number } export const HDKEY_VERSIONS: { [key: string]: VersionBytes } = { bitcoinMain: { bip32: { public: 0x0488b21e, private: 0x0488ade4 }, public: 0 }, bitcoinTest: { bip32: { public: 0x043587cf, private: 0x04358394 }, public: 0 } } export interface HDKeyConstructorOptions { chainCode: Buffer privateKey?: Buffer | null publicKey?: Buffer | null index?: number depth?: number parentFingerprint?: Buffer version?: VersionBytes } export class HDKey { private _version: VersionBytes private _privateKey?: Buffer private _publicKey: Buffer private _chainCode: Buffer private _index: number private _depth: number private _parentFingerprint?: Buffer private _keyIdentifier: Buffer constructor ({ privateKey, publicKey, chainCode, index, depth, parentFingerprint, version }: HDKeyConstructorOptions) { if (!privateKey && !publicKey) { throw new Error('either private key or public key must be provided') } if (privateKey) { this._privateKey = privateKey const ecdh = crypto.createECDH('secp256k1') if ((ecdh as any).curve && (ecdh as any).curve.keyFromPrivate) { // ECDH is not native, fallback to pure-JS elliptic lib this._publicKey = Buffer.from( secp256k1.keyFromPrivate(privateKey).getPublic(true, 'hex'), 'hex' ) } else { ecdh.setPrivateKey(privateKey) this._publicKey = Buffer.from( ecdh.getPublicKey('hex', 'compressed'), 'hex' ) } } else if (publicKey) { this._publicKey = publicKey } this._chainCode = chainCode this._depth = depth || 0 this._index = index || 0 this._parentFingerprint = parentFingerprint this._keyIdentifier = hash160(this._publicKey) this._version = version || HDKEY_VERSIONS.bitcoinMain } static parseMasterSeed (seed: Buffer, version?: VersionBytes): HDKey { const i = hmacSha512('Bitcoin seed', seed) const iL = i.slice(0, 32) const iR = i.slice(32) return new HDKey({ privateKey: iL, chainCode: iR, version }) } static parseExtendedKey ( key: string, version: VersionBytes = HDKEY_VERSIONS.bitcoinMain ): HDKey { // version_bytes[4] || depth[1] || parent_fingerprint[4] || index[4] || chain_code[32] || key_data[33] || checksum[4] const decoded = Buffer.from(bs58.decode(key)) if (decoded.length > 112) { throw new Error('invalid extended key') } const checksum = decoded.slice(-4) const buf = decoded.slice(0, -4) if (!sha256(sha256(buf)).slice(0, 4).equals(checksum)) { throw new Error('invalid checksum') } let o: number = 0 const versionRead = buf.readUInt32BE(o) o += 4 const depth = buf.readUInt8(o) o += 1 let parentFingerprint: Buffer | undefined = buf.slice(o, (o += 4)) if (parentFingerprint.readUInt32BE(0) === 0) { parentFingerprint = undefined } const index = buf.readUInt32BE(o) o += 4 const chainCode = buf.slice(o, (o += 32)) const keyData = buf.slice(o) const privateKey = keyData[0] === 0 ? keyData.slice(1) : undefined const publicKey = keyData[0] !== 0 ? keyData : undefined if ( (privateKey && versionRead !== version.bip32.private) || (publicKey && versionRead !== version.bip32.public) ) { throw new Error('invalid version bytes') } return new HDKey({ privateKey, publicKey, chainCode, index, depth, parentFingerprint, version }) } get privateKey (): Buffer | null { return this._privateKey || null } get publicKey (): Buffer { return this._publicKey } get chainCode (): Buffer { return this._chainCode } get depth (): number { return this._depth } get parentFingerprint (): Buffer | null { return this._parentFingerprint || null } get index (): number { return this._index } get keyIdentifier (): Buffer { return this._keyIdentifier } get fingerprint (): Buffer { return this._keyIdentifier.slice(0, 4) } get version (): VersionBytes { return this._version } get extendedPrivateKey (): string | null { return this._privateKey ? this.serialize(this._version.bip32.private, this._privateKey) : null } get extendedPublicKey (): string { return this.serialize(this._version.bip32.public, this._publicKey) } derive (chain: string): HDKey { const c = chain.toLowerCase() let childKey: HDKey = this c.split('/').forEach(path => { const p = path.trim() if (p === 'm' || p === "m'" || p === '') { return } const index = Number.parseInt(p, 10) if (Number.isNaN(index)) { throw new Error('invalid child key derivation chain') } const hardened = p.slice(-1) === "'" childKey = childKey.deriveChildKey(index, hardened) }) return childKey } private deriveChildKey (childIndex: number, hardened: boolean): HDKey { if (childIndex >= HARDENED_KEY_OFFSET) { throw new Error('invalid index') } if (!this.privateKey && !this.publicKey) { throw new Error('either private key or public key must be provided') } let index: number = childIndex const data: Buffer = Buffer.alloc(37) let o: number = 0 if (hardened) { if (!this.privateKey) { throw new Error('cannot derive a hardened child key from a public key') } // 0x00 || ser256(kpar) || ser32(i) // 0x00[1] || parent_private_key[32] || child_index[4] index += HARDENED_KEY_OFFSET o += 1 o += this.privateKey.copy(data, o) } else { // serP(point(kpar)) || ser32(i) // compressed_parent_public_key[33] || child_index[4] o += this.publicKey.copy(data, o) } o += data.writeUInt32BE(index, o) const i = hmacSha512(this.chainCode, data) const iL = new BN(i.slice(0, 32)) const iR = i.slice(32) // if parse256(IL) >= n, the resulting key is invalid; proceed with the next value for i if (iL.cmp(secp256k1.n) >= 0) { return this.deriveChildKey(childIndex + 1, hardened) } if (this.privateKey) { // ki is parse256(IL) + kpar (mod n) const childKey = iL.add(new BN(this.privateKey)).mod(secp256k1.n) // if ki = 0, the resulting key is invalid; proceed with the next value for i if (childKey.cmp(new BN(0)) === 0) { return this.deriveChildKey(childIndex + 1, hardened) } return new HDKey({ depth: this.depth + 1, privateKey: childKey.toArrayLike(Buffer, 'be', 32), chainCode: iR, parentFingerprint: this.fingerprint, index, version: this.version }) } else { // Ki is point(parse256(IL)) + Kpar = G * IL + Kpar const parentKey = secp256k1.keyFromPublic(this.publicKey).pub const childKey = secp256k1.g.mul(iL).add(parentKey) // if Ki is the point at infinity, the resulting key is invalid; proceed with the next value for i if (childKey.isInfinity()) { return this.deriveChildKey(childIndex + 1, false) } const compressedChildKey = Buffer.from(childKey.encode(null, true)) return new HDKey({ depth: this.depth + 1, publicKey: compressedChildKey, chainCode: iR, parentFingerprint: this.fingerprint, index, version: this.version }) } } private serialize (version: number, key: Buffer): string { // version_bytes[4] || depth[1] || parent_fingerprint[4] || index[4] || chain_code[32] || key_data[33] || checksum[4] const buf = Buffer.alloc(78) let o: number = buf.writeUInt32BE(version, 0) o = buf.writeUInt8(this.depth, o) o += this.parentFingerprint ? this.parentFingerprint.copy(buf, o) : 4 o = buf.writeUInt32BE(this.index, o) o += this.chainCode.copy(buf, o) o += 33 - key.length key.copy(buf, o) const checksum = sha256(sha256(buf)).slice(0, 4) return bs58.encode(Buffer.concat([buf, checksum])) } } function hmacSha512 (key: Buffer | string, data: Buffer): Buffer { return crypto.createHmac('sha512', key).update(data).digest() } function sha256 (data: Buffer): Buffer { return crypto.createHash('sha256').update(data).digest() } function hash160 (data: Buffer): Buffer { const d = crypto.createHash('sha256').update(data).digest() return crypto.createHash('rmd160').update(d).digest() }