diff --git a/packages/bitcore-client/src/encryption.ts b/packages/bitcore-client/src/encryption.ts index 7579477d3f0..b0a89565a07 100644 --- a/packages/bitcore-client/src/encryption.ts +++ b/packages/bitcore-client/src/encryption.ts @@ -23,13 +23,27 @@ export function encryptEncryptionKey(encryptionKey, password) { return encData; } -export function decryptEncryptionKey(encEncryptionKey, password) { +export function decryptEncryptionKey(encEncryptionKey, password, toBuffer: true): Buffer; +export function decryptEncryptionKey(encEncryptionKey, password, toBuffer: false): string; +export function decryptEncryptionKey(encEncryptionKey, password, toBuffer?: boolean): Buffer | string { const password_hash = Buffer.from(SHA512(password)); const key = password_hash.subarray(0, 32); const iv = password_hash.subarray(32, 48); const decipher = crypto.createDecipheriv(algo, key, iv); - const decrypted = decipher.update(encEncryptionKey, 'hex', 'hex' as any) + decipher.final('hex'); - return decrypted; + + const payload = decipher.update(encEncryptionKey, 'hex'); + const final = decipher.final(); + const output = Buffer.concat([payload, final]); + try { + return toBuffer ? output : output.toString('hex'); + } finally { + payload.fill(0); + final.fill(0); + if (!toBuffer) { + // Don't fill output if it's what's returned directly + output.fill(0); + } + } } export function encryptPrivateKey(privKey, pubKey, encryptionKey) { @@ -41,15 +55,42 @@ export function encryptPrivateKey(privKey, pubKey, encryptionKey) { return encData; } -function decryptPrivateKey(encPrivateKey: string, pubKey: string, encryptionKey: string) { - const key = Buffer.from(encryptionKey, 'hex'); +function decryptPrivateKey(encPrivateKey: string, pubKey: string, encryptionKey: Buffer | string) { + if (!Buffer.isBuffer(encryptionKey)) { + encryptionKey = Buffer.from(encryptionKey, 'hex'); + } const doubleHash = Buffer.from(SHA256(SHA256(pubKey)), 'hex'); const iv = doubleHash.subarray(0, 16); - const decipher = crypto.createDecipheriv(algo, key, iv); + const decipher = crypto.createDecipheriv(algo, encryptionKey, iv); const decrypted = decipher.update(encPrivateKey, 'hex', 'utf8') + decipher.final('utf8'); return decrypted; } +function encryptBuffer(data: Buffer, pubKey: string, encryptionKey: Buffer): Buffer { + const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); + const cipher = crypto.createCipheriv(algo, encryptionKey, iv); + const payload = cipher.update(data); + try { + return Buffer.concat([payload, cipher.final()]); + } finally { + payload.fill(0); + } +} + +function decryptToBuffer(encHex: string, pubKey: string, encryptionKey: Buffer): Buffer { + const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); + const decipher = crypto.createDecipheriv(algo, encryptionKey, iv); + + const decrypted = decipher.update(encHex, 'hex'); + const final = decipher.final(); + try { + return Buffer.concat([decrypted, final]); + } finally { + decrypted.fill(0); + final.fill(0); + } +} + function sha512KDF(passphrase: string, salt: Buffer, derivationOptions: { rounds?: number }): string { const rounds = derivationOptions.rounds || 1; // if salt was sent in as a string, we will have to assume the default encoding type @@ -134,6 +175,8 @@ export const Encryption = { decryptEncryptionKey, encryptPrivateKey, decryptPrivateKey, + encryptBuffer, + decryptToBuffer, generateEncryptionKey, bitcoinCoreDecrypt }; diff --git a/packages/bitcore-client/src/storage.ts b/packages/bitcore-client/src/storage.ts index ca43ba97198..3887c94eb88 100644 --- a/packages/bitcore-client/src/storage.ts +++ b/packages/bitcore-client/src/storage.ts @@ -57,8 +57,11 @@ export class Storage { (this.storageType as Mongo)?.close?.(); } - async loadWallet(params: { name: string }): Promise { - const { name } = params; + async loadWallet(params: { name: string }): Promise + async loadWallet(params: { name: string; raw: true }): Promise + async loadWallet(params: { name: string; raw: false }): Promise + async loadWallet(params: { name: string; raw?: boolean }): Promise { + const { name, raw } = params; let wallet: string | void; for (const db of await this.verifyDbs(this.db)) { try { @@ -72,7 +75,7 @@ export class Storage { if (!wallet) { return; } - return JSON.parse(wallet) as IWallet; + return raw ? wallet : JSON.parse(wallet) as IWallet; } async deleteWallet(params: { name: string }) { @@ -189,4 +192,68 @@ export class Storage { const { name, limit, skip } = params; return this.storageType.getAddresses({ name, limit, skip }); } + + /** + * New methods + * TODO: Deprecate above as necessary + */ + async addKeysSafe(params: { name: string; keys: KeyImport[] }) { + const { name, keys } = params; + let i = 0; + for (const key of keys) { + const { path } = key; + const pubKey = key.pubKey; + // key.privKey is encrypted - cannot be directly used to retrieve pubKey if required + if (!pubKey) { + throw new Error(`pubKey is undefined for ${name}. Keys not added to storage`); + } + let payload = {}; + if (pubKey) { + payload = { key: JSON.stringify(key), pubKey, path }; + } + const toStore = JSON.stringify(payload); + // open on first, close on last + await this.storageType.addKeys({ name, key, toStore, open: i === 0, keepAlive: i < keys.length - 1 }); + ++i; + } + } + + async getStoredKeys(params: { addresses: string[]; name: string }): Promise> { + const { addresses, name } = params; + const keys = new Array(); + let i = 0; + for (const address of addresses) { + try { + const key = await this.getStoredKey({ + name, + address, + open: i === 0, // open on first + keepAlive: i < addresses.length - 1, // close on last + }); + keys.push(key); + } catch (err) { + // don't continue from catch - i must be incremented + console.error(err); + } + ++i; + } + return keys; + } + + private async getStoredKey(params: { + address: string; + name: string; + keepAlive: boolean; + open: boolean; + }): Promise { + const { address, name, keepAlive, open } = params; + const payload = await this.storageType.getKey({ name, address, keepAlive, open }); + const json = JSON.parse(payload) || payload; + const { key } = json; // pubKey available - not needed + if (key) { + return JSON.parse(key); + } else { + return json; + } + } } diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 1f27282d643..5e7f3ba429b 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -1,3 +1,4 @@ +import { writeFile } from 'fs/promises'; import 'source-map-support/register'; import * as Bcrypt from 'bcrypt'; import Mnemonic from 'bitcore-mnemonic'; @@ -36,9 +37,11 @@ const chainLibs = { XRP: xrpl, SOL: { SolKit, SolanaProgram } }; +const CURRENT_WALLET_VERSION = 2; export interface IWalletExt extends IWallet { storage?: Storage; + version?: 0 | 2; // Wallet versioning used for backwards compatibility } export class Wallet { @@ -49,7 +52,7 @@ export class Wallet { client: Client; storage: Storage; storageType: string; - unlocked?: { encryptionKey: string; masterKey: string }; + unlocked?: { encryptionKey: Buffer; masterKey: { xprivkey: Buffer; privateKey: Buffer } }; password: string; encryptionKey: string; authPubKey: string; @@ -64,6 +67,7 @@ export class Wallet { lite: boolean; addressType: string; addressZero: string; + version?: number; static XrpAccountFlags = xrpl.AccountSetTfFlags; @@ -120,7 +124,8 @@ export class Wallet { storageType: this.storageType, lite, addressType: this.addressType, - addressZero: this.addressZero + addressZero: this.addressZero, + version: this.version }; } @@ -157,11 +162,7 @@ export class Wallet { const keyType = Constants.ALGO_TO_KEY_TYPE[algo]; hdPrivKey = mnemonic.toHDPrivateKey('', network).derive(Deriver.pathFor(chain, network), keyType); } - const privKeyObj = hdPrivKey.toObject(); - - // Generate authentication keys - const authKey = new PrivateKey(); - const authPubKey = authKey.toPublicKey().toString(); + const privKeyObj = hdPrivKey.toObjectWithBufferPrivateKey(); // Generate public keys // bip44 compatible pubKey @@ -169,8 +170,18 @@ export class Wallet { // Generate and encrypt the encryption key and private key const walletEncryptionKey = Encryption.generateEncryptionKey(); - const encryptionKey = Encryption.encryptEncryptionKey(walletEncryptionKey, password); - const encPrivateKey = Encryption.encryptPrivateKey(JSON.stringify(privKeyObj), pubKey, walletEncryptionKey); + const encryptionKey = Encryption.encryptEncryptionKey(walletEncryptionKey, password); // stored, password-wrapped + + // Encrypt privKeyObj.privateKey & privKeyObj.xprivkey + const xprivBuffer = BitcoreLib.encoding.Base58Check.decode(privKeyObj.xprivkey); + privKeyObj.xprivkey = Encryption.encryptBuffer(xprivBuffer, pubKey, walletEncryptionKey).toString('hex'); + privKeyObj.privateKey = Encryption.encryptBuffer(privKeyObj.privateKey, pubKey, walletEncryptionKey).toString('hex'); + + // Generate authentication keys + const authKey = new PrivateKey(); + const authPubKey = authKey.toPublicKey().toString(); + + const masterKeyWithEncryptedPrivateKeys = JSON.stringify(privKeyObj); storageType = storageType ? storageType : 'Level'; storage = @@ -182,13 +193,11 @@ export class Wallet { storageType }); - let alreadyExists; - try { - alreadyExists = await this.loadWallet({ storage, name, storageType }); - } catch { /* ignore */ } + const alreadyExists = await this.loadWallet({ storage, name, storageType }).catch(() => {/** no op */}); if (alreadyExists) { throw new Error('Wallet already exists'); } + const wallet = new Wallet({ name, chain, @@ -198,7 +207,7 @@ export class Wallet { encryptionKey, authKey, authPubKey, - masterKey: encPrivateKey, + masterKey: masterKeyWithEncryptedPrivateKeys, password, xPubKey: hdPrivKey.xpubkey, pubKey, @@ -207,7 +216,8 @@ export class Wallet { storageType, lite, addressType, - addressZero: null + addressZero: null, + version: CURRENT_WALLET_VERSION, } as IWalletExt); // save wallet to storage and then bitcore-node @@ -218,12 +228,6 @@ export class Wallet { storageType }); - if (!xpriv) { - console.log(mnemonic.toString()); - } else { - console.log(hdPrivKey.toString()); - } - await loadedWallet.register().catch(e => { console.debug(e); console.error('Failed to register wallet with bitcore-node.'); @@ -251,11 +255,12 @@ export class Wallet { let { storage } = params; storage = storage || new Storage({ errorIfExists: false, createIfMissing: false, path, storageType }); const loadedWallet = await storage.loadWallet({ name }); - if (loadedWallet) { - return new Wallet(Object.assign(loadedWallet, { storage })); - } else { + + if (!loadedWallet) { throw new Error('No wallet could be found'); } + + return new Wallet(Object.assign(loadedWallet, { storage })); } /** @@ -280,7 +285,20 @@ export class Wallet { } lock() { - this.unlocked = undefined; + if (Buffer.isBuffer(this.unlocked.masterKey.xprivkey)) { + this.unlocked.masterKey.xprivkey.fill(0); + } + + if (Buffer.isBuffer(this.unlocked.masterKey.privateKey)) { + this.unlocked.masterKey.privateKey.fill(0); + } + this.unlocked.masterKey = null; + + if (Buffer.isBuffer(this.unlocked.encryptionKey)) { + this.unlocked.encryptionKey.fill(0); + } + this.unlocked.encryptionKey = null; + this.unlocked = null; return this; } @@ -289,12 +307,15 @@ export class Wallet { if (!validPass) { throw new Error('Incorrect Password'); } - const encryptionKey = await Encryption.decryptEncryptionKey(this.encryptionKey, password); + const encryptionKey = Encryption.decryptEncryptionKey(this.encryptionKey, password, true); + if (this.version != CURRENT_WALLET_VERSION) { + await this.migrateWallet(encryptionKey); + } let masterKey; if (!this.lite) { - const encMasterKey = this.masterKey; - const masterKeyStr = await Encryption.decryptPrivateKey(encMasterKey, this.pubKey, encryptionKey); - masterKey = JSON.parse(masterKeyStr); + masterKey = JSON.parse(this.masterKey); + masterKey.xprivkey = Encryption.decryptToBuffer(masterKey.xprivkey, this.pubKey, encryptionKey); + masterKey.privateKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, encryptionKey); } this.unlocked = { encryptionKey, @@ -303,6 +324,123 @@ export class Wallet { return this; } + async migrateWallet(encryptionKey: Buffer): Promise { + /** + * 0: Checks + */ + if (this.version == CURRENT_WALLET_VERSION) { + console.warn('Wallet migration unnecessarily called - wallet is current version'); + return this; + } + + if (this.version > CURRENT_WALLET_VERSION) { + console.warn(`Wallet version ${this.version} greater than expected current wallet version ${CURRENT_WALLET_VERSION}`); + return this; + } + + /** + * 1: Wallet to .bak + */ + const rawWallet = await this.storage.loadWallet({ name: this.name, raw: true }); + if (!rawWallet) { + throw new Error('Migration failed - wallet not found'); + } + + await writeFile(`${this.name}.bak`, rawWallet, 'utf8') + .catch(err => { + console.error('Wallet backup failed, aborting migration', err.msg); + throw new Error('Migration failure: failed to write wallet backup file. Aborting.'); + }); + + /** + * Retrieve stored keys for backup and for migration + */ + const addresses = await this.getAddresses(); + const storedKeys = await this.storage.getStoredKeys({ + addresses, + name: this.name, + }); + + // Back up keys (enc) + const backupKeysStr = JSON.stringify(storedKeys); + await writeFile(`${this.name}_keys.bak`, backupKeysStr, 'utf8') + .catch(err => { + console.error('Keys backup failed, aborting migration', err.msg); + throw new Error('Migration failure: failed to write keys backup file. Aborting.'); + }); + + /** + * 2. Convert + */ + + /** + * 2a. Convert masterKey and encryptionKey + */ + const masterKeyStr = Encryption.decryptPrivateKey(this.masterKey, this.pubKey, encryptionKey); + const masterKey = JSON.parse(masterKeyStr); + if (!(masterKey.xprivkey && masterKey.privateKey)) { + throw new Error('Migration failure: masterKey is not formatted as expected'); + } + + const xprivBuffer = BitcoreLib.encoding.Base58Check.decode(masterKey.xprivkey); + const enc_xprivkeyBuffer = Encryption.encryptBuffer(xprivBuffer, this.pubKey, encryptionKey); + xprivBuffer.fill(0); + + masterKey.xprivkey = enc_xprivkeyBuffer.toString('hex'); + enc_xprivkeyBuffer.fill(0); + + const privateKeyBuffer = Buffer.from(masterKey.privateKey, 'hex'); + const enc_privateKeyBuffer = Encryption.encryptBuffer(privateKeyBuffer, this.pubKey, encryptionKey); + privateKeyBuffer.fill(0); + + masterKey.privateKey = enc_privateKeyBuffer.toString('hex'); + enc_privateKeyBuffer.fill(0); + + // String with encrypted hex-encoded xprivkey and privateKey strings + this.masterKey = JSON.stringify(masterKey); + + /** + * 2b. Convert signing keys + */ + const newKeys = []; + for (const key of storedKeys) { + const { encKey, pubKey } = key; + const decryptedKey = Encryption.decryptPrivateKey(encKey, pubKey, encryptionKey); + const decryptedKeyJSON = JSON.parse(decryptedKey); + + // Convert private key to buffer format (uniform across all chains) + const privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, decryptedKeyJSON.privKey); + const encryptedPrivateKeyBuffer = Encryption.encryptBuffer(privKeyBuffer, pubKey, encryptionKey); + privKeyBuffer.fill(0); // Zero out the plaintext buffer + + decryptedKeyJSON.privKey = encryptedPrivateKeyBuffer.toString('hex'); + newKeys.push(decryptedKeyJSON); + } + + /** + * 3. Overwrite + */ + // 3a. Overwrite keys + await this.storage.addKeysSafe({ name: this.name, keys: newKeys }) + .catch(err => { + console.error('Migration failure: updated keys not successfully stored', err); + throw new Error('Migration failure: keys not successfully stored. Use backups to restore prior wallet and keys.'); + }); + + // 3b. Overwrite wallet + this.version = CURRENT_WALLET_VERSION; + const storedEncryptedPassword = this.password; // Wallet.toObject() rehashes password - save and replace + const walletObj = this.toObject(false); + walletObj.password = storedEncryptedPassword; + await this.storage.saveWallet({ wallet: walletObj }) + .catch(err => { + console.error('Migration failure: wallet not successfully saved', err); + throw new Error('Migration failure: wallet not successfully saved. Use backups to restore prior wallet and keys'); + }); + + return this; + } + async register(params: { baseUrl?: string } = {}) { const { baseUrl } = params; if (baseUrl) { @@ -408,7 +546,6 @@ export class Wallet { // If tokenName was given, find the token by name (e.g. USDC_m) let tokenObj = tokenName && this.tokens.find(tok => tok.name === tokenName); // If not found by name AND token was given, find the token by symbol (e.g. USDC) - // NOTE: we don't want to tokenObj = tokenObj || (token && this.tokens.find(tok => tok.symbol === token && [token, undefined].includes(tok.name))); if (!tokenObj) { throw new Error(`${tokenName || token} not found on wallet ${this.name}`); @@ -596,7 +733,6 @@ export class Wallet { } async importKeys(params: { keys: KeyImport[]; rederiveAddys?: boolean }) { - const { encryptionKey } = this.unlocked; const { rederiveAddys } = params; let { keys } = params; let keysToSave = keys.filter(key => typeof key.privKey === 'string'); @@ -611,11 +747,16 @@ export class Wallet { address: key.pubKey ? Deriver.getAddress(this.chain, this.network, key.pubKey, this.addressType) : key.address }) as KeyImport); } + + for (const key of keysToSave) { + const privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, key.privKey); + key.privKey = Encryption.encryptBuffer(privKeyBuffer, key.pubKey, this.unlocked.encryptionKey).toString('hex'); + privKeyBuffer.fill(0); + } if (keysToSave.length) { - await this.storage.addKeys({ + await this.storage.addKeysSafe({ keys: keysToSave, - encryptionKey, name: this.name }); } @@ -642,24 +783,27 @@ export class Wallet { } let addresses = []; let decryptedKeys; - if (!keys && !signingKeys) { - for (const utxo of utxos) { - addresses.push(utxo.address); - } - addresses = addresses.length > 0 ? addresses : await this.getAddresses(); - decryptedKeys = await this.storage.getKeys({ - addresses, - name: this.name, - encryptionKey: this.unlocked.encryptionKey - }); - } else if (!signingKeys) { - addresses.push(keys[0]); - for (const element of utxos) { - const keyToDecrypt = keys.find(key => key.address === element.address); - addresses.push(keyToDecrypt); + let decryptPrivateKeys = true; + if (!signingKeys) { + if (!keys) { + for (const utxo of utxos) { + addresses.push(utxo.address); + } + addresses = addresses.length > 0 ? addresses : await this.getAddresses(); + decryptedKeys = await this.storage.getStoredKeys({ + addresses, + name: this.name, + }); + } else { + addresses.push(keys[0]); + for (const element of utxos) { + const keyToDecrypt = keys.find(key => key.address === element.address); + addresses.push(keyToDecrypt); + } + const decryptedParams = Encryption.bitcoinCoreDecrypt(addresses, passphrase); + decryptedKeys = [...decryptedParams.jsonlDecrypted]; + decryptPrivateKeys = false; } - const decryptedParams = Encryption.bitcoinCoreDecrypt(addresses, passphrase); - decryptedKeys = [...decryptedParams.jsonlDecrypted]; } if (this.isUtxoChain()) { // If changeAddressIdx == null, then save the change key at the current addressIndex (just in case) @@ -667,12 +811,37 @@ export class Wallet { await this.importKeys({ keys: [changeKey] }); } + // Shallow copy to avoid mutation if signingKeys are passed in + const keysForSigning = [...(signingKeys || decryptedKeys)]; + + if (decryptPrivateKeys) { + for (const key of keysForSigning) { + let privKeyBuf: Buffer | undefined; + try { + privKeyBuf = Encryption.decryptToBuffer(key.privKey, key.pubKey, this.unlocked.encryptionKey); + + // Convert buffer to chain-specific native format (e.g., WIF for BTC, hex for ETH, base58 for SOL) + const nativePrivKey = Deriver.privateKeyBufferToNativePrivateKey(this.chain, this.network, privKeyBuf); + + key.privKey = nativePrivKey; + } catch (e) { + console.error('Failed to decrypt/convert private key:', e); + continue; + } finally { + // Zero out the buffer immediately after use + if (Buffer.isBuffer(privKeyBuf)) { + privKeyBuf.fill(0); + } + } + } + } + const payload = { chain: this.chain, network: this.network, tx, - keys: signingKeys || decryptedKeys, - key: signingKeys ? signingKeys[0] : decryptedKeys[0], + keys: keysForSigning, + key: keysForSigning[0], utxos }; return Transactions.sign({ ...payload }); @@ -741,10 +910,21 @@ export class Wallet { } async derivePrivateKey(isChange, addressIndex = this.addressIndex) { + let masterKeyForDeriver: any = this.unlocked.masterKey; + if (Buffer.isBuffer(this.unlocked.masterKey.xprivkey)) { + const xprivString = BitcoreLib.encoding.Base58Check.encode(this.unlocked.masterKey.xprivkey); + const privateKeyString = this.unlocked.masterKey.privateKey.toString('hex'); + masterKeyForDeriver = { + ...this.unlocked.masterKey, + xprivkey: xprivString, + privateKey: privateKeyString + }; + } + const keyToImport = await Deriver.derivePrivateKey( this.chain, this.network, - this.unlocked.masterKey, + masterKeyForDeriver, addressIndex || 0, isChange, this.addressType diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index 0009f562f5a..862635db2c8 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -1,6 +1,7 @@ import * as chai from 'chai'; import * as CWC from 'crypto-wallet-core'; import { AddressTypes, Wallet } from '../../src/wallet'; +import { Encryption } from '../../src/encryption'; import { Api as bcnApi } from '../../../bitcore-node/build/src/services/api'; import { Storage as bcnStorage } from '../../../bitcore-node/build/src/services/storage'; import crypto from 'crypto'; @@ -82,7 +83,8 @@ describe('Wallet', function() { lite: false, addressType, storageType, - baseUrl + baseUrl, + version: 0 }); expect(wallet.addressType).to.equal(AddressTypes[chain]?.[addressType] || 'pubkeyhash'); @@ -123,7 +125,8 @@ describe('Wallet', function() { phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', password: 'abc123', storageType, - baseUrl + baseUrl, + version: 0 }); await wallet.unlock('abc123'); }); @@ -199,7 +202,8 @@ describe('Wallet', function() { phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', password: 'abc123', storageType, - baseUrl + baseUrl, + version: 0 }); await wallet.unlock('abc123'); }); @@ -262,7 +266,8 @@ describe('Wallet', function() { password: 'abc123', storageType, path, - baseUrl + baseUrl, + version: 0 }); await wallet.unlock('abc123'); // 3 address pairs @@ -303,7 +308,8 @@ describe('Wallet', function() { phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', password: 'abc123', storageType, - baseUrl + baseUrl, + version: 0 }); await wallet.unlock('abc123'); requestStub = sandbox.stub(wallet.client, '_request').resolves(); @@ -368,6 +374,112 @@ describe('Wallet', function() { }); }); + describe('signTx v2 key handling', function() { + let txStub: sinon.SinonStub; + afterEach(async function() { + txStub?.restore(); + }); + + describe('BTC (UTXO) decrypts ciphertext to WIF', function() { + walletName = 'BitcoreClientTestSignTxV2-BTC'; + let wallet: Wallet; + + beforeEach(async function() { + wallet = await Wallet.create({ + name: walletName, + chain: 'BTC', + network: 'testnet', + phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', + password: 'abc123', + storageType, + baseUrl + }); + await wallet.unlock('abc123'); + }); + + afterEach(async function() { + await Wallet.deleteWallet({ name: walletName, storageType }); + }); + + it('should decrypt stored ciphertext and hand WIF to Transactions.sign', async function() { + const pk = new CWC.BitcoreLib.PrivateKey(undefined, 'testnet'); + const address = pk.toAddress().toString(); + const privBuf = CWC.Deriver.privateKeyToBuffer('BTC', pk.toString()); + const encPriv = Encryption.encryptBuffer(privBuf, wallet.pubKey, wallet.unlocked.encryptionKey).toString('hex'); + privBuf.fill(0); + + sandbox.stub(wallet.storage, 'getKeys').resolves([ + { + address, + privKey: encPriv, + pubKey: pk.publicKey.toString() + } + ]); + sandbox.stub(wallet, 'derivePrivateKey').resolves({ + address: 'change', + privKey: pk.toString(), + pubKey: pk.publicKey.toString(), + path: 'm/1/0' + }); + sandbox.stub(wallet, 'importKeys').resolves(); + + let capturedPayload; + txStub = sandbox.stub(CWC.Transactions, 'sign').callsFake(payload => { + capturedPayload = payload; + return 'signed'; + }); + + const utxos = [{ address, value: 1 }]; + await wallet.signTx({ tx: 'raw', utxos }); + + txStub.calledOnce.should.equal(true); + capturedPayload.keys[0].privKey.should.equal(pk.toWIF()); + capturedPayload.key.privKey.should.equal(pk.toWIF()); + }); + }); + + describe('ETH (account) decrypts ciphertext to hex and skips plaintext', function() { + walletName = 'BitcoreClientTestSignTxV2-ETH'; + let wallet: Wallet; + + beforeEach(async function() { + wallet = await Wallet.create({ + name: walletName, + chain: 'ETH', + network: 'testnet', + phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', + password: 'abc123', + storageType, + baseUrl + }); + await wallet.unlock('abc123'); + }); + + afterEach(async function() { + await Wallet.deleteWallet({ name: walletName, storageType }); + }); + + it('should decrypt stored ciphertext and hand hex privKey to Transactions.sign', async function() { + const privHex = crypto.randomBytes(32).toString('hex'); + const privBuf = CWC.Deriver.privateKeyToBuffer('ETH', privHex); + const encPriv = Encryption.encryptBuffer(privBuf, wallet.pubKey, wallet.unlocked.encryptionKey).toString('hex'); + privBuf.fill(0); + + let capturedPayload; + txStub = sandbox.stub(CWC.Transactions, 'sign').callsFake(payload => { + capturedPayload = payload; + return 'signed'; + }); + + const signingKeys = [{ address: '0xabc', privKey: encPriv }]; + await wallet.signTx({ tx: 'raw', signingKeys }); + + txStub.calledOnce.should.equal(true); + capturedPayload.keys[0].privKey.should.equal(privHex); + }); + }); + }); + describe('getBalance', function() { walletName = 'BitcoreClientTestGetBalance'; beforeEach(async function() { @@ -378,6 +490,7 @@ describe('Wallet', function() { phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', password: 'abc123', storageType: 'Level', + version: 0, }); await wallet.unlock('abc123'); }); @@ -427,6 +540,7 @@ describe('Wallet', function() { phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', password: 'abc123', storageType: 'Level', + version: 0, }); await wallet.unlock('abc123'); }); @@ -506,7 +620,8 @@ describe('Wallet', function() { password: 'abc123', lite: false, storageType, - baseUrl + baseUrl, + version: 0 }); wallet.tokens = [ diff --git a/packages/bitcore-lib/lib/hdprivatekey.js b/packages/bitcore-lib/lib/hdprivatekey.js index e9bb35d7752..19f1f53b244 100644 --- a/packages/bitcore-lib/lib/hdprivatekey.js +++ b/packages/bitcore-lib/lib/hdprivatekey.js @@ -4,21 +4,20 @@ var assert = require('assert'); var buffer = require('buffer'); var _ = require('lodash'); -var $ = require('./util/preconditions'); - var BN = require('./crypto/bn'); +var Hash = require('./crypto/hash'); +var Point = require('./crypto/point'); +var Random = require('./crypto/random'); var Base58 = require('./encoding/base58'); var Base58Check = require('./encoding/base58check'); -var Hash = require('./crypto/hash'); +var errors = require('./errors'); var Network = require('./networks'); -var Point = require('./crypto/point'); var PrivateKey = require('./privatekey'); -var Random = require('./crypto/random'); -var errors = require('./errors'); var hdErrors = errors.HDPrivateKey; var BufferUtil = require('./util/buffer'); var JSUtil = require('./util/js'); +var $ = require('./util/preconditions'); var MINIMUM_ENTROPY_BITS = 128; var BITS_TO_BYTES = 1 / 8; @@ -73,7 +72,7 @@ function HDPrivateKey(arg) { */ HDPrivateKey.isValidPath = function(arg, hardened) { if (_.isString(arg)) { - var indexes = HDPrivateKey._getDerivationIndexes(arg); + const indexes = HDPrivateKey._getDerivationIndexes(arg); return indexes !== null && _.every(indexes, HDPrivateKey.isValidPath); } @@ -96,7 +95,7 @@ HDPrivateKey.isValidPath = function(arg, hardened) { * @return {Array} */ HDPrivateKey._getDerivationIndexes = function(path) { - var steps = path.split('/'); + const steps = path.split('/'); // Special cases: if (_.includes(HDPrivateKey.RootElementAlias, path)) { @@ -107,15 +106,15 @@ HDPrivateKey._getDerivationIndexes = function(path) { return null; } - var indexes = steps.slice(1).map(function(step) { - var isHardened = step.slice(-1) === '\''; + const indexes = steps.slice(1).map(function(step) { + const isHardened = step.slice(-1) === '\''; if (isHardened) { step = step.slice(0, -1); } if (!step || step[0] === '-') { return NaN; } - var index = +step; // cast to number + let index = +step; // cast to number if (isHardened) { index += HDPrivateKey.Hardened; } @@ -233,28 +232,28 @@ HDPrivateKey.prototype._deriveWithNumber = function(index, hardened, nonComplian index += HDPrivateKey.Hardened; } - var indexBuffer = BufferUtil.integerAsBuffer(index); - var data; + const indexBuffer = BufferUtil.integerAsBuffer(index); + let data; if (hardened && nonCompliant) { // The private key serialization in this case will not be exactly 32 bytes and can be // any value less, and the value is not zero-padded. - var nonZeroPadded = this.privateKey.bn.toBuffer(); + const nonZeroPadded = this.privateKey.bn.toBuffer(); data = BufferUtil.concat([Buffer.from([0]), nonZeroPadded, indexBuffer]); } else if (hardened) { // This will use a 32 byte zero padded serialization of the private key - var privateKeyBuffer = this.privateKey.bn.toBuffer({size: 32}); + const privateKeyBuffer = this.privateKey.bn.toBuffer({ size: 32 }); assert(privateKeyBuffer.length === 32, 'length of private key buffer is expected to be 32 bytes'); data = BufferUtil.concat([Buffer.from([0]), privateKeyBuffer, indexBuffer]); } else { data = BufferUtil.concat([this.publicKey.toBuffer(), indexBuffer]); } - var hash = Hash.sha512hmac(data, this._buffers.chainCode); - var leftPart = BN.fromBuffer(hash.slice(0, 32), { + const hash = Hash.sha512hmac(data, this._buffers.chainCode); + const leftPart = BN.fromBuffer(hash.slice(0, 32), { size: 32 }); - var chainCode = hash.slice(32, 64); + const chainCode = hash.slice(32, 64); - var privateKey = leftPart.add(this.privateKey.toBigNumber()).umod(Point.getN()).toBuffer({ + const privateKey = leftPart.add(this.privateKey.toBigNumber()).umod(Point.getN()).toBuffer({ size: 32 }); @@ -263,7 +262,7 @@ HDPrivateKey.prototype._deriveWithNumber = function(index, hardened, nonComplian return this._deriveWithNumber(index + 1, null, nonCompliant); } - var derived = new HDPrivateKey({ + const derived = new HDPrivateKey({ network: this.network, depth: this.depth + 1, parentFingerPrint: this.fingerPrint, @@ -280,8 +279,8 @@ HDPrivateKey.prototype._deriveFromString = function(path, nonCompliant) { throw new hdErrors.InvalidPath(path); } - var indexes = HDPrivateKey._getDerivationIndexes(path); - var derived = indexes.reduce(function(prev, index) { + const indexes = HDPrivateKey._getDerivationIndexes(path); + const derived = indexes.reduce(function(prev, index) { return prev._deriveWithNumber(index, null, nonCompliant); }, this); @@ -327,7 +326,7 @@ HDPrivateKey.getSerializedError = function(data, network) { return new hdErrors.InvalidLength(data); } if (!_.isUndefined(network)) { - var error = HDPrivateKey._validateNetwork(data, network); + const error = HDPrivateKey._validateNetwork(data, network); if (error) { return error; } @@ -336,11 +335,11 @@ HDPrivateKey.getSerializedError = function(data, network) { }; HDPrivateKey._validateNetwork = function(data, networkArg) { - var network = Network.get(networkArg); + const network = Network.get(networkArg); if (!network) { return new errors.InvalidNetworkArgument(networkArg); } - var version = data.slice(0, 4); + const version = data.slice(0, 4); if (BufferUtil.integerFromBuffer(version) !== network.xprivkey) { return new errors.InvalidNetwork(version); } @@ -364,21 +363,21 @@ HDPrivateKey.prototype._buildFromJSON = function(arg) { HDPrivateKey.prototype._buildFromObject = function(arg) { /* jshint maxcomplexity: 12 */ // TODO: Type validation - var buffers = { + const buffers = { version: arg.network ? BufferUtil.integerAsBuffer(Network.get(arg.network).xprivkey) : arg.version, depth: _.isNumber(arg.depth) ? BufferUtil.integerAsSingleByteBuffer(arg.depth) : arg.depth, parentFingerPrint: _.isNumber(arg.parentFingerPrint) ? BufferUtil.integerAsBuffer(arg.parentFingerPrint) : arg.parentFingerPrint, childIndex: _.isNumber(arg.childIndex) ? BufferUtil.integerAsBuffer(arg.childIndex) : arg.childIndex, - chainCode: _.isString(arg.chainCode) ? Buffer.from(arg.chainCode,'hex') : arg.chainCode, - privateKey: (_.isString(arg.privateKey) && JSUtil.isHexa(arg.privateKey)) ? Buffer.from(arg.privateKey,'hex') : arg.privateKey, + chainCode: _.isString(arg.chainCode) ? Buffer.from(arg.chainCode, 'hex') : arg.chainCode, + privateKey: (_.isString(arg.privateKey) && JSUtil.isHexa(arg.privateKey)) ? Buffer.from(arg.privateKey, 'hex') : arg.privateKey, checksum: arg.checksum ? (arg.checksum.length ? arg.checksum : BufferUtil.integerAsBuffer(arg.checksum)) : undefined }; return this._buildFromBuffers(buffers); }; HDPrivateKey.prototype._buildFromSerialized = function(arg) { - var decoded = Base58Check.decode(arg); - var buffers = { + const decoded = Base58Check.decode(arg); + const buffers = { version: decoded.slice(HDPrivateKey.VersionStart, HDPrivateKey.VersionEnd), depth: decoded.slice(HDPrivateKey.DepthStart, HDPrivateKey.DepthEnd), parentFingerPrint: decoded.slice(HDPrivateKey.ParentFingerPrintStart, @@ -431,7 +430,7 @@ HDPrivateKey.fromSeed = function(hexa, network) { HDPrivateKey.prototype._calcHDPublicKey = function() { if (!this._hdPublicKey) { - var HDPublicKey = require('./hdpublickey'); + const HDPublicKey = require('./hdpublickey'); this._hdPublicKey = new HDPublicKey(this); } }; @@ -462,11 +461,11 @@ HDPrivateKey.prototype._buildFromBuffers = function(arg) { _buffers: arg }); - var sequence = [ + const sequence = [ arg.version, arg.depth, arg.parentFingerPrint, arg.childIndex, arg.chainCode, BufferUtil.emptyBuffer(1), arg.privateKey ]; - var concat = buffer.Buffer.concat(sequence); + const concat = buffer.Buffer.concat(sequence); if (!arg.checksum || !arg.checksum.length) { arg.checksum = Base58Check.checksum(concat); } else { @@ -475,15 +474,15 @@ HDPrivateKey.prototype._buildFromBuffers = function(arg) { } } - var network = Network.get(BufferUtil.integerFromBuffer(arg.version)); - var xprivkey; + const network = Network.get(BufferUtil.integerFromBuffer(arg.version)); + let xprivkey; xprivkey = Base58Check.encode(buffer.Buffer.concat(sequence)); arg.xprivkey = Buffer.from(xprivkey); - var privateKey = new PrivateKey(BN.fromBuffer(arg.privateKey), network); - var publicKey = privateKey.toPublicKey(); - var size = HDPrivateKey.ParentFingerPrintSize; - var fingerPrint = Hash.sha256ripemd160(publicKey.toBuffer()).slice(0, size); + const privateKey = new PrivateKey(BN.fromBuffer(arg.privateKey), network); + const publicKey = privateKey.toPublicKey(); + const size = HDPrivateKey.ParentFingerPrintSize; + const fingerPrint = Hash.sha256ripemd160(publicKey.toBuffer()).slice(0, size); JSUtil.defineImmutable(this, { xprivkey: xprivkey, @@ -516,8 +515,8 @@ HDPrivateKey.prototype._buildFromBuffers = function(arg) { }; HDPrivateKey._validateBufferArguments = function(arg) { - var checkBuffer = function(name, size) { - var buff = arg[name]; + const checkBuffer = function(name, size) { + const buff = arg[name]; assert(BufferUtil.isBuffer(buff), name + ' argument is not a buffer'); assert( buff.length === size, @@ -586,6 +585,20 @@ HDPrivateKey.prototype.toObject = HDPrivateKey.prototype.toJSON = function toObj }; }; +HDPrivateKey.prototype.toObjectWithBufferPrivateKey = function toObjectWithBufferPrivateKey() { + return { + network: Network.get(BufferUtil.integerFromBuffer(this._buffers.version), 'xprivkey').name, + depth: BufferUtil.integerFromSingleByteBuffer(this._buffers.depth), + fingerPrint: BufferUtil.integerFromBuffer(this.fingerPrint), + parentFingerPrint: BufferUtil.integerFromBuffer(this._buffers.parentFingerPrint), + childIndex: BufferUtil.integerFromBuffer(this._buffers.childIndex), + chainCode: BufferUtil.bufferToHex(this._buffers.chainCode), + privateKey: this.privateKey.toBuffer(), + checksum: BufferUtil.integerFromBuffer(this._buffers.checksum), + xprivkey: this.xprivkey + }; +}; + /** * Build a HDPrivateKey from a buffer * diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index 97a50a81560..031e8af451d 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -33,6 +33,25 @@ export abstract class AbstractBitcoreLibDeriver implements IDeriver { pubKey = new this.bitcoreLib.PublicKey(pubKey); return new this.bitcoreLib.Address(pubKey, network, addressType).toString(); } + + /** + * @returns {Buffer} raw secpk1 private key buffer (32 bytes, big-endian) + * @throws {Error} If privKey is not a Buffer (planned forwards compatibility) or string. Propagates all other errors + */ + privateKeyToBuffer(privKey: Buffer | string): Buffer { + if (Buffer.isBuffer(privKey)) return privKey; // forward compatibility + if (typeof privKey !== 'string') throw new Error(`Expected key to be a string, got ${typeof privKey}`); + + const key = new this.bitcoreLib.PrivateKey(privKey); + return key.toBuffer(); + } + + privateKeyBufferToNativePrivateKey(buf: Buffer, network: string): any { + // force compressed WIF without mutating instances + const bn = this.bitcoreLib.crypto.BN.fromBuffer(buf); + const key = new this.bitcoreLib.PrivateKey({ bn, network, compressed: true }); + return key.toWIF(); + } } export class BtcDeriver extends AbstractBitcoreLibDeriver { bitcoreLib = BitcoreLib; diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index 49e5b255317..c9f2b12d709 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -55,4 +55,23 @@ export class EthDeriver implements IDeriver { pubKey = new BitcoreLib.PublicKey(pubKey, network); // network not needed here since ETH doesn't differentiate addresses by network. return this.addressFromPublicKeyBuffer(pubKey.toBuffer()); } + + /** + * @param {Buffer | string} privKey - expects hex-encoded string, as returned from EthDeriver.derivePrivateKey + * @returns {Buffer} + * @throws {Error} If privKey is not a Buffer (planned forwards compatibility) or string. Propagates all other errors + */ + privateKeyToBuffer(privKey: Buffer | string): Buffer { + if (Buffer.isBuffer(privKey)) return privKey; + if (typeof privKey !== 'string') throw new Error(`Expected string, got ${typeof privKey}`); + if (privKey.startsWith('0x')) { + privKey = privKey.slice(2); + }; + // Expects to match return from derivePrivateKey's privKey. + return Buffer.from(privKey, 'hex'); + } + + privateKeyBufferToNativePrivateKey(buf: Buffer, _network: string): any { + return buf.toString('hex'); + } } diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index 2e61381f761..d9afe74bab0 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -119,6 +119,14 @@ export class DeriverProxy { return Paths.BTC.default + accountStr; } } + + privateKeyToBuffer(chain, privateKey: Buffer | string): Buffer { + return this.get(chain).privateKeyToBuffer(privateKey); + } + + privateKeyBufferToNativePrivateKey(chain: string, network: string, buf: Buffer): any { + return this.get(chain).privateKeyBufferToNativePrivateKey(buf, network); + } } export default new DeriverProxy(); diff --git a/packages/crypto-wallet-core/src/derivation/sol/index.ts b/packages/crypto-wallet-core/src/derivation/sol/index.ts index 178cc186f71..ea90c9d8aff 100644 --- a/packages/crypto-wallet-core/src/derivation/sol/index.ts +++ b/packages/crypto-wallet-core/src/derivation/sol/index.ts @@ -54,4 +54,20 @@ export class SolDeriver implements IDeriver { pubKey: Buffer.from(pubKey).toString('hex') } as Key; }; + + /** + * @param {Buffer | string} privKey - expects base 58 encoded string + * @returns {Buffer} + * @throws {Error} If privKey is not a Buffer (planned forwards compatibility) or string. Propagates all other errors + */ + privateKeyToBuffer(privKey: Buffer | string): Buffer { + if (Buffer.isBuffer(privKey)) return privKey; + if (typeof privKey !== 'string') throw new Error(`Expected string, got ${typeof privKey}`); + // Expects to match return from derivePrivateKey's privKey. + return encoding.Base58.decode(privKey); + } + + privateKeyBufferToNativePrivateKey(buf: Buffer, _network: string): any { + return encoding.Base58.encode(buf); + } } \ No newline at end of file diff --git a/packages/crypto-wallet-core/src/derivation/xrp/index.ts b/packages/crypto-wallet-core/src/derivation/xrp/index.ts index fde15598780..58e77740296 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -35,4 +35,20 @@ export class XrpDeriver implements IDeriver { const address = deriveAddress(pubKey); return address; } + + /** + * @param {Buffer | string} privKey - expects hex-encoded string, as returned from XrpDeriver.derivePrivateKey privKey + * @returns {Buffer} + * @throws {Error} If privKey is not a Buffer (planned forwards compatibility) or string. Propagates all other errors + */ + privateKeyToBuffer(privKey: Buffer | string): Buffer { + if (Buffer.isBuffer(privKey)) return privKey; + if (typeof privKey !== 'string') throw new Error(`Expected string, got ${typeof privKey}`); + // Expects to match return from derivePrivateKey's privKey. + return Buffer.from(privKey, 'hex'); + } + + privateKeyBufferToNativePrivateKey(buf: Buffer, _network: string): any { + return buf.toString('hex').toUpperCase(); + } } diff --git a/packages/crypto-wallet-core/src/types/derivation.ts b/packages/crypto-wallet-core/src/types/derivation.ts index 1ada0ffccd7..56d4348e5ac 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -14,4 +14,14 @@ export interface IDeriver { derivePrivateKeyWithPath(network: string, xprivKey: string, path: string, addressType: string): Key; getAddress(network: string, pubKey, addressType: string): string; + + /** + * Used to normalize output of Key.privKey + */ + privateKeyToBuffer(privKey: any): Buffer; + + /** + * Temporary - converts decrypted private key buffer to chain-native private key format + */ + privateKeyBufferToNativePrivateKey(buf: Buffer, network: string): any; } \ No newline at end of file