UNPKG

@ducatus/ducatus-wallet-client-rev

Version:

Client for @ducatus/ducatus-wallet-service-rev

421 lines (346 loc) 11.6 kB
'use strict'; var $ = require('preconditions').singleton(); import * as _ from 'lodash'; import { Constants, Utils } from './common'; import { Credentials } from './credentials'; import { BitcoreLib, Deriver, Transactions } from '@ducatus/ducatus-crypto-wallet-core-rev'; var Bitcore = BitcoreLib; var Mnemonic = require('bitcore-mnemonic'); var sjcl = require('sjcl'); var log = require('./log'); const async = require('async'); const Uuid = require('uuid'); var Errors = require('./errors'); const wordsForLang: any = { en: Mnemonic.Words.ENGLISH, es: Mnemonic.Words.SPANISH, ja: Mnemonic.Words.JAPANESE, zh: Mnemonic.Words.CHINESE, fr: Mnemonic.Words.FRENCH, it: Mnemonic.Words.ITALIAN }; // we always set 'livenet' for xprivs. it has not consecuences // other than the serialization const NETWORK: string = 'livenet'; export class Key { version: number; use0forBCH: boolean; use44forMultisig: boolean; compliantDerivation: boolean; id: any; static FIELDS = [ 'xPrivKey', // obsolte 'xPrivKeyEncrypted', // obsolte 'mnemonic', 'mnemonicEncrypted', 'mnemonicHasPassphrase', 'fingerPrint', // BIP32 32bit fingerprint 'compliantDerivation', 'BIP45', // data for derived credentials. 'use0forBCH', // use the 0 coin' path element in BCH (legacy) 'use44forMultisig', // use the purpose 44' for multisig wallts (legacy) 'version', 'id' ]; constructor() { this.version = 1; this.use0forBCH = false; this.use44forMultisig = false; this.compliantDerivation = true; this.id = Uuid.v4(); } static match(a, b) { return a.id == b.id; } static create = function(opts) { opts = opts || {}; if (opts.language && !wordsForLang[opts.language]) throw new Error('Unsupported language'); var m = new Mnemonic(wordsForLang[opts.language]); while (!Mnemonic.isValid(m.toString())) { m = new Mnemonic(wordsForLang[opts.language]); } let x: any = new Key(); let xpriv = m.toHDPrivateKey(opts.passphrase, NETWORK); x.xPrivKey = xpriv.toString(); x.fingerPrint = xpriv.fingerPrint.toString('hex'); x.mnemonic = m.phrase; x.mnemonicHasPassphrase = !!opts.passphrase; // bug backwards compatibility flags x.use0forBCH = opts.useLegacyCoinType; x.use44forMultisig = opts.useLegacyPurpose; x.compliantDerivation = !opts.nonCompliantDerivation; return x; }; static fromMnemonic = function(words, opts) { $.checkArgument(words); if (opts) $.shouldBeObject(opts); opts = opts || {}; var m = new Mnemonic(words); var x: any = new Key(); let xpriv = m.toHDPrivateKey(opts.passphrase, NETWORK); x.xPrivKey = xpriv.toString(); x.fingerPrint = xpriv.fingerPrint.toString('hex'); x.mnemonic = words; x.mnemonicHasPassphrase = !!opts.passphrase; x.use0forBCH = opts.useLegacyCoinType; x.use44forMultisig = opts.useLegacyPurpose; x.compliantDerivation = !opts.nonCompliantDerivation; return x; }; static fromExtendedPrivateKey = function(xPriv, opts) { $.checkArgument(xPriv); opts = opts || {}; let xpriv; try { xpriv = new Bitcore.HDPrivateKey(xPriv); } catch (e) { throw new Error('Invalid argument'); } var x: any = new Key(); x.xPrivKey = xpriv.toString(); x.fingerPrint = xpriv.fingerPrint.toString('hex'); x.mnemonic = null; x.mnemonicHasPassphrase = null; x.use44forMultisig = opts.useLegacyPurpose; x.use0forBCH = opts.useLegacyCoinType; x.compliantDerivation = !opts.nonCompliantDerivation; return x; }; static fromObj = function(obj) { $.shouldBeObject(obj); var x: any = new Key(); if (obj.version != x.version) { throw new Error('Bad Key version'); } _.each(Key.FIELDS, function(k) { x[k] = obj[k]; }); $.checkState(x.xPrivKey || x.xPrivKeyEncrypted, 'invalid input'); return x; }; toObj = function() { var self = this; var x = {}; _.each(Key.FIELDS, function(k) { x[k] = self[k]; }); return x; }; isPrivKeyEncrypted = function() { return !!this.xPrivKeyEncrypted && !this.xPrivKey; }; checkPassword = function(password) { if (this.isPrivKeyEncrypted()) { try { sjcl.decrypt(password, this.xPrivKeyEncrypted); } catch (ex) { return false; } return true; } return null; }; get = function(password) { var keys: any = {}; let fingerPrintUpdated = false; if (this.isPrivKeyEncrypted()) { $.checkArgument(password, 'Private keys are encrypted, a password is needed'); try { keys.xPrivKey = sjcl.decrypt(password, this.xPrivKeyEncrypted); // update fingerPrint if not set. if (!this.fingerPrint) { let xpriv = new Bitcore.HDPrivateKey(keys.xPrivKey); this.fingerPrint = xpriv.fingerPrint.toString('hex'); fingerPrintUpdated = true; } 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; if (fingerPrintUpdated) { keys.fingerPrintUpdated = true; } } return keys; }; encrypt = 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; }; decrypt = 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) { log.error('error decrypting:', ex); throw new Error('Could not decrypt'); } }; derive = function(password, path) { $.checkArgument(path, 'no path at derive()'); var xPrivKey = new Bitcore.HDPrivateKey(this.get(password).xPrivKey, NETWORK); var deriveFn = this.compliantDerivation ? _.bind(xPrivKey.deriveChild, xPrivKey) : _.bind(xPrivKey.deriveNonCompliantChild, xPrivKey); return deriveFn(path); }; _checkCoin(coin) { if (!_.includes(Constants.COINS, coin)) throw new Error('Invalid coin'); } _checkNetwork(network) { if (!_.includes(['livenet', 'testnet'], network)) throw new Error('Invalid network'); } /* * This is only used on "create" * no need to include/support * BIP45 */ getBaseAddressDerivationPath(opts) { $.checkArgument(opts, 'Need to provide options'); $.checkArgument(opts.n >= 1, 'n need to be >=1'); let purpose = opts.n == 1 || this.use44forMultisig ? '44' : '48'; var coinCode = '0'; if (opts.network == 'testnet' && Constants.UTXO_COINS.includes(opts.coin)) { coinCode = '1'; } else if (opts.coin == 'bch') { if (this.use0forBCH) { coinCode = '1025'; } else { coinCode = '145'; } } else if (opts.coin == 'btc') { coinCode = '1025'; } else if (opts.coin == 'duc') { coinCode = '0'; } else if (opts.coin == 'eth') { coinCode = '60'; } else if (opts.coin == 'xrp') { coinCode = '144'; } else if (opts.coin == 'ducx') { coinCode = '1060'; } else { throw new Error('unknown coin: ' + opts.coin); } return 'm/' + purpose + "'/" + coinCode + "'/" + opts.account + "'"; } /* * opts.coin * opts.network * opts.account * opts.n */ createCredentials = function(password, opts) { opts = opts || {}; if (password) $.shouldBeString(password, 'provide password'); this._checkCoin(opts.coin); this._checkNetwork(opts.network); $.shouldBeNumber(opts.account, 'Invalid account'); $.shouldBeNumber(opts.n, 'Invalid n'); $.shouldBeUndefined(opts.useLegacyCoinType); $.shouldBeUndefined(opts.useLegacyPurpose); let path = this.getBaseAddressDerivationPath(opts); let xPrivKey = this.derive(password, path); let requestPrivKey = this.derive(password, Constants.PATHS.REQUEST_KEY).privateKey.toString(); if (opts.network == 'testnet') { // Hacky: BTC/BCH xPriv depends on network: This code is to // convert a livenet xPriv to a testnet xPriv let x = xPrivKey.toObject(); x.network = 'testnet'; delete x.xprivkey; delete x.checksum; x.privateKey = _.padStart(x.privateKey, 64, '0'); xPrivKey = new Bitcore.HDPrivateKey(x); } return Credentials.fromDerivedKey({ xPubKey: xPrivKey.hdPublicKey.toString(), coin: opts.coin, network: opts.network, account: opts.account, n: opts.n, rootPath: path, keyId: this.id, requestPrivKey, addressType: opts.addressType, walletPrivKey: opts.walletPrivKey }); }; /* * opts * opts.path * opts.requestPrivKey */ createAccess = function(password, opts) { opts = opts || {}; $.shouldBeString(opts.path); var requestPrivKey = new Bitcore.PrivateKey(opts.requestPrivKey || null); var requestPubKey = requestPrivKey.toPublicKey().toString(); var xPriv = this.derive(password, opts.path); var signature = Utils.signRequestPubKey(requestPubKey, xPriv); requestPrivKey = requestPrivKey.toString(); return { signature, requestPrivKey }; }; sign = function(rootPath, txp, password, cb) { $.shouldBeString(rootPath); if (this.isPrivKeyEncrypted() && !password) { return cb(new Errors.ENCRYPTED_PRIVATE_KEY()); } var privs = []; var derived: any = {}; var derived = this.derive(password, rootPath); var xpriv = new Bitcore.HDPrivateKey(derived); var t = Utils.buildTx(txp); if (Constants.UTXO_COINS.includes(txp.coin)) { _.each(txp.inputs, function(i) { $.checkState(i.path, 'Input derivation path not available (signing transaction)'); if (!derived[i.path]) { derived[i.path] = xpriv.deriveChild(i.path).privateKey; privs.push(derived[i.path]); } }); var signatures = _.map(privs, function(priv, i) { return t.getSignatures(priv); }); signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { return s.signature.toDER().toString('hex'); }); return signatures; } else { let tx = t.uncheckedSerialize(); tx = typeof tx === 'string' ? [tx] : tx; const chain = Utils.getChain(txp.coin); const txArray = _.isArray(tx) ? tx : [tx]; const isChange = false; const addressIndex = 0; const { privKey, pubKey } = Deriver.derivePrivateKey(chain, txp.network, derived, addressIndex, isChange); let signatures = []; for (const rawTx of txArray) { const signed = Transactions.getSignature({ chain, tx: rawTx, key: { privKey, pubKey } }); signatures.push(signed); } return signatures; } }; }