moac-lightwallets
Version:
A lightweight moac javascript wallet.
529 lines (397 loc) • 16.6 kB
JavaScript
var CryptoJS = require('crypto-js');
var Transaction = require('ethereumjs-tx');
var EC = require('elliptic').ec;
var ec = new EC('secp256k1');
var bitcore = require('bitcore-lib');
var Random = bitcore.crypto.Random;
var Hash = bitcore.crypto.Hash;
var Mnemonic = require('bitcore-mnemonic');
var nacl = require('tweetnacl');
var scrypt = require('scrypt-async');
var encryption = require('./encryption');
var signing = require('./signing');
function strip0x (input) {
if (typeof(input) !== 'string') {
return input;
}
else if (input.length >= 2 && input.slice(0,2) === '0x') {
return input.slice(2);
}
else {
return input;
}
}
function add0x (input) {
if (typeof(input) !== 'string') {
return input;
}
else if (input.length < 2 || input.slice(0,2) !== '0x') {
return '0x' + input;
}
else {
return input;
}
}
function leftPadString (stringToPad, padChar, length) {
var repreatedPadChar = '';
for (var i=0; i<length; i++) {
repreatedPadChar += padChar;
}
return ( (repreatedPadChar + stringToPad).slice(-length) );
}
function nacl_encodeHex(msgUInt8Arr) {
var msgBase64 = nacl.util.encodeBase64(msgUInt8Arr);
return (new Buffer(msgBase64, 'base64')).toString('hex');
}
function nacl_decodeHex(msgHex) {
var msgBase64 = (new Buffer(msgHex, 'hex')).toString('base64');
return nacl.util.decodeBase64(msgBase64);
}
var KeyStore = function(mnemonic, pwDerivedKey, hdPathString) {
this.defaultHdPathString = "m/0'/0'/0'";
if (hdPathString === undefined) {
hdPathString = this.defaultHdPathString;
}
this.ksData = {};
this.ksData[hdPathString] = {};
pathKsData = this.ksData[hdPathString];
pathKsData.info = {curve: 'secp256k1', purpose: 'sign'};
this.encSeed = undefined;
this.encHdRootPriv = undefined;
this.version = 2;
pathKsData.encHdPathPriv = undefined;
pathKsData.hdIndex = 0;
pathKsData.encPrivKeys = {};
pathKsData.addresses = [];
if ( (typeof pwDerivedKey !== 'undefined') && (typeof mnemonic !== 'undefined') ){
var words = mnemonic.split(' ');
if (!Mnemonic.isValid(mnemonic, Mnemonic.Words.ENGLISH) || words.length !== 12){
throw new Error('KeyStore: Invalid mnemonic');
}
// Pad the seed to length 120 before encrypting
var 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`.
//
// Default hdRoot is m/0'/0'/0', the overall logic is
// m/0'/Persona'/Purpose', where the 0' purpose is
// for standard Ethereum accounts.
var hdRoot = new Mnemonic(mnemonic).toHDPrivateKey().xprivkey;
this.encHdRootPriv = KeyStore._encryptString(hdRoot, pwDerivedKey);
var hdRootKey = new bitcore.HDPrivateKey(hdRoot);
var hdPath = hdRootKey.derive(hdPathString).xprivkey;
pathKsData.encHdPathPriv = KeyStore._encryptString(hdPath, pwDerivedKey);
}
};
KeyStore._encryptString = function (string, pwDerivedKey) {
var nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
var encObj = nacl.secretbox(nacl.util.decodeUTF8(string), nonce, pwDerivedKey);
var encString = { 'encStr': nacl.util.encodeBase64(encObj),
'nonce': nacl.util.encodeBase64(nonce)};
return encString;
};
KeyStore._decryptString = function (encryptedStr, pwDerivedKey) {
var secretbox = nacl.util.decodeBase64(encryptedStr.encStr);
var nonce = nacl.util.decodeBase64(encryptedStr.nonce);
var decryptedStr = nacl.secretbox.open(secretbox, nonce, pwDerivedKey);
return nacl.util.encodeUTF8(decryptedStr);
};
KeyStore._encryptKey = function (privKey, pwDerivedKey) {
var privKeyArray = nacl_decodeHex(privKey);
var nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
var encKey = nacl.secretbox(privKeyArray, nonce, pwDerivedKey);
encKey = { 'key': nacl.util.encodeBase64(encKey), 'nonce': nacl.util.encodeBase64(nonce)};
return encKey;
};
KeyStore._decryptKey = function (encryptedKey, pwDerivedKey) {
var secretbox = nacl.util.decodeBase64(encryptedKey.key);
var nonce = nacl.util.decodeBase64(encryptedKey.nonce);
var decryptedKey = nacl.secretbox.open(secretbox, nonce, pwDerivedKey);
return nacl_encodeHex(decryptedKey);
};
KeyStore._computeAddressFromPrivKey = function (privKey) {
var keyPair = ec.genKeyPair();
keyPair._importPrivate(privKey, 'hex');
var compact = false;
var pubKey = keyPair.getPublic(compact, 'hex').slice(2);
var pubKeyWordArray = CryptoJS.enc.Hex.parse(pubKey);
var hash = CryptoJS.SHA3(pubKeyWordArray, { outputLength: 256 });
var 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.')
}
var privKeyBase64 = (new Buffer(privKey, 'hex')).toString('base64')
var privKeyUInt8Array = nacl.util.decodeBase64(privKeyBase64);
var pubKey = nacl.box.keyPair.fromSecretKey(privKeyUInt8Array).publicKey;
var pubKeyBase64 = nacl.util.encodeBase64(pubKey);
var pubKeyHex = (new Buffer(pubKeyBase64, 'base64')).toString('hex');
return pubKeyHex;
}
KeyStore.prototype.addHdDerivationPath = function (hdPathString, pwDerivedKey, info) {
if (info.purpose !== 'sign' && info.purpose !== 'asymEncrypt') {
throw new Error("KeyStore.addHdDerivationPath: info.purpose is '" + info.purpose + "' but must be either 'sign' or 'asymEncrypt'.");
}
if (info.curve !== 'secp256k1' && info.curve !== 'curve25519') {
throw new Error("KeyStore.addHdDerivationPath: info.curve is '" + info.curve + "' but must be either 'secp256k1' or 'curve25519'.");
}
var hdRoot = KeyStore._decryptString(this.encHdRootPriv, pwDerivedKey);
var hdRootKey = new bitcore.HDPrivateKey(hdRoot);
var hdPath = hdRootKey.derive(hdPathString).xprivkey;
this.ksData[hdPathString] = {};
this.ksData[hdPathString].info = info;
this.ksData[hdPathString].encHdPathPriv = KeyStore._encryptString(hdPath, pwDerivedKey);
this.ksData[hdPathString].hdIndex = 0;
this.ksData[hdPathString].encPrivKeys = {};
if (info.purpose === 'sign') {
this.ksData[hdPathString].addresses = [];
}
else if (info.purpose === 'asymEncrypt') {
this.ksData[hdPathString].pubKeys = [];
}
}
KeyStore.prototype.setDefaultHdDerivationPath = function (hdPathString) {
if (this.ksData[hdPathString] === undefined) {
throw new Error('setDefaultHdDerivationPath: HD path does not exist. Cannot set default.');
}
this.defaultHdPathString = hdPathString;
}
KeyStore.prototype._generatePrivKeys = function(pwDerivedKey, n, hdPathString) {
if (hdPathString === undefined) {
hdPathString = this.defaultHdPathString;
}
var hdRoot = KeyStore._decryptString(this.ksData[hdPathString].encHdPathPriv, pwDerivedKey);
var keys = [];
for (var i = 0; i < n; i++){
var hdprivkey = new bitcore.HDPrivateKey(hdRoot).derive(this.ksData[hdPathString].hdIndex++);
var privkeyBuf = hdprivkey.privateKey.toBuffer();
var privkeyHex = privkeyBuf.toString('hex');
if (privkeyBuf.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 (privkeyBuf.length < 32) {
// Pad private key if too short
// bitcore has a bug where it sometimes returns
// truncated keys
privkeyHex = leftPadString(privkeyBuf.toString('hex'), '0', 64);
}
else if (privkeyBuf.length > 32) {
throw new Error('Private key larger than 32 bytes. Aborting!');
}
var encPrivKey = KeyStore._encryptKey(privkeyHex, pwDerivedKey);
keys[i] = {
privKey: privkeyHex,
encPrivKey: encPrivKey
}
}
return keys;
};
// This function is tested using the test vectors here:
// http://www.di-mgt.com.au/sha_testvectors.html
KeyStore._concatAndSha256 = function(entropyBuf0, entropyBuf1) {
var totalEnt = Buffer.concat([entropyBuf0, entropyBuf1]);
if (totalEnt.length !== entropyBuf0.length + entropyBuf1.length) {
throw new Error('generateRandomSeed: Logic error! Concatenation of entropy sources failed.')
}
var hashedEnt = Hash.sha256(totalEnt);
return hashedEnt;
}
// External static functions
// 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) {
var seed = '';
if (extraEntropy === undefined) {
seed = new Mnemonic(Mnemonic.Words.ENGLISH);
}
else if (typeof extraEntropy === 'string') {
var entBuf = new Buffer(extraEntropy);
var randBuf = Random.getRandomBuffer(256 / 8);
var 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.prototype.isDerivedKeyCorrect = function(pwDerivedKey) {
var paddedSeed = KeyStore._decryptString(this.encSeed, pwDerivedKey);
if (paddedSeed.length > 0) {
return true;
}
return false;
};
// Takes keystore serialized as string and returns an instance of KeyStore
KeyStore.deserialize = function (keystore) {
var jsonKS = JSON.parse(keystore);
if (jsonKS.version === undefined || jsonKS.version === 1) {
throw new Error('Old version of serialized keystore. Please use KeyStore.upgradeOldSerialized() to convert it to the latest version.')
}
// Create keystore
var keystoreX = new KeyStore();
keystoreX.encSeed = jsonKS.encSeed;
keystoreX.encHdRootPriv = jsonKS.encHdRootPriv;
keystoreX.ksData = jsonKS.ksData;
return keystoreX;
};
// External API functions
KeyStore.prototype.serialize = function () {
var jsonKS = {'encSeed': this.encSeed,
'ksData' : this.ksData,
'encHdRootPriv' : this.encHdRootPriv,
'version' : this.version};
return JSON.stringify(jsonKS);
};
KeyStore.prototype.getAddresses = function (hdPathString) {
if (hdPathString === undefined) {
hdPathString = this.defaultHdPathString;
}
if (this.ksData[hdPathString].info.purpose !== 'sign') {
throw new Error('KeyStore.getAddresses: Addresses not defined when purpose is not "sign"');
}
return this.ksData[hdPathString].addresses;
};
KeyStore.prototype.getSeed = function (pwDerivedKey) {
var paddedSeed = KeyStore._decryptString(this.encSeed, pwDerivedKey);
return paddedSeed.trim();
};
KeyStore.prototype.exportPrivateKey = function (address, pwDerivedKey, hdPathString) {
if (hdPathString === undefined) {
hdPathString = this.defaultHdPathString;
}
var address = strip0x(address);
if (this.ksData[hdPathString].encPrivKeys[address] === undefined) {
throw new Error('KeyStore.exportPrivateKey: Address not found in KeyStore');
}
var encPrivKey = this.ksData[hdPathString].encPrivKeys[address];
var privKey = KeyStore._decryptKey(encPrivKey, pwDerivedKey);
return privKey;
};
KeyStore.prototype.generateNewAddress = function(pwDerivedKey, n, hdPathString) {
if (hdPathString === undefined) {
hdPathString = this.defaultHdPathString;
}
if (this.ksData[hdPathString].info.purpose !== 'sign') {
throw new Error('KeyStore.generateNewAddress: Address not defined when purpose is not "sign"');
}
if (!this.encSeed) {
throw new Error('KeyStore.generateNewAddress: No seed set');
}
n = n || 1;
var keys = this._generatePrivKeys(pwDerivedKey, n, hdPathString);
for (var i = 0; i < n; i++) {
var keyObj = keys[i];
var address = KeyStore._computeAddressFromPrivKey(keyObj.privKey);
this.ksData[hdPathString].encPrivKeys[address] = keyObj.encPrivKey;
this.ksData[hdPathString].addresses.push(address);
}
};
KeyStore.prototype.generateNewEncryptionKeys = function(pwDerivedKey, n, hdPathString) {
if (hdPathString === undefined) {
hdPathString = this.defaultHdPathString;
}
if (this.ksData[hdPathString].info.purpose !== 'asymEncrypt') {
throw new Error('KeyStore.generateNewEncryptionKeys: Address not defined when purpose is not "asymEncrypt"');
}
if (!this.encSeed) {
throw new Error('KeyStore.generateNewEncryptionKeys: No seed set');
}
n = n || 1;
var keys = this._generatePrivKeys(pwDerivedKey, n, hdPathString);
var curve = this.ksData[hdPathString].info.curve;
for (var i = 0; i < n; i++) {
var keyObj = keys[i];
var pubkey = KeyStore._computePubkeyFromPrivKey(keyObj.privKey, curve);
this.ksData[hdPathString].encPrivKeys[pubkey] = keyObj.encPrivKey;
this.ksData[hdPathString].pubKeys.push(pubkey);
}
};
KeyStore.prototype.getPubKeys = function (hdPathString) {
if (hdPathString === undefined) {
hdPathString = this.defaultHdPathString;
}
if (this.ksData[hdPathString].info.purpose !== 'asymEncrypt') {
throw new Error('KeyStore.getPubKeys: Not defined when purpose is not "asymEncrypt"');
}
if (this.ksData[hdPathString].pubKeys === undefined) {
throw new Error('KeyStore.getPubKeys: No pubKeys data found!');
}
return this.ksData[hdPathString].pubKeys;
}
KeyStore.deriveKeyFromPassword = function(password, callback) {
var salt = 'lightwalletSalt'; // should we have user-defined salt?
var logN = 14;
var r = 8;
var dkLen = 32;
var interruptStep = 200;
var cb = function(derKey) {
var ui8arr = (new Uint8Array(derKey));
callback(null, ui8arr);
}
scrypt(password, salt, logN, r, dkLen, interruptStep, cb, null);
}
// Async functions exposed for Hooked Web3-provider
// hasAddress(address, callback)
// signTransaction(txParams, callback)
//
// The function signTransaction() needs the
// function KeyStore.prototype.passwordProvider(callback)
// to be set in order to run properly.
// The function passwordProvider is an async function
// that calls the callback(err, password) with a password
// supplied by the user or by other means.
// The user of the hooked web3-provider is encouraged
// to write their own passwordProvider.
//
// Uses defaultHdPathString for the addresses.
KeyStore.prototype.passwordProvider = function (callback) {
var password = prompt("Enter password to continue","Enter password");
callback(null, password);
}
KeyStore.prototype.hasAddress = function (address, callback) {
var addrToCheck = strip0x(address);
if (this.ksData[this.defaultHdPathString].encPrivKeys[addrToCheck] === undefined) {
callback('Address not found!', false);
}
else {
callback(null, true);
}
};
KeyStore.prototype.signTransaction = function (txParams, callback) {
var ethjsTxParams = {};
ethjsTxParams.from = add0x(txParams.from);
ethjsTxParams.to = add0x(txParams.to);
ethjsTxParams.gasLimit = add0x(txParams.gas);
ethjsTxParams.gasPrice = add0x(txParams.gasPrice);
ethjsTxParams.nonce = add0x(txParams.nonce);
ethjsTxParams.value = add0x(txParams.value);
ethjsTxParams.data = add0x(txParams.data);
var txObj = new Transaction(ethjsTxParams);
var rawTx = txObj.serialize().toString('hex');
var signingAddress = strip0x(txParams.from);
var self = this;
this.passwordProvider( function (err, password) {
if (err) return callback(err);
KeyStore.deriveKeyFromPassword(password, function (err, pwDerivedKey) {
var signedTx = signing.signTx(self, pwDerivedKey, rawTx, signingAddress, self.defaultHdPathString);
callback(null, '0x' + signedTx);
})
})
};
module.exports = KeyStore;