UNPKG

eth-lightwallet

Version:

A lightweight ethereum javascript wallet.

430 lines (329 loc) 12.8 kB
const CryptoJS = require('crypto-js'); const Util = require('ethereumjs-util'); const EC = require('elliptic').ec; const BitCore = require('bitcore-lib'); const Random = BitCore.crypto.Random; const Hash = BitCore.crypto.Hash; const Mnemonic = require('bitcore-mnemonic'); const Nacl = require('tweetnacl'); const NaclUtil = require('tweetnacl-util'); const ScryptAsync = require('scrypt-async'); const Assert = require('./assert'); const Encryption = require('./encryption'); const Signing = require('./signing'); const TxUtils = require('./txutils'); const ec = new EC('secp256k1'); function leftPadString(stringToPad, padChar, length) { let repeatedPadChar = ''; for (let i = 0; i < length; i++) { repeatedPadChar += padChar; } return ((repeatedPadChar + stringToPad).slice(-length)); } const KeyStore = function () { }; KeyStore.prototype.init = function (mnemonic, pwDerivedKey, hdPathString, salt) { this.salt = salt; this.hdPathString = hdPathString; this.encSeed = undefined; this.encHdRootPriv = undefined; this.version = 3; this.hdIndex = 0; this.encPrivKeys = {}; this.addresses = []; if ((typeof pwDerivedKey !== 'undefined') && (typeof mnemonic !== 'undefined')) { const words = mnemonic.split(' '); if (!KeyStore.isSeedValid(mnemonic) || words.length !== 12) { throw new Error('KeyStore: Invalid mnemonic'); } // Pad the seed to length 120 before encrypting const paddedSeed = leftPadString(mnemonic, ' ', 120); this.encSeed = KeyStore._encryptString(paddedSeed, pwDerivedKey); // hdRoot is the relative root from which we derive the keys using generateNewAddress(). // The derived keys are then `hdRoot/hdIndex`. const hdRoot = new Mnemonic(mnemonic).toHDPrivateKey().xprivkey; const hdRootKey = new BitCore.HDPrivateKey(hdRoot); const hdPathKey = hdRootKey.derive(hdPathString).xprivkey; this.encHdRootPriv = KeyStore._encryptString(hdPathKey, pwDerivedKey); } }; KeyStore.prototype.isDerivedKeyCorrect = function (pwDerivedKey) { const paddedSeed = KeyStore._decryptString(this.encSeed, pwDerivedKey); return paddedSeed && paddedSeed.length > 0; }; KeyStore.prototype.serialize = function () { return JSON.stringify({ encSeed: this.encSeed, encHdRootPriv: this.encHdRootPriv, addresses: this.addresses, encPrivKeys: this.encPrivKeys, hdPathString: this.hdPathString, salt: this.salt, hdIndex: this.hdIndex, version: this.version, }); }; KeyStore.prototype.getAddresses = function () { return this.addresses.map((addr) => Util.addHexPrefix(addr)); }; KeyStore.prototype.getSeed = function (pwDerivedKey) { Assert.derivedKey(this, pwDerivedKey); const paddedSeed = KeyStore._decryptString(this.encSeed, pwDerivedKey); if (!paddedSeed || paddedSeed.length === 0) { throw new Error('Provided password derived key is wrong'); } return paddedSeed.trim(); }; KeyStore.prototype.exportPrivateKey = function (address, pwDerivedKey) { Assert.derivedKey(this, pwDerivedKey); const addr = Util.stripHexPrefix(address).toLowerCase(); if (this.encPrivKeys[addr] === undefined) { throw new Error('KeyStore.exportPrivateKey: Address not found in KeyStore'); } const encPrivateKey = this.encPrivKeys[addr]; return KeyStore._decryptKey(encPrivateKey, pwDerivedKey); }; KeyStore.prototype.generateNewAddress = function (pwDerivedKey, n) { Assert.derivedKey(this, pwDerivedKey); if (!this.encSeed) { throw new Error('KeyStore.generateNewAddress: No seed set'); } n = n || 1; const keys = this._generatePrivKeys(pwDerivedKey, n); for (let i = 0; i < n; i++) { const keyObj = keys[i]; const address = KeyStore._computeAddressFromPrivKey(keyObj.privKey); this.encPrivKeys[address] = keyObj.encPrivKey; this.addresses.push(address); } }; KeyStore.prototype.keyFromPassword = function (password, callback) { KeyStore.deriveKeyFromPasswordAndSalt(password, this.salt, callback); }; KeyStore.prototype.passwordProvider = function (callback) { const password = prompt('Enter password to continue', 'Enter password'); callback(null, password); }; KeyStore.prototype.hasAddress = function (address, callback) { const addrToCheck = Util.stripHexPrefix(address); if (this.encPrivKeys[addrToCheck] === undefined) { const err = new Error('Address not found!'); callback(err, false); return; } callback(null, true); }; KeyStore.prototype.signTransaction = function (txParams, callback) { const { gas, ...params } = txParams; const txObj = { ...params, gasLimit: gas, }; const tx = TxUtils.createTx(txObj); const rawTx = TxUtils.txToHexString(tx); const signingAddress = Util.stripHexPrefix(txParams.from); this.passwordProvider((err, password) => { if (err) { callback(err); return; } this.keyFromPassword(password, (err, pwDerivedKey) => { if (err) { callback(err); return; } const signedTx = Signing.signTx(this, pwDerivedKey, rawTx, signingAddress); callback(null, Util.addHexPrefix(signedTx)); }); }); }; KeyStore.prototype._generatePrivKeys = function (pwDerivedKey, n) { Assert.derivedKey(this, pwDerivedKey); const hdRoot = KeyStore._decryptString(this.encHdRootPriv, pwDerivedKey); if (!hdRoot || hdRoot.length === 0) { throw new Error('Provided password derived key is wrong'); } const keys = []; for (let i = 0; i < n; i++) { const hdPrivateKey = new BitCore.HDPrivateKey(hdRoot).derive(this.hdIndex++); const privateKeyBuf = hdPrivateKey.privateKey.toBuffer(); let privateKeyHex = privateKeyBuf.toString('hex'); if (privateKeyBuf.length < 16) { // Way too small key, something must have gone wrong // Halt and catch fire throw new Error('Private key suspiciously small: < 16 bytes. Aborting!'); } else if (privateKeyBuf.length > 32) { throw new Error('Private key larger than 32 bytes. Aborting!'); } else if (privateKeyBuf.length < 32) { // Pad private key if too short // bitcore has a bug where it sometimes returns // truncated keys privateKeyHex = leftPadString(privateKeyBuf.toString('hex'), '0', 64); } const encPrivateKey = KeyStore._encryptKey(privateKeyHex, pwDerivedKey); keys[i] = { privKey: privateKeyHex, encPrivKey: encPrivateKey }; } return keys; }; KeyStore.createVault = function (opts, cb) { const { hdPathString, seedPhrase, password } = opts; let salt = opts.salt; // Default hdPathString if (!hdPathString) { const err = new Error('Keystore: Must include hdPathString in createVault inputs. Suggested alternatives are m/0\'/0\'/0\' for previous lightwallet default, or m/44\'/60\'/0\'/0 for BIP44 (used by Jaxx & MetaMask)'); return cb(err); } if (!seedPhrase) { const err = new Error('Keystore: Must include seedPhrase in createVault inputs.'); return cb(err); } if (!salt) { salt = KeyStore.generateSalt(32); } KeyStore.deriveKeyFromPasswordAndSalt(password, salt, (err, pwDerivedKey) => { if (err) { cb(err); return; } const ks = new KeyStore(); ks.init(seedPhrase, pwDerivedKey, hdPathString, salt); cb(null, ks); }); }; KeyStore.generateSalt = function (byteCount) { return BitCore.crypto.Random.getRandomBuffer(byteCount || 32).toString('base64'); }; // Generates a random seed. If the optional string extraEntropy is set, // a random set of entropy is created, then concatenated with extraEntropy // and hashed to produce the entropy that gives the seed. // Thus if extraEntropy comes from a high-entropy source (like dice) // it can give some protection from a bad RNG. // If extraEntropy is not set, the random number generator is used directly. KeyStore.generateRandomSeed = function (extraEntropy) { let seed = ''; if (extraEntropy === undefined) { seed = new Mnemonic(Mnemonic.Words.ENGLISH); } else if (typeof extraEntropy === 'string') { const entBuf = new Buffer(extraEntropy); const randBuf = Random.getRandomBuffer(256 / 8); const hashedEnt = this._concatAndSha256(randBuf, entBuf).slice(0, 128 / 8); seed = new Mnemonic(hashedEnt, Mnemonic.Words.ENGLISH); } else { throw new Error('generateRandomSeed: extraEntropy is set but not a string.'); } return seed.toString(); }; KeyStore.isSeedValid = function (seed) { return Mnemonic.isValid(seed, Mnemonic.Words.ENGLISH); }; KeyStore.deserialize = function (keystore) { const dataKS = JSON.parse(keystore); const { version, salt, encSeed, encHdRootPriv, encPrivKeys, hdIndex, hdPathString, addresses } = dataKS; if (version === undefined || version < 3) { throw new Error('Old version of serialized keystore. Please use KeyStore.upgradeOldSerialized() to convert it to the latest version.'); } const ks = new KeyStore(); ks.salt = salt; ks.hdPathString = hdPathString; ks.encSeed = encSeed; ks.encHdRootPriv = encHdRootPriv; ks.version = version; ks.hdIndex = hdIndex; ks.encPrivKeys = encPrivKeys; ks.addresses = addresses; return ks; }; KeyStore.deriveKeyFromPasswordAndSalt = function (password, salt, callback) { // Do not require salt, and default it to 'lightwalletSalt' // (for backwards compatibility) if (!callback && typeof salt === 'function') { callback = salt; salt = KeyStore.DEFAULT_SALT; } else if (!salt && typeof callback === 'function') { salt = KeyStore.DEFAULT_SALT; } const logN = 14; const r = 8; const dkLen = 32; const interruptStep = 200; const cb = function (derKey) { let err = null; let ui8arr = null; try { ui8arr = (new Uint8Array(derKey)); } catch (e) { err = e; } callback(err, ui8arr); }; ScryptAsync(password, salt, logN, r, dkLen, interruptStep, cb, null); }; KeyStore._encryptString = function (string, pwDerivedKey) { const nonce = Nacl.randomBytes(Nacl.secretbox.nonceLength); const encStr = Nacl.secretbox(NaclUtil.decodeUTF8(string), nonce, pwDerivedKey); return { encStr: NaclUtil.encodeBase64(encStr), nonce: NaclUtil.encodeBase64(nonce), }; }; KeyStore._decryptString = function (encryptedStr, pwDerivedKey) { const decStr = NaclUtil.decodeBase64(encryptedStr.encStr); const nonce = NaclUtil.decodeBase64(encryptedStr.nonce); const decryptedStr = Nacl.secretbox.open(decStr, nonce, pwDerivedKey); if (decryptedStr === null) { return false; } return NaclUtil.encodeUTF8(decryptedStr); }; KeyStore._encryptKey = function (privateKey, pwDerivedKey) { const nonce = Nacl.randomBytes(Nacl.secretbox.nonceLength); const privateKeyArray = Encryption.decodeHex(privateKey); const encKey = Nacl.secretbox(privateKeyArray, nonce, pwDerivedKey); return { key: NaclUtil.encodeBase64(encKey), nonce: NaclUtil.encodeBase64(nonce), }; }; KeyStore._decryptKey = function (encryptedKey, pwDerivedKey) { const decKey = NaclUtil.decodeBase64(encryptedKey.key); const nonce = NaclUtil.decodeBase64(encryptedKey.nonce); const decryptedKey = Nacl.secretbox.open(decKey, nonce, pwDerivedKey); if (decryptedKey === null) { throw new Error('Decryption failed!'); } return Encryption.encodeHex(decryptedKey); }; KeyStore._computeAddressFromPrivKey = function (privateKey) { const keyPair = ec.genKeyPair(); keyPair._importPrivate(privateKey, 'hex'); const pubKey = keyPair.getPublic(false, 'hex').slice(2); const pubKeyWordArray = CryptoJS.enc.Hex.parse(pubKey); const hash = CryptoJS.SHA3(pubKeyWordArray, { outputLength: 256 }); const address = hash.toString(CryptoJS.enc.Hex).slice(24); return address; }; KeyStore._computePubkeyFromPrivKey = function (privKey, curve) { if (curve !== 'curve25519') { throw new Error('KeyStore._computePubkeyFromPrivKey: Only "curve25519" supported.'); } const privateKeyBase64 = (new Buffer(privKey, 'hex')).toString('base64'); const privateKeyUInt8Array = NaclUtil.decodeBase64(privateKeyBase64); const pubKey = Nacl.box.keyPair.fromSecretKey(privateKeyUInt8Array).publicKey; const pubKeyBase64 = NaclUtil.encodeBase64(pubKey); const pubKeyHex = (new Buffer(pubKeyBase64, 'base64')).toString('hex'); return pubKeyHex; }; // This function is tested using the test vectors here: // http://www.di-mgt.com.au/sha_testvectors.html KeyStore._concatAndSha256 = function (entropyBuf0, entropyBuf1) { const totalEnt = Buffer.concat([entropyBuf0, entropyBuf1]); if (totalEnt.length !== entropyBuf0.length + entropyBuf1.length) { throw new Error('generateRandomSeed: Logic error! Concatenation of entropy sources failed.'); } return Hash.sha256(totalEnt); }; KeyStore.DEFAULT_SALT = 'lightwalletSalt'; module.exports = KeyStore;