From 0e5e6b4bfff9fefbad26e048e2d098c4d6fd0c62 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 12:45:00 -0500 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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 From 1b61909f92d7ae776310dbd8cdfefa7cb66fe868 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Mon, 26 Jan 2026 10:08:11 -0500 Subject: [PATCH 12/22] update IDeriver and implementation classes' privaetKeyToBuffer method signature --- packages/crypto-wallet-core/src/derivation/btc/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/eth/index.ts | 7 +++++-- packages/crypto-wallet-core/src/derivation/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/sol/index.ts | 6 ++---- packages/crypto-wallet-core/src/derivation/xrp/index.ts | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index b61c3aed395..031e8af451d 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -38,7 +38,7 @@ export abstract class AbstractBitcoreLibDeriver implements IDeriver { * @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 { + 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}`); diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index 3f0c5245680..57481b2e3f1 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -57,13 +57,16 @@ export class EthDeriver implements IDeriver { } /** - * @param {any} privKey - expects hex-encoded string, as returned from EthDeriver.derivePrivateKey + * @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: any): Buffer { + 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'); } diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index 65e7d51bfa4..d9afe74bab0 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, privateKey: any): Buffer { + privateKeyToBuffer(chain, privateKey: Buffer | string): Buffer { return this.get(chain).privateKeyToBuffer(privateKey); } diff --git a/packages/crypto-wallet-core/src/derivation/sol/index.ts b/packages/crypto-wallet-core/src/derivation/sol/index.ts index fcc3e243d28..ea90c9d8aff 100644 --- a/packages/crypto-wallet-core/src/derivation/sol/index.ts +++ b/packages/crypto-wallet-core/src/derivation/sol/index.ts @@ -56,13 +56,11 @@ export class SolDeriver implements IDeriver { }; /** - * @param {any} privKey - expects base 58 encoded string + * @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 - * - * TODO */ - privateKeyToBuffer(privKey: any): Buffer { + 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. diff --git a/packages/crypto-wallet-core/src/derivation/xrp/index.ts b/packages/crypto-wallet-core/src/derivation/xrp/index.ts index 59f69a60ce9..58e77740296 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -37,11 +37,11 @@ export class XrpDeriver implements IDeriver { } /** - * @param {any} privKey - expects hex-encoded string, as returned from XrpDeriver.derivePrivateKey privKey + * @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: any): Buffer { + 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. From 57c4d0c002d34b81e558eb4d01ff469a0203ac79 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Mon, 26 Jan 2026 11:11:01 -0500 Subject: [PATCH 13/22] remove legacy wallet creation code --- packages/bitcore-client/src/wallet.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 4d65818ed78..f40f1afabde 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -137,8 +137,6 @@ 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'); } @@ -172,12 +170,10 @@ export class Wallet { 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'); - } + // 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(Buffer.from(privKeyObj.privateKey, 'hex'), pubKey, walletEncryptionKey).toString('hex'); // Generate authentication keys const authKey = new PrivateKey(); @@ -196,13 +192,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, @@ -222,7 +216,7 @@ export class Wallet { lite, addressType, addressZero: null, - version, + version: 2, } as IWalletExt); // save wallet to storage and then bitcore-node From 2afe18e3b61ee5e161a18eeaa935562e66bab551 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Mon, 26 Jan 2026 16:31:05 -0500 Subject: [PATCH 14/22] refactored loadWallet to auto-migrate wallets to current version --- packages/bitcore-client/src/wallet.ts | 48 ++++++++++++++++++--------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index f40f1afabde..ae18bb1af7c 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -36,6 +36,7 @@ const chainLibs = { XRP: xrpl, SOL: { SolKit, SolanaProgram } }; +const CURRENT_WALLET_VERSION = 2; export interface IWalletExt extends IWallet { storage?: Storage; @@ -160,7 +161,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(); + const privKeyObj = hdPrivKey.toObjectWithBufferPrivateKey(); // Generate public keys // bip44 compatible pubKey @@ -173,14 +174,18 @@ export class Wallet { // 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(Buffer.from(privKeyObj.privateKey, 'hex'), pubKey, walletEncryptionKey).toString('hex'); + // privKeyObj.privateKey = Encryption.encryptBuffer(Buffer.from(privKeyObj.privateKey, 'hex'), 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(); - // Generate and encrypt the encryption key and private key - const encPrivateKey = Encryption.encryptPrivateKey(JSON.stringify(privKeyObj), pubKey, walletEncryptionKey); + /** + * TODO: Remove Encryption.encryptPrivateKey - now private keys are encrypted BEFORE stringification, so private keys can be decrypted NEVER AS STRINGS + * After this TODO, the downstream consequence is that the wallet's masterKey will NOT have to be decrypted - so that will need to be changed too. + */ + const masterKeyWithEncryptedPrivateKeys = Encryption.encryptPrivateKey(JSON.stringify(privKeyObj), pubKey, walletEncryptionKey); storageType = storageType ? storageType : 'Level'; storage = @@ -196,7 +201,7 @@ export class Wallet { if (alreadyExists) { throw new Error('Wallet already exists'); } - + const wallet = new Wallet({ name, chain, @@ -206,7 +211,7 @@ export class Wallet { encryptionKey, authKey, authPubKey, - masterKey: encPrivateKey, + masterKey: masterKeyWithEncryptedPrivateKeys, password, xPubKey: hdPrivKey.xpubkey, pubKey, @@ -216,7 +221,7 @@ export class Wallet { lite, addressType, addressZero: null, - version: 2, + version: CURRENT_WALLET_VERSION, } as IWalletExt); // save wallet to storage and then bitcore-node @@ -227,11 +232,7 @@ export class Wallet { storageType }); - if (!xpriv) { - console.log(mnemonic.toString()); - } else { - console.log(hdPrivKey.toString()); - } + console.log(xpriv ? hdPrivKey.toString() : mnemonic.toString()); await loadedWallet.register().catch(e => { console.debug(e); @@ -260,11 +261,28 @@ 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'); } + + let wallet = new Wallet(Object.assign(loadedWallet, { storage })); + if (wallet.version > CURRENT_WALLET_VERSION) { + throw new Error(`Invalid wallet version ${wallet.version} exceeds current wallet version ${CURRENT_WALLET_VERSION}`); + } + + if (wallet.version != CURRENT_WALLET_VERSION) { + wallet = await wallet.migrateWallet(); + } + + return wallet; + } + + async migrateWallet(): Promise { + /** + * TODO: + */ + return this; } /** From 77c541dda6be9b290266410e2b91f04f4201dec1 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Mon, 26 Jan 2026 16:34:00 -0500 Subject: [PATCH 15/22] add HDPrivateKey toObjectWithBufferPrivateKey --- packages/bitcore-lib/lib/hdprivatekey.js | 95 ++++++++++++++---------- 1 file changed, 54 insertions(+), 41 deletions(-) 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 * From 2f40d20373317d0f07b2c84ef88ab4086e82a483 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 27 Jan 2026 16:29:37 -0500 Subject: [PATCH 16/22] update encrypt/decrypt methods with flexibility and buffer-first approach --- packages/bitcore-client/src/encryption.ts | 52 +++++++++++++++-------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/bitcore-client/src/encryption.ts b/packages/bitcore-client/src/encryption.ts index f63aa9bc8f6..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,36 +55,40 @@ 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: string): Buffer { - const key = Buffer.from(encryptionKey, 'hex'); +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, key, iv); - return Buffer.concat([cipher.update(data), cipher.final()]); + 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: string): Buffer { - const key = Buffer.from(encryptionKey, 'hex'); +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, key, iv); + const decipher = crypto.createDecipheriv(algo, encryptionKey, iv); const decrypted = decipher.update(encHex, 'hex'); const final = decipher.final(); - if (final.length) { - const out = Buffer.concat([decrypted, final]); + try { + return Buffer.concat([decrypted, final]); + } finally { decrypted.fill(0); final.fill(0); - return out; } - return decrypted; } function sha512KDF(passphrase: string, salt: Buffer, derivationOptions: { rounds?: number }): string { From c05c5b92c68d207b36f2f8a1a2289faedaeb0649 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 28 Jan 2026 17:22:31 -0500 Subject: [PATCH 17/22] remove backwards compatibility, add migration method --- packages/bitcore-client/src/wallet.ts | 187 ++++++++++++++++---------- 1 file changed, 116 insertions(+), 71 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index ae18bb1af7c..b2875ae8cef 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'; @@ -51,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; @@ -168,7 +169,7 @@ export class Wallet { const pubKey = hdPrivKey.publicKey.toString(); // Generate and encrypt the encryption key and private key - const walletEncryptionKey = Encryption.generateEncryptionKey().toString('hex'); // raw 32-byte key as hex + const walletEncryptionKey = Encryption.generateEncryptionKey(); const encryptionKey = Encryption.encryptEncryptionKey(walletEncryptionKey, password); // stored, password-wrapped // Encrypt privKeyObj.privateKey & privKeyObj.xprivkey @@ -181,11 +182,7 @@ export class Wallet { const authKey = new PrivateKey(); const authPubKey = authKey.toPublicKey().toString(); - /** - * TODO: Remove Encryption.encryptPrivateKey - now private keys are encrypted BEFORE stringification, so private keys can be decrypted NEVER AS STRINGS - * After this TODO, the downstream consequence is that the wallet's masterKey will NOT have to be decrypted - so that will need to be changed too. - */ - const masterKeyWithEncryptedPrivateKeys = Encryption.encryptPrivateKey(JSON.stringify(privKeyObj), pubKey, walletEncryptionKey); + const masterKeyWithEncryptedPrivateKeys = JSON.stringify(privKeyObj); storageType = storageType ? storageType : 'Level'; storage = @@ -266,23 +263,17 @@ export class Wallet { throw new Error('No wallet could be found'); } - let wallet = new Wallet(Object.assign(loadedWallet, { storage })); - if (wallet.version > CURRENT_WALLET_VERSION) { - throw new Error(`Invalid wallet version ${wallet.version} exceeds current wallet version ${CURRENT_WALLET_VERSION}`); - } + return new Wallet(Object.assign(loadedWallet, { storage })); + // TODO REMOVE + // if (wallet.version > CURRENT_WALLET_VERSION) { + // throw new Error(`Invalid wallet version ${wallet.version} exceeds current wallet version ${CURRENT_WALLET_VERSION}`); + // } - if (wallet.version != CURRENT_WALLET_VERSION) { - wallet = await wallet.migrateWallet(); - } + // if (wallet.version != CURRENT_WALLET_VERSION) { + // wallet = await wallet.migrateWallet(); + // } - return wallet; - } - - async migrateWallet(): Promise { - /** - * TODO: - */ - return this; + // return wallet; } /** @@ -307,7 +298,19 @@ 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; + + // TODO: this.unlocked.encryptionKey should also be Bufferized and zeroed here + + this.unlocked.encryptionKey = null; + this.unlocked = null; return this; } @@ -316,37 +319,96 @@ 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.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.xprivkey, this.pubKey, encryptionKey); - masterKey.xprivkey = decryptedxprivBuffer.toString('hex'); - decryptedxprivBuffer.fill(0); - - const decryptedPrivKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, encryptionKey); - masterKey.privateKey = decryptedPrivKey.toString('hex'); - decryptedPrivKey.fill(0); - } + masterKey.xprivkey = Encryption.decryptToBuffer(masterKey.xprivkey, this.pubKey, encryptionKey); + masterKey.privateKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, encryptionKey); } this.unlocked = { - encryptionKey, + encryptionKey, // todo: buffer masterKey }; 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 err; + }); + + /** + * 2. Convert + */ + const masterKeyStr = Encryption.decryptPrivateKey(this.masterKey, this.pubKey, encryptionKey); + // Here, masterKeyStr.xprivkey and masterKeyStr.privateKey are both plaintext. Encrypt with encryption key + const masterKey = JSON.parse(masterKeyStr); + if (!(masterKey.xprivkey && masterKey.privateKey)) { + console.warn('WARNING - masterKey is not formatted as expected'); + throw new Error('Wallet migration failed, aborting'); + } + + 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); + + /** + * 3. Overwrite + */ + this.version = CURRENT_WALLET_VERSION; + await this.storage.saveWallet({ wallet: this.toObject(false) }) + .catch(err => { + console.error('Wallet migration failed, rely on backup', err); + }); + + /** + * 4. Return + */ + return this; + } + async register(params: { baseUrl?: string } = {}) { const { baseUrl } = params; if (baseUrl) { @@ -640,7 +702,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'); @@ -656,34 +717,19 @@ export class Wallet { }) 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(this.chain, key.privKey); - key.privKey = Encryption.encryptBuffer(privKeyBuffer, this.pubKey, encryptionKey).toString('hex'); - privKeyBuffer.fill(0); - } + // For each key to save, buffer -> encrypt to buffer -> hex string (should be undone as needed) + 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(this.chain, key.privKey); + key.privKey = Encryption.encryptBuffer(privKeyBuffer, this.pubKey, this.unlocked.encryptionKey).toString('hex'); + privKeyBuffer.fill(0); } if (keysToSave.length) { - 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 - }); - } + await this.storage.addKeysSafe({ + keys: keysToSave, + name: this.name + }); } const addedAddresses = keys.map(key => { return { address: key.address }; @@ -715,10 +761,9 @@ export class Wallet { addresses.push(utxo.address); } addresses = addresses.length > 0 ? addresses : await this.getAddresses(); - decryptedKeys = await this.storage.getKeys({ + decryptedKeys = await this.storage.getStoredKeys({ addresses, name: this.name, - encryptionKey: this.unlocked.encryptionKey }); } else { addresses.push(keys[0]); @@ -838,7 +883,7 @@ export class Wallet { const keyToImport = await Deriver.derivePrivateKey( this.chain, this.network, - this.unlocked.masterKey, + this.unlocked.masterKey, // TODO - derivePrivateKey should work with masterKey buffer privateKey (and xprivkey if necessary) addressIndex || 0, isChange, this.addressType From 9750fa2e536822407c4bc4a31c87f12b20a8692f Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 28 Jan 2026 21:36:42 -0500 Subject: [PATCH 18/22] implement storage method replacements for add/get keys, and overload loadWallet with 'raw' param --- packages/bitcore-client/src/storage.ts | 85 +++++++++++++++++++------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/packages/bitcore-client/src/storage.ts b/packages/bitcore-client/src/storage.ts index 17baa1e5969..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 }) { @@ -180,9 +183,23 @@ export class Storage { } } - async addKeysSafe(params: { name: string; keys: KeyImport[]; encryptionKey: string }) { - const { name, keys, encryptionKey } = params; - let open = true; + async getAddress(params: { name: string; address: string }) { + const { name, address } = params; + return this.storageType.getAddress({ name, address, keepAlive: true, open: true }); + } + + async getAddresses(params: { name: string; limit?: number; skip?: number }) { + 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; @@ -191,28 +208,52 @@ export class Storage { 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 }; + if (pubKey) { + payload = { key: JSON.stringify(key), 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; + // open on first, close on last + await this.storageType.addKeys({ name, key, toStore, open: i === 0, keepAlive: i < keys.length - 1 }); + ++i; } } - async getAddress(params: { name: string; address: string }) { - const { name, address } = params; - return this.storageType.getAddress({ name, address, keepAlive: true, open: true }); + 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; } - async getAddresses(params: { name: string; limit?: number; skip?: number }) { - const { name, limit, skip } = params; - return this.storageType.getAddresses({ name, limit, skip }); + 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; + } } } From b3c107807ec228c4b4f46cf6c42aed2c389fb9b4 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 29 Jan 2026 09:54:08 -0500 Subject: [PATCH 19/22] remove extra decrypt in unlock for old versions --- packages/bitcore-client/src/wallet.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index b2875ae8cef..9a76e28b42c 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -325,9 +325,7 @@ export class Wallet { } 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); } From 34735a85296cd5a7d6dea3e767880fce8dbdae2a Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 29 Jan 2026 13:07:27 -0500 Subject: [PATCH 20/22] fix password issue --- packages/bitcore-client/src/wallet.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 9a76e28b42c..167ec66b8d3 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -264,16 +264,6 @@ export class Wallet { } return new Wallet(Object.assign(loadedWallet, { storage })); - // TODO REMOVE - // if (wallet.version > CURRENT_WALLET_VERSION) { - // throw new Error(`Invalid wallet version ${wallet.version} exceeds current wallet version ${CURRENT_WALLET_VERSION}`); - // } - - // if (wallet.version != CURRENT_WALLET_VERSION) { - // wallet = await wallet.migrateWallet(); - // } - - // return wallet; } /** @@ -396,7 +386,10 @@ export class Wallet { * 3. Overwrite */ this.version = CURRENT_WALLET_VERSION; - await this.storage.saveWallet({ wallet: this.toObject(false) }) + const savedPassword = this.password; // Wallet.toObject() rehashes password - save and replace + const walletObj = this.toObject(false); + walletObj.password = savedPassword; + await this.storage.saveWallet({ wallet: walletObj }) .catch(err => { console.error('Wallet migration failed, rely on backup', err); }); From 1d861bb27c8e64a9a42e3f1f0ba0d6459ee3221e Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 29 Jan 2026 15:26:30 -0500 Subject: [PATCH 21/22] fix privkey buff assignment in key treatment - signTx --- packages/bitcore-client/src/wallet.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 167ec66b8d3..297fce25cf8 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -785,9 +785,10 @@ export class Wallet { // 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); + privKeyBuf = Encryption.decryptToBuffer(key.encKey, this.pubKey, this.unlocked.encryptionKey); key.privKey = Deriver.privateKeyBufferToNativePrivateKey(this.chain, this.network, privKeyBuf); - } catch { + } catch (e) { + console.error(e); continue; } finally { if (Buffer.isBuffer(privKeyBuf)) { From d6bb7cf3e3dbd95c13fce780a1839404ce0e9f56 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Mon, 2 Feb 2026 15:36:37 -0500 Subject: [PATCH 22/22] implementation complete, cleanup complete --- packages/bitcore-client/src/wallet.ts | 113 ++++++++++++++++++-------- 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 297fce25cf8..5e7f3ba429b 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -67,7 +67,7 @@ export class Wallet { lite: boolean; addressType: string; addressZero: string; - version?: number; // If 2, master key xprivkey and privateKey are encrypted and serialized BEFORE + version?: number; static XrpAccountFlags = xrpl.AccountSetTfFlags; @@ -175,7 +175,6 @@ export class Wallet { // 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(Buffer.from(privKeyObj.privateKey, 'hex'), pubKey, walletEncryptionKey).toString('hex'); privKeyObj.privateKey = Encryption.encryptBuffer(privKeyObj.privateKey, pubKey, walletEncryptionKey).toString('hex'); // Generate authentication keys @@ -229,8 +228,6 @@ export class Wallet { storageType }); - console.log(xpriv ? hdPrivKey.toString() : mnemonic.toString()); - await loadedWallet.register().catch(e => { console.debug(e); console.error('Failed to register wallet with bitcore-node.'); @@ -297,8 +294,9 @@ export class Wallet { } this.unlocked.masterKey = null; - // TODO: this.unlocked.encryptionKey should also be Bufferized and zeroed here - + if (Buffer.isBuffer(this.unlocked.encryptionKey)) { + this.unlocked.encryptionKey.fill(0); + } this.unlocked.encryptionKey = null; this.unlocked = null; return this; @@ -320,7 +318,7 @@ export class Wallet { masterKey.privateKey = Encryption.decryptToBuffer(masterKey.privateKey, this.pubKey, encryptionKey); } this.unlocked = { - encryptionKey, // todo: buffer + encryptionKey, masterKey }; return this; @@ -351,18 +349,37 @@ export class Wallet { await writeFile(`${this.name}.bak`, rawWallet, 'utf8') .catch(err => { console.error('Wallet backup failed, aborting migration', err.msg); - throw err; + 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); - // Here, masterKeyStr.xprivkey and masterKeyStr.privateKey are both plaintext. Encrypt with encryption key const masterKey = JSON.parse(masterKeyStr); if (!(masterKey.xprivkey && masterKey.privateKey)) { - console.warn('WARNING - masterKey is not formatted as expected'); - throw new Error('Wallet migration failed, aborting'); + throw new Error('Migration failure: masterKey is not formatted as expected'); } const xprivBuffer = BitcoreLib.encoding.Base58Check.decode(masterKey.xprivkey); @@ -383,20 +400,44 @@ export class Wallet { this.masterKey = JSON.stringify(masterKey); /** - * 3. Overwrite + * 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 savedPassword = this.password; // Wallet.toObject() rehashes password - save and replace + const storedEncryptedPassword = this.password; // Wallet.toObject() rehashes password - save and replace const walletObj = this.toObject(false); - walletObj.password = savedPassword; + walletObj.password = storedEncryptedPassword; await this.storage.saveWallet({ wallet: walletObj }) .catch(err => { - console.error('Wallet migration failed, rely on backup', 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'); }); - /** - * 4. Return - */ return this; } @@ -505,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}`); @@ -708,11 +748,9 @@ export class Wallet { }) as KeyImport); } - // For each key to save, buffer -> encrypt to buffer -> hex string (should be undone as needed) 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(this.chain, key.privKey); - key.privKey = Encryption.encryptBuffer(privKeyBuffer, this.pubKey, this.unlocked.encryptionKey).toString('hex'); + key.privKey = Encryption.encryptBuffer(privKeyBuffer, key.pubKey, this.unlocked.encryptionKey).toString('hex'); privKeyBuffer.fill(0); } @@ -776,21 +814,21 @@ export class Wallet { // Shallow copy to avoid mutation if signingKeys are passed in const keysForSigning = [...(signingKeys || decryptedKeys)]; - 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 - */ + if (decryptPrivateKeys) { 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.encKey, this.pubKey, this.unlocked.encryptionKey); - key.privKey = Deriver.privateKeyBufferToNativePrivateKey(this.chain, this.network, privKeyBuf); + 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(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); } @@ -872,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, // TODO - derivePrivateKey should work with masterKey buffer privateKey (and xprivkey if necessary) + masterKeyForDeriver, addressIndex || 0, isChange, this.addressType