From 56f15eff41356938cefba4ed4e2efbda5945aaae Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Fri, 7 Dec 2018 12:15:51 -0200 Subject: [PATCH] initial commit --- README.md | 19 +++- hdkey.js | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 53 +++++++++++ package.json | 21 +++++ 4 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 hdkey.js create mode 100644 index.js create mode 100644 package.json diff --git a/README.md b/README.md index b295002..4730caf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # cli-seed-export -simple js cli tool to recover old key +simple js cli tool to recover status key + +install: +``` +git clone https://github.com/status-im/cli-seed-export +cd cli-seed-export +npm install +``` + +recovering [old key](https://ideas.status.im/ideas/142-wallet-compatibility/README) _(varies with password)_: + +`npm start -- old` + +recovering moot key: +`npm start -- moot` + +recovering normal key: +`npm start` diff --git a/hdkey.js b/hdkey.js new file mode 100644 index 0000000..f0407a2 --- /dev/null +++ b/hdkey.js @@ -0,0 +1,241 @@ +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var crypto = require('crypto') +var cs = require('coinstring') +var secp256k1 = require('secp256k1') + +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.') + assert(secp256k1.privateKeyVerify(value) === true, 'Invalid private key') + + this._privateKey = value + this._publicKey = secp256k1.publicKeyCreate(value, true) + 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.') + assert(secp256k1.publicKeyVerify(value) === true, 'Invalid public key') + + this._publicKey = secp256k1.publicKeyConvert(value, 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 () { + if (this._privateKey) return cs.encode(serialize(this, this.versions.private, Buffer.concat([Buffer.alloc(1, 0), this.privateKey]))) + else return null + } +}) + +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(/^[mM]{1}/.test(c), 'Path must start with "m" or "M"') + 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 = Buffer.allocUnsafe(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 = Buffer.alloc(1, 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) + + // Private parent key -> private child key + if (this.privateKey) { + // ki = parse256(IL) + kpar (mod n) + try { + hd.privateKey = secp256k1.privateKeyTweakAdd(this.privateKey, IL) + // throw if IL >= n || (privateKey + IL) === 0 + } catch (err) { + // In case parse256(IL) >= n or ki == 0, one should proceed with the next value for i + return this.derive(index + 1) + } + // Public parent key -> public child key + } else { + // Ki = point(parse256(IL)) + Kpar + // = G*IL + Kpar + try { + hd.publicKey = secp256k1.publicKeyTweakAdd(this.publicKey, IL, true) + // throw if IL >= n || (g**IL + publicKey) is infinity + } 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.derive(index + 1, isHardened) + } + } + + hd.chainCode = IR + hd.depth = this.depth + 1 + hd.parentFingerprint = this.fingerprint// .readUInt32BE(0) + hd.index = index + + return hd +} + +HDKey.prototype.sign = function (hash) { + return secp256k1.sign(hash, this.privateKey).signature +} + +HDKey.prototype.verify = function (hash, signature) { + return secp256k1.verify(hash, signature, this.publicKey) +} + +HDKey.prototype.wipePrivateData = function () { + if (this._privateKey) crypto.randomBytes(this._privateKey.length).copy(this._privateKey) + this._privateKey = null + return this +} + +HDKey.prototype.toJSON = function () { + return { + xpriv: this.privateExtendedKey, + xpub: this.publicExtendedKey + } +} + +HDKey.fromMasterSeed = function (seedBuffer, salt, versions) { + var I = crypto.createHmac('sha512', Buffer.from(salt, 'utf8')).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 = Buffer.allocUnsafe(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() +} + +HDKey.HARDENED_OFFSET = HARDENED_OFFSET +module.exports = HDKey diff --git a/index.js b/index.js new file mode 100644 index 0000000..db3b0b3 --- /dev/null +++ b/index.js @@ -0,0 +1,53 @@ +const StatusJS = require('status-js-api'); +const ethUtil = require('ethereumjs-util'); +const crypto = require('crypto'); +const HDKey = require('./hdkey.js'); +let prompt = require('password-prompt') + +const moot = "source indoor foster invest draft mechanic fortune lion spike what town one" +const defaultSalt = "mnemonic"; +const defaultHDSalt = "Bitcoin seed"; +//https://github.com/status-im/status-go/blob/develop/account/accounts.go#L142 +async function generateAccount (mnemonic, password = "", salt = defaultSalt, hdSalt = defaultHDSalt) { + + console.log("Seed Phrase.: " + mnemonic); + + //https://github.com/status-im/status-go/blob/develop/extkeys/mnemonic.go#L128 + var mnemonicKey = crypto.pbkdf2Sync(mnemonic, salt+password, 2048, 64, 'sha512') + + const hdwallet = HDKey.fromMasterSeed(mnemonicKey, hdSalt); + const path = "m/44'/60'/0'/0/0"; + const wallet = hdwallet.derive(path); + + + let privateKey = '0x' + wallet.privateKey.toString('hex'); + console.log("Private Key.: " + privateKey); + + + let publicKey = ethUtil.privateToPublic(privateKey).toString('hex'); + console.log("Public Key..: 0x04" + publicKey); + + let address = '0x' + ethUtil.publicToAddress('0x' + publicKey).toString('hex'); + console.log("Address.....: " + address); + let statusname = await new StatusJS().getUserName('0x04' + publicKey); + console.log("User Name...: " + statusname); +} + +async function main(mode){ + if (mode == "moot") { + console.log("#moot") + generateAccount(moot); + return; + } + let seed = await prompt('Recovery Phrase: ') + if (mode == "old"){ + let password = await prompt('Account Password: ') + generateAccount(seed, password, "status-im", "status-im"); + return; + } + + generateAccount(seed); + +} + +main(process.argv[2]); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f2ef006 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "status-seed-export", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "coinstring": "^2.0.0", + "crypto": "^1.0.1", + "ethereumjs-util": "^6.0.0", + "password-prompt": "^1.0.7", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1", + "status-js-api": "^1.0.9" + } +}