UNPKG

bitcore-wallet-client-excc

Version:

Client for bitcore-wallet-service-excc

510 lines (413 loc) 14.7 kB
'use strict'; var $ = require('preconditions').singleton(); var _ = require('lodash'); var Bitcore = require('bitcore-lib-excc'); var Mnemonic = require('bitcore-mnemonic-excc'); var sjcl = require('sjcl'); var Common = require('./common'); var Constants = Common.Constants; var Utils = Common.Utils; var FIELDS = [ 'coin', 'network', 'xPrivKey', 'xPrivKeyEncrypted', 'xPubKey', 'requestPrivKey', 'requestPubKey', 'copayerId', 'publicKeyRing', 'walletId', 'walletName', 'm', 'n', 'walletPrivKey', 'personalEncryptingKey', 'sharedEncryptingKey', 'copayerName', 'externalSource', 'mnemonic', 'mnemonicEncrypted', 'entropySource', 'mnemonicHasPassphrase', 'derivationStrategy', 'account', 'compliantDerivation', 'addressType', 'hwInfo', 'entropySourcePath', ]; function Credentials() { this.version = '1.0.0'; this.derivationStrategy = Constants.DERIVATION_STRATEGIES.BIP44; this.account = 0; }; function _checkCoin(coin) { if (!_.includes(['excc', 'bch'], coin)) throw new Error('Invalid coin'); }; function _checkNetwork(network) { if (!_.includes(['livenet', 'testnet'], network)) throw new Error('Invalid network'); }; Credentials.create = function(coin, network) { _checkCoin(coin); _checkNetwork(network); var x = new Credentials(); x.coin = coin; x.network = network; x.xPrivKey = (new Bitcore.HDPrivateKey(network)).toString(); x.compliantDerivation = true; 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, 'it': Mnemonic.Words.ITALIAN, }; Credentials.createWithMnemonic = function(coin, network, passphrase, language, account, opts) { _checkCoin(coin); _checkNetwork(network); if (!wordsForLang[language]) throw new Error('Unsupported language'); $.shouldBeNumber(account); opts = opts || {}; var m = new Mnemonic(wordsForLang[language]); while (!Mnemonic.isValid(m.toString())) { m = new Mnemonic(wordsForLang[language]) }; var x = new Credentials(); x.coin = coin; x.network = network; x.account = account; x.xPrivKey = m.toHDPrivateKey(passphrase, network).toString(); x.compliantDerivation = true; x._expand(); x.mnemonic = m.phrase; x.mnemonicHasPassphrase = !!passphrase; return x; }; Credentials.fromExtendedPrivateKey = function(coin, xPrivKey, account, derivationStrategy, opts) { _checkCoin(coin); $.shouldBeNumber(account); $.checkArgument(_.includes(_.values(Constants.DERIVATION_STRATEGIES), derivationStrategy)); opts = opts || {}; var x = new Credentials(); x.coin = coin; x.xPrivKey = xPrivKey; x.account = account; x.derivationStrategy = derivationStrategy; x.compliantDerivation = !opts.nonCompliantDerivation; if (opts.walletPrivKey) { x.addWalletPrivateKey(opts.walletPrivKey); } x._expand(); return x; }; // note that mnemonic / passphrase is NOT stored Credentials.fromMnemonic = function(coin, network, words, passphrase, account, derivationStrategy, opts) { _checkCoin(coin); _checkNetwork(network); $.shouldBeNumber(account); $.checkArgument(_.includes(_.values(Constants.DERIVATION_STRATEGIES), derivationStrategy)); opts = opts || {}; var m = new Mnemonic(words); var x = new Credentials(); x.coin = coin; x.xPrivKey = m.toHDPrivateKey(passphrase, network).toString(); x.mnemonic = words; x.mnemonicHasPassphrase = !!passphrase; x.account = account; x.derivationStrategy = derivationStrategy; x.compliantDerivation = !opts.nonCompliantDerivation; x.entropySourcePath = opts.entropySourcePath; if (opts.walletPrivKey) { x.addWalletPrivateKey(opts.walletPrivKey); } x._expand(); return x; }; /* * BWC uses * xPrivKey -> m/44'/network'/account' -> Base Address Key * so, xPubKey is PublicKeyHD(xPrivKey.deriveChild("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(coin, xPubKey, source, entropySourceHex, account, derivationStrategy, opts) { _checkCoin(coin); $.checkArgument(entropySourceHex); $.shouldBeNumber(account); $.checkArgument(_.includes(_.values(Constants.DERIVATION_STRATEGIES), derivationStrategy)); opts = opts || {}; 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.coin = coin; x.xPubKey = xPubKey; x.entropySource = Bitcore.crypto.Hash.sha256sha256(entropyBuffer).toString('hex'); x.account = account; x.derivationStrategy = derivationStrategy; x.externalSource = source; x.compliantDerivation = true; 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.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); var deriveFn = this.compliantDerivation ? _.bind(xPrivKey.deriveChild, xPrivKey) : _.bind(xPrivKey.deriveNonCompliantChild, xPrivKey); var derivedXPrivKey = deriveFn(this.getBaseAddressDerivationPath()); // this is the xPubKey shared with the server. this.xPubKey = derivedXPrivKey.hdPublicKey.toString(); } // requests keys from mnemonics, but using a xPubkey // This is only used when importing mnemonics FROM // an hwwallet, in which xPriv was not available when // the wallet was created. if (this.entropySourcePath) { var seed = deriveFn(this.entropySourcePath).publicKey.toBuffer(); this.entropySource = Bitcore.crypto.Hash.sha256sha256(seed).toString('hex'); } if (this.entropySource) { // request keys from entropy (hw wallets) var seed = this._hashFromEntropy('reqPrivKey', 32); var privKey = new Bitcore.PrivateKey(seed.toString('hex'), network); this.requestPrivKey = privKey.toString(); this.requestPubKey = privKey.toPublicKey().toString(); } else { // request keys derived from xPriv var requestDerivation = deriveFn(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'); } this.personalEncryptingKey = this._hashFromEntropy('personalKey', 16).toString('base64'); $.checkState(this.coin); this.copayerId = Utils.xPubToCopayerId(this.coin, 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.coin = x.coin || 'excc'; 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; 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(password) { var path = this.getBaseAddressDerivationPath(); var xPrivKey = new Bitcore.HDPrivateKey(this.getKeys(password).xPrivKey, this.network); var deriveFn = !!this.compliantDerivation ? _.bind(xPrivKey.deriveChild, xPrivKey) : _.bind(xPrivKey.deriveNonCompliantChild, xPrivKey); return deriveFn(path); }; Credentials.prototype.addWalletPrivateKey = function(walletPrivKey) { this.walletPrivKey = walletPrivKey; this.sharedEncryptingKey = Utils.privateKeyToAESKey(walletPrivKey); }; Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, copayerName) { this.walletId = walletId; this.walletName = walletName; this.m = m; this.n = n; if (copayerName) this.copayerName = copayerName; if (this.derivationStrategy == 'BIP44' && n == 1) this.addressType = Constants.SCRIPT_TYPES.P2PKH; else this.addressType = 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.encryptPrivateKey = function(password, opts) { if (this.xPrivKeyEncrypted) throw new Error('Private key already encrypted'); 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); delete this.xPrivKey; delete this.mnemonic; }; Credentials.prototype.decryptPrivateKey = function(password) { if (!this.xPrivKeyEncrypted) throw new Error('Private key is not encrypted'); try { this.xPrivKey = sjcl.decrypt(password, this.xPrivKeyEncrypted); if (this.mnemonicEncrypted) { this.mnemonic = sjcl.decrypt(password, this.mnemonicEncrypted); } delete this.xPrivKeyEncrypted; delete this.mnemonicEncrypted; } catch (ex) { throw new Error('Could not decrypt'); } }; Credentials.prototype.getKeys = function(password) { var keys = {}; if (this.isPrivKeyEncrypted()) { $.checkArgument(password, 'Private keys are encrypted, a password is needed'); try { keys.xPrivKey = sjcl.decrypt(password, this.xPrivKeyEncrypted); if (this.mnemonicEncrypted) { keys.mnemonic = sjcl.decrypt(password, this.mnemonicEncrypted); } } catch (ex) { throw new Error('Could not decrypt'); } } else { keys.xPrivKey = this.xPrivKey; keys.mnemonic = this.mnemonic; } return keys; }; 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.coin = 'excc'; credentials.derivationStrategy = Constants.DERIVATION_STRATEGIES.BIP45; credentials.xPrivKey = w.privateKey.extendedPrivateKeyString; credentials._expand(); credentials.addWalletPrivateKey(walletPrivKeyFromOldCopayWallet(w)); credentials.addWalletInfo(w.opts.id, w.opts.name, w.opts.requiredCopayers, w.opts.totalCopayers) 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)) .deriveChild(path).hdPublicKey; } else { // this var path = Constants.PATHS.REQUEST_KEY_AUTH; requestDerivation = (new Bitcore.HDPublicKey(xPubStr)).deriveChild(path); } // Grab Copayer Name var hd = new Bitcore.HDPublicKey(xPubStr).deriveChild('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;