From 0e5e6b4bfff9fefbad26e048e2d098c4d6fd0c62 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 12:45:00 -0500 Subject: [PATCH 01/11] add privateKeyToBuffer to IDeriver, implement method for concrete classes, & expose method with DeriverProxy --- .../crypto-wallet-core/src/derivation/btc/index.ts | 12 ++++++++++++ .../crypto-wallet-core/src/derivation/eth/index.ts | 12 ++++++++++++ .../crypto-wallet-core/src/derivation/index.ts | 4 ++++ .../crypto-wallet-core/src/derivation/sol/index.ts | 14 ++++++++++++++ .../crypto-wallet-core/src/derivation/xrp/index.ts | 12 ++++++++++++ .../crypto-wallet-core/src/types/derivation.ts | 5 +++++ 6 files changed, 59 insertions(+) diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index 97a50a81560..934414f59e4 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -33,6 +33,18 @@ 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: any): 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(); + } } 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 1a1d9389b34..5b31785031e 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -55,4 +55,16 @@ 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 {any} 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: any): 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'); + } } diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index 2e61381f761..83b02c03e02 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -119,6 +119,10 @@ export class DeriverProxy { return Paths.BTC.default + accountStr; } } + + privateKeyToBuffer(chain, network, privateKey: any): Buffer { + return this.get(chain).privateKeyToBuffer(privateKey); + } } 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..2e24a9a3ba8 100644 --- a/packages/crypto-wallet-core/src/derivation/sol/index.ts +++ b/packages/crypto-wallet-core/src/derivation/sol/index.ts @@ -54,4 +54,18 @@ export class SolDeriver implements IDeriver { pubKey: Buffer.from(pubKey).toString('hex') } as Key; }; + + /** + * @param {any} 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 + * + * TODO + */ + privateKeyToBuffer(privKey: any): 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); + } } \ 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..009cc38a507 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -35,4 +35,16 @@ export class XrpDeriver implements IDeriver { const address = deriveAddress(pubKey); return address; } + + /** + * @param {any} 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: any): 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'); + } } diff --git a/packages/crypto-wallet-core/src/types/derivation.ts b/packages/crypto-wallet-core/src/types/derivation.ts index 1ada0ffccd7..7fa5a000840 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -14,4 +14,9 @@ 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; } \ No newline at end of file From b63ad0f097a74394996ed6dd408289e4ee6b244f Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 12:46:13 -0500 Subject: [PATCH 02/11] And encryptBuffer & decryptToBuffer pairs to Encryption class. --- packages/bitcore-client/src/encryption.ts | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/bitcore-client/src/encryption.ts b/packages/bitcore-client/src/encryption.ts index 7579477d3f0..f63aa9bc8f6 100644 --- a/packages/bitcore-client/src/encryption.ts +++ b/packages/bitcore-client/src/encryption.ts @@ -50,6 +50,29 @@ function decryptPrivateKey(encPrivateKey: string, pubKey: string, encryptionKey: return decrypted; } +function encryptBuffer(data: Buffer, pubKey: string, encryptionKey: string): Buffer { + const key = Buffer.from(encryptionKey, 'hex'); + const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); + const cipher = crypto.createCipheriv(algo, key, iv); + return Buffer.concat([cipher.update(data), cipher.final()]); +} + +function decryptToBuffer(encHex: string, pubKey: string, encryptionKey: string): Buffer { + const key = Buffer.from(encryptionKey, 'hex'); + const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); + const decipher = crypto.createDecipheriv(algo, key, iv); + + const decrypted = decipher.update(encHex, 'hex'); + const final = decipher.final(); + if (final.length) { + const out = Buffer.concat([decrypted, final]); + decrypted.fill(0); + final.fill(0); + return out; + } + return decrypted; +} + 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 +157,8 @@ export const Encryption = { decryptEncryptionKey, encryptPrivateKey, decryptPrivateKey, + encryptBuffer, + decryptToBuffer, generateEncryptionKey, bitcoinCoreDecrypt }; From 01215faf3cf3027bfd69409ac681b4acc191a3a0 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 13:55:57 -0500 Subject: [PATCH 03/11] adds Storage.addKeysSafe method & implements private-key encrypted writes for wallets --- packages/bitcore-client/src/storage.ts | 26 ++++++++++ packages/bitcore-client/src/wallet.ts | 69 ++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/bitcore-client/src/storage.ts b/packages/bitcore-client/src/storage.ts index ca43ba97198..370e2818cfc 100644 --- a/packages/bitcore-client/src/storage.ts +++ b/packages/bitcore-client/src/storage.ts @@ -180,6 +180,32 @@ export class Storage { } } + async addKeysSafe(params: { name: string; keys: KeyImport[]; encryptionKey: string }) { + const { name, keys, encryptionKey } = params; + let open = true; + for (const key of keys) { + const { path } = key; + const pubKey = key.pubKey; + // addKeysSafe operates on KeyImports whose privKeys are encrypted. If pubKey + if (!pubKey) { + throw new Error(`pubKey is undefined for ${name}. Keys not added to storage`); + } + let payload = {}; + if (pubKey && key.privKey && encryptionKey) { + const toEncrypt = JSON.stringify(key); + const encKey = Encryption.encryptPrivateKey(toEncrypt, pubKey, encryptionKey); + payload = { encKey, pubKey, path }; + } + const toStore = JSON.stringify(payload); + let keepAlive = true; + if (key === keys[keys.length - 1]) { + keepAlive = false; + } + await this.storageType.addKeys({ name, key, toStore, keepAlive, open }); + open = false; + } + } + async getAddress(params: { name: string; address: string }) { const { name, address } = params; return this.storageType.getAddress({ name, address, keepAlive: true, open: true }); diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 1f27282d643..685b5eecf02 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -39,6 +39,7 @@ const chainLibs = { export interface IWalletExt extends IWallet { storage?: Storage; + version?: 2; // Wallet versioning used for backwards compatibility } export class Wallet { @@ -64,6 +65,7 @@ export class Wallet { lite: boolean; addressType: string; addressZero: string; + version?: number; // If 2, master key xprivkey and privateKey are encrypted and serialized BEFORE static XrpAccountFlags = xrpl.AccountSetTfFlags; @@ -159,10 +161,6 @@ export class Wallet { } const privKeyObj = hdPrivKey.toObject(); - // Generate authentication keys - const authKey = new PrivateKey(); - const authPubKey = authKey.toPublicKey().toString(); - // Generate public keys // bip44 compatible pubKey const pubKey = hdPrivKey.publicKey.toString(); @@ -170,6 +168,17 @@ export class Wallet { // Generate and encrypt the encryption key and private key const walletEncryptionKey = Encryption.generateEncryptionKey(); const encryptionKey = Encryption.encryptEncryptionKey(walletEncryptionKey, password); + + // Encrypt privKeyObj.privateKey & privKeyObj.xprivkey + const xprivBuffer = BitcoreLib.encoding.Base58Check.decode(privKeyObj.xprivkey); + privKeyObj.xprivkey = Encryption.encryptBuffer(xprivBuffer, pubKey, encryptionKey).toString('hex'); + privKeyObj.privateKey = Encryption.encryptBuffer(Buffer.from(privKeyObj.privateKey, 'hex'), pubKey, encryptionKey).toString('hex'); + + // Generate authentication keys + const authKey = new PrivateKey(); + const authPubKey = authKey.toPublicKey().toString(); + + // Generate and encrypt the encryption key and private key const encPrivateKey = Encryption.encryptPrivateKey(JSON.stringify(privKeyObj), pubKey, walletEncryptionKey); storageType = storageType ? storageType : 'Level'; @@ -207,7 +216,8 @@ export class Wallet { storageType, lite, addressType, - addressZero: null + addressZero: null, + version: 2, } as IWalletExt); // save wallet to storage and then bitcore-node @@ -294,7 +304,24 @@ export class Wallet { if (!this.lite) { const encMasterKey = this.masterKey; const masterKeyStr = await Encryption.decryptPrivateKey(encMasterKey, this.pubKey, encryptionKey); + // masterKey.xprivkey & masterKey.privateKey are encrypted with encryptionKey masterKey = JSON.parse(masterKeyStr); + + if (this.version === 2) { + /** + * Phase 1 implementation of string-based secrets clean-up (Dec 10, 2025): + * Maintain buffers until last possible moment while maintaining prior boundary + * + * Phase 2 should update call site to propagate buffer usage outwards to enable buffer cleanup upon completion + */ + const decryptedxprivBuffer = Encryption.decryptToBuffer(masterKey.xpriv, this.pubKey, this.unlocked.encryptionKey); + masterKey.xpriv = BitcoreLib.encoding.Base58Check.encode(decryptedxprivBuffer); + decryptedxprivBuffer.fill(0); + + const decryptedPrivKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, this.unlocked.encryptionKey); + masterKey.privateKey = decryptedPrivKey.toString(); + decryptedPrivKey.fill(0); + } } this.unlocked = { encryptionKey, @@ -611,13 +638,35 @@ export class Wallet { address: key.pubKey ? Deriver.getAddress(this.chain, this.network, key.pubKey, this.addressType) : key.address }) as KeyImport); } + + /** + * Phase 1: Encrypt key.privKey at boundary + */ + if (this.version === 2) { + // todo: encrypt key.privKey + for (const key of keysToSave) { + // The goal here is to make it so when the key is retrieved, it's uniform + const privKeyBuffer = Deriver.privateKeyToBuffer(key.privKey); + key.privKey = Encryption.encryptBuffer(privKeyBuffer, this.pubKey, encryptionKey).toString('hex'); + privKeyBuffer.fill(0); + } + } if (keysToSave.length) { - await this.storage.addKeys({ - keys: keysToSave, - encryptionKey, - name: this.name - }); + if (this.version === 2) { + await this.storage.addKeysSafe({ + keys: keysToSave, + encryptionKey, + name: this.name + }); + } else { + // Backwards compatibility + await this.storage.addKeys({ + keys: keysToSave, + encryptionKey, + name: this.name + }); + } } const addedAddresses = keys.map(key => { return { address: key.address }; From 4bf659cf8ed876d68bf0bf0f94834f301dc0f842 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 14:38:56 -0500 Subject: [PATCH 04/11] fix Deriver.privateKeyToBuffer call --- packages/bitcore-client/src/wallet.ts | 6 +++++- packages/crypto-wallet-core/src/derivation/index.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 685b5eecf02..7505418a68d 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -646,7 +646,7 @@ export class Wallet { // todo: encrypt key.privKey for (const key of keysToSave) { // The goal here is to make it so when the key is retrieved, it's uniform - const privKeyBuffer = Deriver.privateKeyToBuffer(key.privKey); + const privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, key.privKey); key.privKey = Encryption.encryptBuffer(privKeyBuffer, this.pubKey, encryptionKey).toString('hex'); privKeyBuffer.fill(0); } @@ -716,6 +716,10 @@ export class Wallet { await this.importKeys({ keys: [changeKey] }); } + // if (this.version === 2) { + + // } + const payload = { chain: this.chain, network: this.network, diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index 83b02c03e02..321390fe9a5 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -120,7 +120,7 @@ export class DeriverProxy { } } - privateKeyToBuffer(chain, network, privateKey: any): Buffer { + privateKeyToBuffer(chain, privateKey: any): Buffer { return this.get(chain).privateKeyToBuffer(privateKey); } } From 2bb39e5169fcefee6d208ff8f73143388156c9f9 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 15:24:40 -0500 Subject: [PATCH 05/11] implement privateKeyBuffertoNativePrivateKey on IDeriver concrete classes and add passthrough on DeriverProxy --- packages/crypto-wallet-core/src/derivation/btc/index.ts | 4 ++++ packages/crypto-wallet-core/src/derivation/eth/index.ts | 4 ++++ packages/crypto-wallet-core/src/derivation/index.ts | 4 ++++ packages/crypto-wallet-core/src/derivation/sol/index.ts | 4 ++++ packages/crypto-wallet-core/src/derivation/xrp/index.ts | 4 ++++ packages/crypto-wallet-core/src/types/derivation.ts | 5 +++++ 6 files changed, 25 insertions(+) diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index 934414f59e4..75ee8c51e31 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -45,6 +45,10 @@ export abstract class AbstractBitcoreLibDeriver implements IDeriver { const key = new this.bitcoreLib.PrivateKey(privKey); return key.toBuffer(); } + + privateKeyBufferToNativePrivateKey(buf: Buffer, network: string): any { + return this.bitcoreLib.PrivateKey.fromBuffer(buf, network).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 5b31785031e..3f0c5245680 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -67,4 +67,8 @@ export class EthDeriver implements IDeriver { // 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 321390fe9a5..65e7d51bfa4 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -123,6 +123,10 @@ export class DeriverProxy { privateKeyToBuffer(chain, privateKey: any): 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 2e24a9a3ba8..fcc3e243d28 100644 --- a/packages/crypto-wallet-core/src/derivation/sol/index.ts +++ b/packages/crypto-wallet-core/src/derivation/sol/index.ts @@ -68,4 +68,8 @@ export class SolDeriver implements IDeriver { // 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 009cc38a507..59f69a60ce9 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -47,4 +47,8 @@ export class XrpDeriver implements IDeriver { // 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 7fa5a000840..56d4348e5ac 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -19,4 +19,9 @@ export interface IDeriver { * 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 From 23ba0e3256558dd5fc3434a706bbda8881480e05 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 15:39:10 -0500 Subject: [PATCH 06/11] add backwards-compatible attempt to decrypt key.privKey & serialize it to expected form --- packages/bitcore-client/src/wallet.ts | 28 +++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 7505418a68d..5c1c970921a 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -716,16 +716,36 @@ export class Wallet { await this.importKeys({ keys: [changeKey] }); } - // if (this.version === 2) { + // Shallow copy to avoid mutation if signingKeys are passed in + const keysForSigning = [...(signingKeys || decryptedKeys)]; - // } + if (this.version === 2) { + /** + * Phase 1: Convert encrypted private keys directly to strings as required by Transactions.sign (as of Dec 11, 2025) + * This mitigates the security improvement, but also removes the requirement for changing Transaction.sign fully immediately + */ + for (const key of keysForSigning) { + // In Phase 2, this would be passed directly to Transaction.sign in a try/finally, which will fill(0) + let privKeyBuf: Buffer | undefined; + try { + privKeyBuf = Encryption.decryptToBuffer(key.privKey, this.pubKey, this.unlocked.encryptionKey); + key.privKey = Deriver.privateKeyBufferToNativePrivateKey(this.chain, this.network, privKeyBuf); + } catch { + continue; + } finally { + 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 }); From dfe9aca99415346baf81ec2f5b7d78c4e8c19ea1 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 16:34:05 -0500 Subject: [PATCH 07/11] fixed backwards compat issue and wrote tests to backwards compat --- packages/bitcore-client/src/wallet.ts | 25 ++-- .../bitcore-client/test/unit/wallet.test.ts | 127 +++++++++++++++++- 2 files changed, 136 insertions(+), 16 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 5c1c970921a..5a4f5aba934 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -39,7 +39,7 @@ const chainLibs = { export interface IWalletExt extends IWallet { storage?: Storage; - version?: 2; // Wallet versioning used for backwards compatibility + version?: 0 | 2; // Wallet versioning used for backwards compatibility } export class Wallet { @@ -122,7 +122,8 @@ export class Wallet { storageType: this.storageType, lite, addressType: this.addressType, - addressZero: this.addressZero + addressZero: this.addressZero, + version: this.version }; } @@ -136,6 +137,8 @@ export class Wallet { static async create(params: Partial) { const { network, name, phrase, xpriv, password, path, lite, baseUrl } = params; let { chain, storageType, storage, addressType } = params; + // For create: allow explicit 0 to signal legacy (undefined). Everything else defaults to v2. + const version = params.version === 0 ? undefined : 2; if (phrase && xpriv) { throw new Error('You can only provide either a phrase or a xpriv, not both'); } @@ -166,13 +169,15 @@ export class Wallet { const pubKey = hdPrivKey.publicKey.toString(); // Generate and encrypt the encryption key and private key - const walletEncryptionKey = Encryption.generateEncryptionKey(); - const encryptionKey = Encryption.encryptEncryptionKey(walletEncryptionKey, password); - - // Encrypt privKeyObj.privateKey & privKeyObj.xprivkey - const xprivBuffer = BitcoreLib.encoding.Base58Check.decode(privKeyObj.xprivkey); - privKeyObj.xprivkey = Encryption.encryptBuffer(xprivBuffer, pubKey, encryptionKey).toString('hex'); - privKeyObj.privateKey = Encryption.encryptBuffer(Buffer.from(privKeyObj.privateKey, 'hex'), pubKey, encryptionKey).toString('hex'); + const walletEncryptionKey = Encryption.generateEncryptionKey().toString('hex'); // raw 32-byte key as hex + const encryptionKey = Encryption.encryptEncryptionKey(walletEncryptionKey, password); // stored, password-wrapped + + // Encrypt privKeyObj.privateKey & privKeyObj.xprivkey (only for v2) + if (version === 2) { + const xprivBuffer = BitcoreLib.encoding.Base58Check.decode(privKeyObj.xprivkey); + privKeyObj.xprivkey = Encryption.encryptBuffer(xprivBuffer, pubKey, walletEncryptionKey).toString('hex'); + privKeyObj.privateKey = Encryption.encryptBuffer(Buffer.from(privKeyObj.privateKey, 'hex'), pubKey, walletEncryptionKey).toString('hex'); + } // Generate authentication keys const authKey = new PrivateKey(); @@ -217,7 +222,7 @@ export class Wallet { lite, addressType, addressZero: null, - version: 2, + version, } as IWalletExt); // save wallet to storage and then bitcore-node 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 = [ From bb01d246c7b754e0300992f33f696977d27f7774 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 16:52:31 -0500 Subject: [PATCH 08/11] bug fix --- packages/bitcore-client/src/wallet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 5a4f5aba934..244f6d017f3 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -319,11 +319,11 @@ export class Wallet { * * Phase 2 should update call site to propagate buffer usage outwards to enable buffer cleanup upon completion */ - const decryptedxprivBuffer = Encryption.decryptToBuffer(masterKey.xpriv, this.pubKey, this.unlocked.encryptionKey); - masterKey.xpriv = BitcoreLib.encoding.Base58Check.encode(decryptedxprivBuffer); + const decryptedxprivBuffer = Encryption.decryptToBuffer(masterKey.xprivkey, this.pubKey, encryptionKey); + masterKey.xprivkey = BitcoreLib.encoding.Base58Check.encode(decryptedxprivBuffer); decryptedxprivBuffer.fill(0); - const decryptedPrivKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, this.unlocked.encryptionKey); + const decryptedPrivKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, encryptionKey); masterKey.privateKey = decryptedPrivKey.toString(); decryptedPrivKey.fill(0); } From 5b8d5c3a93041ad9e2f9bbd63faa745867f79f6e Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 17:12:24 -0500 Subject: [PATCH 09/11] fix BTCDeriver private key buffer to native private key --- packages/crypto-wallet-core/src/derivation/btc/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index 75ee8c51e31..b61c3aed395 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -47,7 +47,10 @@ export abstract class AbstractBitcoreLibDeriver implements IDeriver { } privateKeyBufferToNativePrivateKey(buf: Buffer, network: string): any { - return this.bitcoreLib.PrivateKey.fromBuffer(buf, network).toWIF(); + // 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 { From e1b901e3bfa59cebe4c1bf2ec8036c248547ed55 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Mon, 15 Dec 2025 17:00:52 -0500 Subject: [PATCH 10/11] fix Wallet.unlock buffer to string conversion --- packages/bitcore-client/src/storage.ts | 2 +- packages/bitcore-client/src/wallet.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bitcore-client/src/storage.ts b/packages/bitcore-client/src/storage.ts index 370e2818cfc..17baa1e5969 100644 --- a/packages/bitcore-client/src/storage.ts +++ b/packages/bitcore-client/src/storage.ts @@ -186,7 +186,7 @@ export class Storage { for (const key of keys) { const { path } = key; const pubKey = key.pubKey; - // addKeysSafe operates on KeyImports whose privKeys are encrypted. If 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`); } diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 244f6d017f3..d42a52e558c 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -320,11 +320,11 @@ export class Wallet { * Phase 2 should update call site to propagate buffer usage outwards to enable buffer cleanup upon completion */ const decryptedxprivBuffer = Encryption.decryptToBuffer(masterKey.xprivkey, this.pubKey, encryptionKey); - masterKey.xprivkey = BitcoreLib.encoding.Base58Check.encode(decryptedxprivBuffer); + masterKey.xprivkey = decryptedxprivBuffer.toString('hex'); decryptedxprivBuffer.fill(0); const decryptedPrivKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, encryptionKey); - masterKey.privateKey = decryptedPrivKey.toString(); + masterKey.privateKey = decryptedPrivKey.toString('hex'); decryptedPrivKey.fill(0); } } From 2670f11706eba004c5776f887f74335a1d031798 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 16 Dec 2025 09:57:36 -0500 Subject: [PATCH 11/11] add another backwards compatibility check for bitcoin core wallet --- packages/bitcore-client/src/wallet.ts | 40 +++++++++++++++------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index d42a52e558c..4d65818ed78 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -696,24 +696,28 @@ 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.getKeys({ + addresses, + name: this.name, + encryptionKey: this.unlocked.encryptionKey + }); + } 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) @@ -724,7 +728,7 @@ export class Wallet { // Shallow copy to avoid mutation if signingKeys are passed in const keysForSigning = [...(signingKeys || decryptedKeys)]; - if (this.version === 2) { + if (this.version === 2 && decryptPrivateKeys) { /** * Phase 1: Convert encrypted private keys directly to strings as required by Transactions.sign (as of Dec 11, 2025) * This mitigates the security improvement, but also removes the requirement for changing Transaction.sign fully immediately