bitcore-wallet-client
Version:
Client for bitcore-wallet-service
440 lines (352 loc) • 12.6 kB
JavaScript
'use strict';
var $ = require('preconditions').singleton();
var _ = require('lodash');
var Bitcore = require('bitcore-lib');
var Mnemonic = require('bitcore-mnemonic');
var sjcl = require('sjcl');
var Common = require('./common');
var Constants = Common.Constants;
var Utils = Common.Utils;
var FIELDS = [
'network',
'xPrivKey',
'xPrivKeyEncrypted',
'xPubKey',
'requestPrivKey',
'requestPubKey',
'copayerId',
'publicKeyRing',
'walletId',
'walletName',
'm',
'n',
'walletPrivKey',
'personalEncryptingKey',
'sharedEncryptingKey',
'copayerName',
'externalSource',
'mnemonic',
'mnemonicEncrypted',
'entropySource',
'mnemonicHasPassphrase',
'derivationStrategy',
'account',
'addressType',
];
function Credentials() {
this.version = '1.0.0';
this.derivationStrategy = Constants.DERIVATION_STRATEGIES.BIP44;
this.account = 0;
};
function _checkNetwork(network) {
if (!_.contains(['livenet', 'testnet'], network)) throw new Error('Invalid network');
};
Credentials.create = function(network) {
_checkNetwork(network);
var x = new Credentials();
x.network = network;
x.xPrivKey = (new Bitcore.HDPrivateKey(network)).toString();
x._expand();
return x;
};
var wordsForLang = {
'en': Mnemonic.Words.ENGLISH,
'es': Mnemonic.Words.SPANISH,
'ja': Mnemonic.Words.JAPANESE,
'zh': Mnemonic.Words.CHINESE,
'fr': Mnemonic.Words.FRENCH,
};
Credentials.createWithMnemonic = function(network, passphrase, language, account) {
_checkNetwork(network);
if (!wordsForLang[language]) throw new Error('Unsupported language');
$.shouldBeNumber(account);
var m = new Mnemonic(wordsForLang[language]);
while (!Mnemonic.isValid(m.toString())) {
m = new Mnemonic(wordsForLang[language])
};
var x = new Credentials();
x.network = network;
x.account = account;
x.xPrivKey = m.toHDPrivateKey(passphrase, network).toString();
x._expand();
x.mnemonic = m.phrase;
x.mnemonicHasPassphrase = !!passphrase;
return x;
};
Credentials.fromExtendedPrivateKey = function(xPrivKey) {
var x = new Credentials();
x.xPrivKey = xPrivKey;
x._expand();
return x;
};
// note that mnemonic / passphrase is NOT stored
Credentials.fromMnemonic = function(network, words, passphrase, account, derivationStrategy) {
_checkNetwork(network);
$.shouldBeNumber(account);
$.checkArgument(_.contains(_.values(Constants.DERIVATION_STRATEGIES), derivationStrategy));
var m = new Mnemonic(words);
var x = new Credentials();
x.xPrivKey = m.toHDPrivateKey(passphrase, network).toString();
x.mnemonicHasPassphrase = !!passphrase;
x.account = account;
x.derivationStrategy = derivationStrategy;
x._expand();
return x;
};
/*
* BWC uses
* xPrivKey -> m/44'/network'/account' -> Base Address Key
* so, xPubKey is PublicKeyHD(xPrivKey.derive("m/44'/network'/account'").
*
* For external sources, this derivation should be done before
* call fromExtendedPublicKey
*
* entropySource should be a HEX string containing pseudo-random data, that can
* be deterministically derived from the xPrivKey, and should not be derived from xPubKey
*/
Credentials.fromExtendedPublicKey = function(xPubKey, source, entropySourceHex, account, derivationStrategy) {
$.checkArgument(entropySourceHex);
$.shouldBeNumber(account);
$.checkArgument(_.contains(_.values(Constants.DERIVATION_STRATEGIES), derivationStrategy));
var entropyBuffer = new Buffer(entropySourceHex, 'hex');
//require at least 112 bits of entropy
$.checkArgument(entropyBuffer.length >= 14, 'At least 112 bits of entropy are needed')
var x = new Credentials();
x.xPubKey = xPubKey;
x.entropySource = Bitcore.crypto.Hash.sha256sha256(entropyBuffer).toString('hex');
x.account = account;
x.derivationStrategy = derivationStrategy;
x.externalSource = source;
x._expand();
return x;
};
// Get network from extended private key or extended public key
Credentials._getNetworkFromExtendedKey = function(xKey) {
$.checkArgument(xKey && _.isString(xKey));
return xKey.charAt(0) == 't' ? 'testnet' : 'livenet';
};
Credentials._xPubToCopayerId = function(xpub) {
var hash = sjcl.hash.sha256.hash(xpub);
return sjcl.codec.hex.fromBits(hash);
};
Credentials.prototype._hashFromEntropy = function(prefix, length) {
$.checkState(prefix);
var b = new Buffer(this.entropySource, 'hex');
var b2 = Bitcore.crypto.Hash.sha256hmac(b, new Buffer(prefix));
return b2.slice(0, length);
};
Credentials.prototype._expand = function() {
$.checkState(this.xPrivKey || (this.xPubKey && this.entropySource));
var network = Credentials._getNetworkFromExtendedKey(this.xPrivKey || this.xPubKey);
if (this.network) {
$.checkState(this.network == network);
} else {
this.network = network;
}
if (this.xPrivKey) {
var xPrivKey = new Bitcore.HDPrivateKey.fromString(this.xPrivKey);
// this extra derivation is not to share a non hardened xPubKey to the server.
var addressDerivation = xPrivKey.derive(this.getBaseAddressDerivationPath());
this.xPubKey = (new Bitcore.HDPublicKey(addressDerivation)).toString();
var requestDerivation = xPrivKey.derive(Constants.PATHS.REQUEST_KEY);
this.requestPrivKey = requestDerivation.privateKey.toString();
var pubKey = requestDerivation.publicKey;
this.requestPubKey = pubKey.toString();
this.entropySource = Bitcore.crypto.Hash.sha256(requestDerivation.privateKey.toBuffer()).toString('hex');
} else {
var seed = this._hashFromEntropy('reqPrivKey', 32);
var privKey = new Bitcore.PrivateKey(seed.toString('hex'), network);
this.requestPrivKey = privKey.toString();
this.requestPubKey = privKey.toPublicKey().toString();
}
this.personalEncryptingKey = this._hashFromEntropy('personalKey', 16).toString('base64');
this.copayerId = Credentials._xPubToCopayerId(this.xPubKey);
this.publicKeyRing = [{
xPubKey: this.xPubKey,
requestPubKey: this.requestPubKey,
}];
};
Credentials.fromObj = function(obj) {
var x = new Credentials();
_.each(FIELDS, function(k) {
x[k] = obj[k];
});
x.derivationStrategy = x.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
x.addressType = x.addressType || Constants.SCRIPT_TYPES.P2SH;
x.account = x.account || 0;
$.checkState(x.xPrivKey || x.xPubKey || x.xPrivKeyEncrypted, "invalid input");
return x;
};
Credentials.prototype.toObj = function() {
var self = this;
if (self.hasPrivKeyEncrypted())
self.lock();
var x = {};
_.each(FIELDS, function(k) {
x[k] = self[k];
});
return x;
};
Credentials.prototype.getBaseAddressDerivationPath = function() {
var purpose;
switch (this.derivationStrategy) {
case Constants.DERIVATION_STRATEGIES.BIP45:
return "m/45'";
case Constants.DERIVATION_STRATEGIES.BIP44:
purpose = '44';
break;
case Constants.DERIVATION_STRATEGIES.BIP48:
purpose = '48';
break;
}
var coin = (this.network == 'livenet' ? "0" : "1");
return "m/" + purpose + "'/" + coin + "'/" + this.account + "'";
};
Credentials.prototype.getDerivedXPrivKey = function() {
var path = this.getBaseAddressDerivationPath();
return new Bitcore.HDPrivateKey(this.xPrivKey, this.network).derive(path);
};
Credentials.prototype.addWalletPrivateKey = function(walletPrivKey) {
this.walletPrivKey = walletPrivKey;
this.sharedEncryptingKey = Utils.privateKeyToAESKey(walletPrivKey);
};
Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, walletPrivKey, copayerName) {
this.walletId = walletId;
this.walletName = walletName;
this.m = m;
this.n = n;
if (walletPrivKey)
this.addWalletPrivateKey(walletPrivKey);
if (copayerName)
this.copayerName = copayerName;
this.addressType = (n == 1) ? Constants.SCRIPT_TYPES.P2PKH : Constants.SCRIPT_TYPES.P2SH;
// Use m/48' for multisig hardware wallets
if (!this.xPrivKey && this.externalSource && n > 1) {
this.derivationStrategy = Constants.DERIVATION_STRATEGIES.BIP48;
}
if (n == 1) {
this.addPublicKeyRing([{
xPubKey: this.xPubKey,
requestPubKey: this.requestPubKey,
}]);
}
};
Credentials.prototype.hasWalletInfo = function() {
return !!this.walletId;
};
Credentials.prototype.isPrivKeyEncrypted = function() {
return (!!this.xPrivKeyEncrypted) && !this.xPrivKey;
};
Credentials.prototype.hasPrivKeyEncrypted = function() {
return (!!this.xPrivKeyEncrypted);
};
Credentials.prototype.setPrivateKeyEncryption = function(password, opts) {
if (this.xPrivKeyEncrypted)
throw new Error('Encrypted Privkey Already exists');
if (!this.xPrivKey)
throw new Error('No private key to encrypt');
this.xPrivKeyEncrypted = sjcl.encrypt(password, this.xPrivKey, opts);
if (!this.xPrivKeyEncrypted)
throw new Error('Could not encrypt');
if (this.mnemonic)
this.mnemonicEncrypted = sjcl.encrypt(password, this.mnemonic, opts);
};
Credentials.prototype.disablePrivateKeyEncryption = function() {
if (!this.xPrivKeyEncrypted)
throw new Error('Private Key is not encrypted');
if (!this.xPrivKey)
throw new Error('Wallet is locked, cannot disable encryption');
this.xPrivKeyEncrypted = null;
this.mnemonicEncrypted = null;
};
Credentials.prototype.lock = function() {
if (!this.xPrivKeyEncrypted)
throw new Error('Could not lock, no encrypted private key');
delete this.xPrivKey;
delete this.mnemonic;
};
Credentials.prototype.unlock = function(password) {
$.checkArgument(password);
if (this.xPrivKeyEncrypted) {
this.xPrivKey = sjcl.decrypt(password, this.xPrivKeyEncrypted);
if (this.mnemonicEncrypted) {
this.mnemonic = sjcl.decrypt(password, this.mnemonicEncrypted);
}
}
};
Credentials.prototype.addPublicKeyRing = function(publicKeyRing) {
this.publicKeyRing = _.clone(publicKeyRing);
};
Credentials.prototype.canSign = function() {
return (!!this.xPrivKey || !!this.xPrivKeyEncrypted);
};
Credentials.prototype.setNoSign = function() {
delete this.xPrivKey;
delete this.xPrivKeyEncrypted;
delete this.mnemonic;
delete this.mnemonicEncrypted;
};
Credentials.prototype.isComplete = function() {
if (!this.m || !this.n) return false;
if (!this.publicKeyRing || this.publicKeyRing.length != this.n) return false;
return true;
};
Credentials.prototype.hasExternalSource = function() {
return (typeof this.externalSource == "string");
};
Credentials.prototype.getExternalSourceName = function() {
return this.externalSource;
};
Credentials.prototype.getMnemonic = function() {
if (this.mnemonicEncrypted && !this.mnemonic) {
throw new Error('Credentials are encrypted');
}
return this.mnemonic;
};
Credentials.prototype.clearMnemonic = function() {
delete this.mnemonic;
delete this.mnemonicEncrypted;
};
Credentials.fromOldCopayWallet = function(w) {
function walletPrivKeyFromOldCopayWallet(w) {
// IN BWS, the master Pub Keys are not sent to the server,
// so it is safe to use them as seed for wallet's shared secret.
var seed = w.publicKeyRing.copayersExtPubKeys.sort().join('');
var seedBuf = new Buffer(seed);
var privKey = new Bitcore.PrivateKey.fromBuffer(Bitcore.crypto.Hash.sha256(seedBuf));
return privKey.toString();
};
var credentials = new Credentials();
credentials.derivationStrategy = Constants.DERIVATION_STRATEGIES.BIP45;
credentials.xPrivKey = w.privateKey.extendedPrivateKeyString;
credentials._expand();
credentials.addWalletInfo(w.opts.id, w.opts.name, w.opts.requiredCopayers, w.opts.totalCopayers, walletPrivKeyFromOldCopayWallet(w))
var pkr = _.map(w.publicKeyRing.copayersExtPubKeys, function(xPubStr) {
var isMe = xPubStr === credentials.xPubKey;
var requestDerivation;
if (isMe) {
var path = Constants.PATHS.REQUEST_KEY;
requestDerivation = (new Bitcore.HDPrivateKey(credentials.xPrivKey))
.derive(path).hdPublicKey;
} else {
// this
var path = Constants.PATHS.REQUEST_KEY_AUTH;
requestDerivation = (new Bitcore.HDPublicKey(xPubStr)).derive(path);
}
// Grab Copayer Name
var hd = new Bitcore.HDPublicKey(xPubStr).derive('m/2147483646/0/0');
var pubKey = hd.publicKey.toString('hex');
var copayerName = w.publicKeyRing.nicknameFor[pubKey];
if (isMe) {
credentials.copayerName = copayerName;
}
return {
xPubKey: xPubStr,
requestPubKey: requestDerivation.publicKey.toString(),
copayerName: copayerName,
};
});
credentials.addPublicKeyRing(pkr);
return credentials;
};
module.exports = Credentials;