UNPKG

@bcpros/bitcore-wallet-client

Version:
580 lines (507 loc) 17.1 kB
'use strict'; import Mnemonic from '@bcpros/bitcore-mnemonic'; import { BitcoreLib as Bitcore, BitcoreLibCash, Deriver, Transactions } from '@bcpros/crypto-wallet-core'; import { singleton } from 'preconditions'; import sjcl from 'sjcl'; import 'source-map-support/register'; import Uuid from 'uuid'; import { Constants, Utils } from './common'; import { Credentials } from './credentials'; import { Errors } from './errors'; import log from './log'; const $ = singleton(); 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 no consequences // other than the serialization const NETWORK: string = 'livenet'; export class Key { #xPrivKey: string; #xPrivKeyEncrypted: string; #version: number; #mnemonic: string; #mnemonicEncrypted: string; #mnemonicHasPassphrase: boolean; public id: any; public use0forBCH: boolean; public use44forMultisig: boolean; public compliantDerivation: boolean; public BIP45: boolean; public fingerPrint: string; /* * public readonly exportFields = { * 'xPrivKey': '#xPrivKey', * 'xPrivKeyEncrypted': '#xPrivKeyEncrypted', * 'mnemonic': '#mnemonic', * 'mnemonicEncrypted': '#mnemonicEncrypted', * 'version': '#version', * 'mnemonicHasPassphrase': 'mnemonicHasPassphrase', * 'fingerPrint': 'fingerPrint', // 32bit fingerprint * 'compliantDerivation': 'compliantDerivation', * 'BIP45': 'BIP45', * * // data for derived credentials. * 'use0forBCH': 'use0forBCH', // use the 0 coin' path element in BCH (legacy) * 'use44forMultisig': 'use44forMultisig', // use the purpose 44' for multisig wallts (legacy) * 'id': 'id', * }; */ /** * @param {Object} opts * @param {String} opts.password encrypting password * @param {String} seedType new|extendedPrivateKey|object|mnemonic * @param {String} seedData */ constructor( opts: { id?: string; seedType: string; seedData?: any; passphrase?: string; // seed passphrase password?: string; // encrypting password sjclOpts?: any; // options to SJCL encrypt use0forBCH?: boolean; useLegacyPurpose?: boolean; useLegacyCoinType?: boolean; nonCompliantDerivation?: boolean; language?: string; } = { seedType: 'new' } ) { this.#version = 1; this.id = opts.id || Uuid.v4(); // bug backwards compatibility flags this.use0forBCH = opts.useLegacyCoinType; this.use44forMultisig = opts.useLegacyPurpose; this.compliantDerivation = !opts.nonCompliantDerivation; let x = opts.seedData; switch (opts.seedType) { case 'new': if (opts.language && !wordsForLang[opts.language]) throw new Error('Unsupported language'); let m = new Mnemonic(wordsForLang[opts.language]); while (!Mnemonic.isValid(m.toString())) { m = new Mnemonic(wordsForLang[opts.language]); } this.setFromMnemonic(m, opts); break; case 'mnemonic': $.checkArgument(x, 'Need to provide opts.seedData'); $.checkArgument(typeof x === 'string', 'sourceData need to be a string'); this.setFromMnemonic(new Mnemonic(x), opts); break; case 'extendedPrivateKey': $.checkArgument(x, 'Need to provide opts.seedData'); let xpriv; try { xpriv = new Bitcore.HDPrivateKey(x); } catch (e) { throw new Error('Invalid argument'); } this.fingerPrint = xpriv.fingerPrint.toString('hex'); if (opts.password) { this.#xPrivKeyEncrypted = sjcl.encrypt( opts.password, xpriv.toString(), opts ); if (!this.#xPrivKeyEncrypted) throw new Error('Could not encrypt'); } else { this.#xPrivKey = xpriv.toString(); } this.#mnemonic = null; this.#mnemonicHasPassphrase = null; break; case 'object': $.shouldBeObject(x, 'Need to provide an object at opts.seedData'); $.shouldBeUndefined( opts.password, 'opts.password not allowed when source is object' ); if (this.#version != x.version) { throw new Error('Bad Key version'); } this.#xPrivKey = x.xPrivKey; this.#xPrivKeyEncrypted = x.xPrivKeyEncrypted; this.#mnemonic = x.mnemonic; this.#mnemonicEncrypted = x.mnemonicEncrypted; this.#mnemonicHasPassphrase = x.mnemonicHasPassphrase; this.#version = x.version; this.fingerPrint = x.fingerPrint; this.compliantDerivation = x.compliantDerivation; this.BIP45 = x.BIP45; this.id = x.id; this.use0forBCH = x.use0forBCH; this.use44forMultisig = x.use44forMultisig; $.checkState( this.#xPrivKey || this.#xPrivKeyEncrypted, 'Failed state: #xPrivKey || #xPrivKeyEncrypted at Key constructor' ); break; case 'objectV1': // Default Values for V1 this.use0forBCH = false; this.use44forMultisig = false; this.compliantDerivation = true; this.id = Uuid.v4(); if (x.compliantDerivation != null) this.compliantDerivation = x.compliantDerivation; if (x.id != null) this.id = x.id; this.#xPrivKey = x.xPrivKey; this.#xPrivKeyEncrypted = x.xPrivKeyEncrypted; this.#mnemonic = x.mnemonic; this.#mnemonicEncrypted = x.mnemonicEncrypted; this.#mnemonicHasPassphrase = x.mnemonicHasPassphrase; this.#version = x.version || 1; this.fingerPrint = x.fingerPrint; // If the wallet was single seed... multisig walelts accounts // will be 48' this.use44forMultisig = x.n > 1 ? true : false; // if old credentials had use145forBCH...use it. // else,if the wallet is bch, set it to true. this.use0forBCH = x.use145forBCH ? false : x.coin == 'bch' ? true : false; this.BIP45 = x.derivationStrategy == 'BIP45'; break; default: throw new Error('Unknown seed source: ' + opts.seedType); } } static match(a, b) { // fingerPrint is not always available (because xPriv could has // been imported encrypted) return a.id == b.id || a.fingerPrint == b.fingerPrint; } private setFromMnemonic( m, opts: { passphrase?: string; password?: string; sjclOpts?: any } ) { const xpriv = m.toHDPrivateKey(opts.passphrase, NETWORK); this.fingerPrint = xpriv.fingerPrint.toString('hex'); if (opts.password) { this.#xPrivKeyEncrypted = sjcl.encrypt( opts.password, xpriv.toString(), opts.sjclOpts ); if (!this.#xPrivKeyEncrypted) throw new Error('Could not encrypt'); this.#mnemonicEncrypted = sjcl.encrypt( opts.password, m.phrase, opts.sjclOpts ); if (!this.#mnemonicEncrypted) throw new Error('Could not encrypt'); } else { this.#xPrivKey = xpriv.toString(); this.#mnemonic = m.phrase; this.#mnemonicHasPassphrase = !!opts.passphrase; } } toObj() { const ret = { xPrivKey: this.#xPrivKey, xPrivKeyEncrypted: this.#xPrivKeyEncrypted, mnemonic: this.#mnemonic, mnemonicEncrypted: this.#mnemonicEncrypted, version: this.#version, mnemonicHasPassphrase: this.#mnemonicHasPassphrase, fingerPrint: this.fingerPrint, // 32bit fingerprint compliantDerivation: this.compliantDerivation, BIP45: this.BIP45, // data for derived credentials. use0forBCH: this.use0forBCH, use44forMultisig: this.use44forMultisig, id: this.id }; return JSON.parse(JSON.stringify(ret)); }; isPrivKeyEncrypted() { return !!this.#xPrivKeyEncrypted && !this.#xPrivKey; }; checkPassword(password) { if (this.isPrivKeyEncrypted()) { try { sjcl.decrypt(password, this.#xPrivKeyEncrypted); } catch (ex) { return false; } return true; } return null; }; get(password) { let 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; } } keys.mnemonicHasPassphrase = this.#mnemonicHasPassphrase || false; return keys; }; encrypt(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); this.#xPrivKey = null; this.#mnemonic = null; }; decrypt(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); } this.#xPrivKeyEncrypted = null; this.#mnemonicEncrypted = null; } catch (ex) { log.error('error decrypting:', ex); throw new Error('Could not decrypt'); } }; derive(password, path): Bitcore.HDPrivateKey { $.checkArgument(path, 'no path at derive()'); const xPrivKey = new Bitcore.HDPrivateKey( this.get(password).xPrivKey, NETWORK ); const deriveFn = this.compliantDerivation ? xPrivKey.deriveChild.bind(xPrivKey) : xPrivKey.deriveNonCompliantChild.bind(xPrivKey); return deriveFn(path); }; _checkChain(chain) { if (!Constants.CHAINS.includes(chain)) throw new Error('Invalid chain'); }; _checkNetwork(network) { if (!['livenet', 'testnet', 'regtest'].includes(network)) throw new Error('Invalid network ' + 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'); const chain = opts.chain || Utils.getChain(opts.coin); let purpose = opts.n == 1 || this.use44forMultisig ? '44' : '48'; let coinCode = '0'; // checking in chains for simplicity if ( ['testnet', 'regtest]'].includes(opts.network) && Constants.UTXO_CHAINS.includes(chain) ) { coinCode = '1'; } else if (chain == 'bch') { if (this.use0forBCH || opts.use0forBCH) { coinCode = '0'; } else { coinCode = '145'; } } else if (chain == 'btc') { coinCode = '0'; } else if (chain == 'eth') { coinCode = '60'; } else if (chain == 'matic') { coinCode = '60'; // the official matic derivation path is 966 but users will expect address to be same as ETH } else if (chain == 'arb') { coinCode = '60'; } else if (chain == 'op') { coinCode = '60'; } else if (chain == 'base') { coinCode = '60'; } else if (chain == 'xrp') { coinCode = '144'; } else if (chain == 'doge') { coinCode = '3'; } else if (chain == 'xec') { coinCode = '899'; if (opts.isSlpToken) { if (opts.isPath899) { coinCode = '899'; } else { coinCode = '1899'; } if (opts.isFromRaipay) { coinCode = '145'; } } } else if (chain == 'xpi') { coinCode = '10605'; } else if (chain == 'ltc') { coinCode = '2'; } else { throw new Error('unknown chain: ' + chain); } return 'm/' + purpose + "'/" + coinCode + "'/" + opts.account + "'"; }; /* * opts.chain * opts.network * opts.account * opts.n */ createCredentials(password, opts) { opts = opts || {}; opts.chain = opts.chain || Utils.getChain(opts.coin); if (password) $.shouldBeString(password, 'provide password'); 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 (['testnet', 'regtest'].includes(opts.network)) { // Hacky: BTC/BCH xPriv depends on network: This code is to // convert a livenet xPriv to a testnet/regtest xPriv let x = xPrivKey.toObject(); x.network = opts.network; delete x.xprivkey; delete x.checksum; x.privateKey = x.privateKey.padStart(64, '0'); xPrivKey = new Bitcore.HDPrivateKey(x); } return Credentials.fromDerivedKey({ xPubKey: xPrivKey.hdPublicKey.toString(), coin: opts.coin, chain: opts.chain?.toLowerCase() || Utils.getChain(opts.coin), // getChain -> backwards compatibility network: opts.network, account: opts.account, n: opts.n, rootPath: path, keyId: this.id, requestPrivKey, addressType: opts.addressType, walletPrivKey: opts.walletPrivKey, isSlpToken: !!opts.isSlpToken, isFromRaipay: !!opts.isFromRaipay, isPath899: !!opts.isPath899 }); }; /** * @param {string} password * @param {Object} opts * @param {string} opts.path * @param {string|PrivateKey} [opts.requestPrivKey] */ createAccess(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(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); var chain = txp.chain?.toLowerCase() || Utils.getChain(txp.coin); // getChain -> backwards compatibility if (Constants.UTXO_CHAINS.includes(chain)) { for (const i of txp.inputs) { $.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 = privs.map(function(priv, i) { return t.getSignatures(priv, undefined, txp.signingMethod); }); signatures = signatures.flat().sort((a, b) => a.inputIndex - b.inputIndex); // DEBUG // for (let sig of signatures) { // if (!t.isValidSignature(sig)) { // throw new Error('INVALID SIGNATURE'); // } // } signatures = signatures.map(sig => sig.signature.toDER().toString('hex')); return signatures; } else { let tx = t.uncheckedSerialize(); tx = typeof tx === 'string' ? [tx] : tx; const txArray = Array.isArray(tx) ? tx : [tx]; const isChange = false; const addressIndex = 0; const { privKey, pubKey } = Deriver.derivePrivateKey( chain.toUpperCase(), txp.network, derived, addressIndex, isChange ); let signatures = []; for (const rawTx of txArray) { const signed = Transactions.getSignature({ chain: chain.toUpperCase(), tx: rawTx, key: { privKey, pubKey } }); signatures.push(signed); } return signatures; } }; }