happyucjs-lightwallet
Version:
A lightweight happyuc javascript wallet.
503 lines (388 loc) • 14.8 kB
JavaScript
const CryptoJS = require('crypto-js');
const Transaction = require('happyucjs-tx');
const EC = require('elliptic').ec;
const ec = new EC('secp256k1');
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 scrypt = require('scrypt-async');
// const encryption = require('./encryption');
const 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) {
let repreatedPadChar = '';
for (let i=0; i<length; i++) {
repreatedPadChar += padChar;
}
return ( (repreatedPadChar + stringToPad).slice(-length) );
}
function nacl_encodeHex(msgUInt8Arr) {
let msgBase64 = nacl.util.encodeBase64(msgUInt8Arr);
return (new Buffer(msgBase64, 'base64')).toString('hex');
}
function nacl_decodeHex(msgHex) {
let msgBase64 = (new Buffer(msgHex, 'hex')).toString('base64');
return nacl.util.decodeBase64(msgBase64);
}
function KeyStore(){}
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') ){
let 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
let paddedSeed = leftPadString(mnemonic, ' ', 120);
this.encSeed = encryptString(paddedSeed, pwDerivedKey);
// hdRoot is the relative root from which we derive the keys using
// generateNewAddress(). The derived keys are then
// `hdRoot/hdIndex`.
let hdRoot = new Mnemonic(mnemonic).toHDPrivateKey().xprivkey;
let hdRootKey = new bitcore.HDPrivateKey(hdRoot);
let hdPathKey = hdRootKey.derive(hdPathString).xprivkey;
this.encHdRootPriv = encryptString(hdPathKey, pwDerivedKey);
}
}
KeyStore.createVault = function(opts, cb) {
// Default hdPathString
if (!('hdPathString' in opts)) {
let 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' in opts)) {
let err = new Error('Keystore: Must include seedPhrase in createVault inputs.')
return cb(err)
}
if (!('salt' in opts)) {
opts.salt = generateSalt(32);
}
KeyStore.deriveKeyFromPasswordAndSalt(opts.password, opts.salt, function(err, pwDerivedKey) {
if (err) return cb(err);
let ks = new KeyStore();
ks.init(opts.seedPhrase, pwDerivedKey, opts.hdPathString, opts.salt);
cb(null, ks);
});
};
KeyStore.generateSalt = generateSalt;
function generateSalt (byteCount) {
return bitcore.crypto.Random.getRandomBuffer(byteCount || 32).toString('base64');
}
KeyStore.prototype.isDerivedKeyCorrect = function(pwDerivedKey) {
let paddedSeed = KeyStore._decryptString(this.encSeed, pwDerivedKey);
if (paddedSeed.length > 0) {
return true;
}
return false;
};
function encryptString (string, pwDerivedKey) {
let nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
let encObj = nacl.secretbox(nacl.util.decodeUTF8(string), nonce, pwDerivedKey);
let encString = { 'encStr': nacl.util.encodeBase64(encObj),
'nonce': nacl.util.encodeBase64(nonce)};
return encString;
};
KeyStore._encryptString = encryptString
KeyStore._decryptString = function (encryptedStr, pwDerivedKey) {
let secretbox = nacl.util.decodeBase64(encryptedStr.encStr);
let nonce = nacl.util.decodeBase64(encryptedStr.nonce);
let decryptedStr = nacl.secretbox.open(secretbox, nonce, pwDerivedKey);
if (decryptedStr === undefined) {
throw new Error("Decryption failed!");
}
return nacl.util.encodeUTF8(decryptedStr);
};
KeyStore._encryptKey = function (privKey, pwDerivedKey) {
let privKeyArray = nacl_decodeHex(privKey);
let nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
let encKey = nacl.secretbox(privKeyArray, nonce, pwDerivedKey);
encKey = { 'key': nacl.util.encodeBase64(encKey), 'nonce': nacl.util.encodeBase64(nonce)};
return encKey;
};
KeyStore._decryptKey = function (encryptedKey, pwDerivedKey) {
let secretbox = nacl.util.decodeBase64(encryptedKey.key);
let nonce = nacl.util.decodeBase64(encryptedKey.nonce);
let decryptedKey = nacl.secretbox.open(secretbox, nonce, pwDerivedKey);
if (decryptedKey === undefined) {
throw new Error("Decryption failed!");
}
return nacl_encodeHex(decryptedKey);
};
KeyStore._computeAddressFromPrivKey = function (privKey) {
let keyPair = ec.genKeyPair();
keyPair._importPrivate(privKey, 'hex');
let compact = false;
let pubKey = keyPair.getPublic(compact, 'hex').slice(2);
let pubKeyWordArray = CryptoJS.enc.Hex.parse(pubKey);
let hash = CryptoJS.SHA3(pubKeyWordArray, { outputLength: 256 });
let 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.')
}
let privKeyBase64 = (new Buffer(privKey, 'hex')).toString('base64')
let privKeyUInt8Array = nacl.util.decodeBase64(privKeyBase64);
let pubKey = nacl.box.keyPair.fromSecretKey(privKeyUInt8Array).publicKey;
let pubKeyBase64 = nacl.util.encodeBase64(pubKey);
let pubKeyHex = (new Buffer(pubKeyBase64, 'base64')).toString('hex');
return pubKeyHex;
}
KeyStore.prototype._generatePrivKeys = function(pwDerivedKey, n) {
if(!this.isDerivedKeyCorrect(pwDerivedKey)) {
throw new Error("Incorrect derived key!");
}
let hdRoot = KeyStore._decryptString(this.encHdRootPriv, pwDerivedKey);
if (hdRoot.length === 0) {
throw new Error('Provided password derived key is wrong');
}
let keys = [];
for (let i = 0; i < n; i++){
let hdprivkey = new bitcore.HDPrivateKey(hdRoot).derive(this.hdIndex++);
let privkeyBuf = hdprivkey.privateKey.toBuffer();
let 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!');
}
let 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) {
let totalEnt = Buffer.concat([entropyBuf0, entropyBuf1]);
if (totalEnt.length !== entropyBuf0.length + entropyBuf1.length) {
throw new Error('generateRandomSeed: Logic error! Concatenation of entropy sources failed.')
}
let 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) {
let seed = '';
if (extraEntropy === undefined) {
seed = new Mnemonic(Mnemonic.Words.ENGLISH);
}
else if (typeof extraEntropy === 'string') {
let entBuf = new Buffer(extraEntropy);
let randBuf = Random.getRandomBuffer(256 / 8);
let 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)
};
// Takes keystore serialized as string and returns an instance of KeyStore
KeyStore.deserialize = function (keystore) {
let jsonKS = JSON.parse(keystore);
if (jsonKS.version === undefined || jsonKS.version !== 3) {
throw new Error('Old version of serialized keystore. Please use KeyStore.upgradeOldSerialized() to convert it to the latest version.')
}
// Create keystore
let keystoreX = new KeyStore();
keystoreX.salt = jsonKS.salt
keystoreX.hdPathString = jsonKS.hdPathString
keystoreX.encSeed = jsonKS.encSeed;
keystoreX.encHdRootPriv = jsonKS.encHdRootPriv;
keystoreX.version = jsonKS.version;
keystoreX.hdIndex = jsonKS.hdIndex;
keystoreX.encPrivKeys = jsonKS.encPrivKeys;
keystoreX.addresses = jsonKS.addresses;
return keystoreX;
};
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 = 'lightwalletSalt'
} else if (!salt && typeof callback === 'function') {
salt = 'lightwalletSalt'
}
let logN = 14;
let r = 8;
let dkLen = 32;
let interruptStep = 200;
let cb = function(derKey) {
let err = null
let ui8arr = null
try{
ui8arr = (new Uint8Array(derKey));
} catch (e) {
err = e
}
callback(err, ui8arr);
}
scrypt(password, salt, logN, r, dkLen, interruptStep, cb, null);
}
// External API functions
KeyStore.prototype.serialize = function () {
let jsonKS = {'encSeed': this.encSeed,
'encHdRootPriv' : this.encHdRootPriv,
'addresses' : this.addresses,
'encPrivKeys' : this.encPrivKeys,
'hdPathString' : this.hdPathString,
'salt': this.salt,
'hdIndex' : this.hdIndex,
'version' : this.version};
return JSON.stringify(jsonKS);
};
KeyStore.prototype.getAddresses = function () {
let prefixedAddresses = this.addresses.map(function (addr) {
return add0x(addr)
});
return prefixedAddresses;
};
KeyStore.prototype.getSeed = function (pwDerivedKey) {
if(!this.isDerivedKeyCorrect(pwDerivedKey)) {
throw new Error("Incorrect derived key!");
}
let paddedSeed = KeyStore._decryptString(this.encSeed, pwDerivedKey);
return paddedSeed.trim();
};
KeyStore.prototype.exportPrivateKey = function (address0, pwDerivedKey) {
if(!this.isDerivedKeyCorrect(pwDerivedKey)) {
throw new Error("Incorrect derived key!");
}
let address = strip0x(address0).toLowerCase();
if (this.encPrivKeys[address] === undefined) {
throw new Error('KeyStore.exportPrivateKey: Address not found in KeyStore');
}
let encPrivKey = this.encPrivKeys[address];
let privKey = KeyStore._decryptKey(encPrivKey, pwDerivedKey);
return privKey;
};
KeyStore.prototype.generateNewAddress = function(pwDerivedKey, n) {
if(!this.isDerivedKeyCorrect(pwDerivedKey)) {
throw new Error("Incorrect derived key!");
}
if (!this.encSeed) {
throw new Error('KeyStore.generateNewAddress: No seed set');
}
n = n || 1;
let keys = this._generatePrivKeys(pwDerivedKey, n);
for (let i = 0; i < n; i++) {
let keyObj = keys[i];
let 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);
}
// Async functions exposed for Hooked Webu-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 webu-provider is encouraged
// to write their own passwordProvider.
//
// Uses defaultHdPathString for the addresses.
KeyStore.prototype.passwordProvider = function (callback)
{
let password = prompt("Enter password to continue","Enter password");
callback(null, password);
}
KeyStore.prototype.hasAddress = function (address, callback) {
let addrToCheck = strip0x(address);
if (this.encPrivKeys[addrToCheck] === undefined) {
callback('Address not found!', false);
}
else {
callback(null, true);
}
};
KeyStore.prototype.signTransaction = function (txParams, callback) {
let hucjsTxParams = {};
hucjsTxParams.from = add0x(txParams.from);
hucjsTxParams.to = add0x(txParams.to);
hucjsTxParams.gasLimit = add0x(txParams.gas);
hucjsTxParams.gasPrice = add0x(txParams.gasPrice);
hucjsTxParams.nonce = add0x(txParams.nonce);
hucjsTxParams.value = add0x(txParams.value);
hucjsTxParams.data = add0x(txParams.data);
let txObj = new Transaction(hucjsTxParams);
let rawTx = txObj.serialize().toString('hex');
let signingAddress = strip0x(txParams.from);
let salt = this.salt;
let self = this;
this.passwordProvider( function (err, password, salt) {
if (err) return callback(err);
if (!salt) {
salt = self.salt
}
self.keyFromPassword(password, function (err, pwDerivedKey) {
if (err) return callback(err);
let signedTx = signing.signTx(self, pwDerivedKey, rawTx, signingAddress, self.defaultHdPathString);
callback(null, '0x' + signedTx);
})
})
};
module.exports = KeyStore;