From 0e5e6b4bfff9fefbad26e048e2d098c4d6fd0c62 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 11 Dec 2025 12:45:00 -0500 Subject: [PATCH 01/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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 From fe6e8d197155bdf547cc9ed3be68c6588618bdbd Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 3 Feb 2026 16:42:50 -0500 Subject: [PATCH 23/51] Add getPublicKey to DeriverProxy and to IDeriver --- packages/crypto-wallet-core/src/derivation/index.ts | 4 ++++ packages/crypto-wallet-core/src/types/derivation.ts | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index d9afe74bab0..de97b13a9f5 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -105,6 +105,10 @@ export class DeriverProxy { return this.get(chain).getAddress(network, pubKey, addressType); } + getPublicKey(chain, network, privKey) { + return this.get(chain).getPublicKey(network, privKey); + } + pathFor(chain, network, account = 0) { const normalizedChain = chain.toUpperCase(); const accountStr = `${account}'`; diff --git a/packages/crypto-wallet-core/src/types/derivation.ts b/packages/crypto-wallet-core/src/types/derivation.ts index 56d4348e5ac..10261a09fac 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -15,6 +15,12 @@ export interface IDeriver { getAddress(network: string, pubKey, addressType: string): string; + /** + * Derive the public key for a given chain-native private key representation. + * Used when importing plaintext private keys that may not include `pubKey`. + */ + getPublicKey(network: string, privKey: any): string; + /** * Used to normalize output of Key.privKey */ From 516c4a01c9878b75c2c7bb7e2f23ef2193c8434b Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 3 Feb 2026 16:59:15 -0500 Subject: [PATCH 24/51] buffer-based privkey implementation of Deriver.getPublicKey --- .../crypto-wallet-core/src/derivation/btc/index.ts | 10 ++++++++++ .../crypto-wallet-core/src/derivation/eth/index.ts | 11 +++++++++++ packages/crypto-wallet-core/src/derivation/index.ts | 5 ++++- .../crypto-wallet-core/src/derivation/sol/index.ts | 8 ++++++++ .../crypto-wallet-core/src/derivation/xrp/index.ts | 11 +++++++++++ packages/crypto-wallet-core/src/types/derivation.ts | 3 ++- 6 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index 031e8af451d..46095179cad 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -34,6 +34,16 @@ export abstract class AbstractBitcoreLibDeriver implements IDeriver { return new this.bitcoreLib.Address(pubKey, network, addressType).toString(); } + getPublicKey(network: string, privKey: Buffer): string { + if (!Buffer.isBuffer(privKey)) { + throw new Error('Expected privKey to be a Buffer'); + } + // Force compressed pubkey (buffer does not encode compression flag) + const bn = this.bitcoreLib.crypto.BN.fromBuffer(privKey); + const key = new this.bitcoreLib.PrivateKey({ bn, network, compressed: true }); + return key.publicKey.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 diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index c9f2b12d709..7abdef8aa59 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -56,6 +56,17 @@ export class EthDeriver implements IDeriver { return this.addressFromPublicKeyBuffer(pubKey.toBuffer()); } + getPublicKey(_network: string, privKey: Buffer): string { + if (!Buffer.isBuffer(privKey)) { + throw new Error('Expected privKey to be a Buffer'); + } + // Match the pubKey representation returned from derivePrivateKeyWithPath (hex string) + // Force compressed pubkey (buffer does not encode compression flag) + const bn = BitcoreLib.crypto.BN.fromBuffer(privKey); + const key = new BitcoreLib.PrivateKey({ bn, compressed: true }); + return key.publicKey.toString('hex'); + } + /** * @param {Buffer | string} privKey - expects hex-encoded string, as returned from EthDeriver.derivePrivateKey * @returns {Buffer} diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index de97b13a9f5..6bddb225c82 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -105,7 +105,10 @@ export class DeriverProxy { return this.get(chain).getAddress(network, pubKey, addressType); } - getPublicKey(chain, network, privKey) { + /** + * Caller responsible for cleaning up privKey buffer + */ + getPublicKey(chain, network, privKey: Buffer) { return this.get(chain).getPublicKey(network, privKey); } diff --git a/packages/crypto-wallet-core/src/derivation/sol/index.ts b/packages/crypto-wallet-core/src/derivation/sol/index.ts index ea90c9d8aff..b277c594f43 100644 --- a/packages/crypto-wallet-core/src/derivation/sol/index.ts +++ b/packages/crypto-wallet-core/src/derivation/sol/index.ts @@ -16,6 +16,14 @@ export class SolDeriver implements IDeriver { return this.addressFromPublicKeyBuffer(Buffer.from(pubKey, 'hex')); } + getPublicKey(_network: string, privKey: Buffer): string { + if (!Buffer.isBuffer(privKey)) { + throw new Error('Expected privKey to be a Buffer'); + } + const pubKey = ed25519.getPublicKey(privKey, false); + return Buffer.from(pubKey).toString('hex'); + } + addressFromPublicKeyBuffer(pubKey: Buffer): string { if (pubKey.length > 32) { pubKey = pubKey.subarray(pubKey.length - 32); diff --git a/packages/crypto-wallet-core/src/derivation/xrp/index.ts b/packages/crypto-wallet-core/src/derivation/xrp/index.ts index 58e77740296..b6d99549b52 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -36,6 +36,17 @@ export class XrpDeriver implements IDeriver { return address; } + getPublicKey(_network: string, privKey: Buffer): string { + if (!Buffer.isBuffer(privKey)) { + throw new Error('Expected privKey to be a Buffer'); + } + // Match the pubKey representation returned from derivePrivateKeyWithPath (uppercase hex string) + // Force compressed pubkey (buffer does not encode compression flag) + const bn = BitcoreLib.crypto.BN.fromBuffer(privKey); + const key = new BitcoreLib.PrivateKey({ bn, compressed: true }); + return key.publicKey.toString('hex').toUpperCase(); + } + /** * @param {Buffer | string} privKey - expects hex-encoded string, as returned from XrpDeriver.derivePrivateKey privKey * @returns {Buffer} diff --git a/packages/crypto-wallet-core/src/types/derivation.ts b/packages/crypto-wallet-core/src/types/derivation.ts index 10261a09fac..c1f833a2c59 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -18,8 +18,9 @@ export interface IDeriver { /** * Derive the public key for a given chain-native private key representation. * Used when importing plaintext private keys that may not include `pubKey`. + * Caller should clean up buffer after use */ - getPublicKey(network: string, privKey: any): string; + getPublicKey(network: string, privKey: Buffer): string; /** * Used to normalize output of Key.privKey From 702eca77ba4cbdc85be4457610dd69247fb7ae86 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 3 Feb 2026 17:10:52 -0500 Subject: [PATCH 25/51] fix deriver bug in eth and xrp getPublicKey --- packages/bitcore-client/src/wallet.ts | 41 ++++++++++++++----- .../bitcore-client/test/unit/wallet.test.ts | 13 ++++-- .../src/derivation/eth/index.ts | 6 +-- .../src/derivation/xrp/index.ts | 6 +-- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 5e7f3ba429b..4e31c8b3f4d 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -735,23 +735,44 @@ export class Wallet { async importKeys(params: { keys: KeyImport[]; rederiveAddys?: boolean }) { const { rederiveAddys } = params; let { keys } = params; + // Avoid mutating caller-owned references (we'll encrypt privKeys below) + keys = keys.map(k => ({ ...k })); let keysToSave = keys.filter(key => typeof key.privKey === 'string'); if (rederiveAddys) { - keysToSave = keysToSave.map(key => ({ - ...key, - address: key.pubKey ? Deriver.getAddress(this.chain, this.network, key.pubKey, this.addressType) : key.address - }) as KeyImport); - keys = keys.map(key => ({ - ...key, - address: key.pubKey ? Deriver.getAddress(this.chain, this.network, key.pubKey, this.addressType) : key.address - }) as KeyImport); + keys = keys.map(key => { + let pubKey = key.pubKey; + if (!pubKey && typeof key.privKey === 'string') { + const privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, key.privKey); + try { + pubKey = Deriver.getPublicKey(this.chain, this.network, privKeyBuffer); + } finally { + privKeyBuffer.fill(0); + } + } + return { + ...key, + pubKey, + address: pubKey ? Deriver.getAddress(this.chain, this.network, pubKey, this.addressType) : key.address + } as KeyImport; + }); + keysToSave = keys.filter(key => typeof key.privKey === 'string'); } for (const key of keysToSave) { const privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, key.privKey); - key.privKey = Encryption.encryptBuffer(privKeyBuffer, key.pubKey, this.unlocked.encryptionKey).toString('hex'); - privKeyBuffer.fill(0); + try { + if (!key.pubKey) { + key.pubKey = Deriver.getPublicKey(this.chain, this.network, privKeyBuffer); + } + if (!key.pubKey) { + throw new Error(`pubKey is undefined for ${this.name}. Keys not added to storage`); + } + key.privKey = Encryption.encryptBuffer(privKeyBuffer, key.pubKey, this.unlocked.encryptionKey).toString('hex'); + } finally { + // Buffer creator should sanitize + privKeyBuffer.fill(0); + } } if (keysToSave.length) { diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index 862635db2c8..b536c6846aa 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -405,10 +405,11 @@ describe('Wallet', 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'); + // v2 key encryption uses the key's pubKey as the IV salt (not the wallet pubKey) + const encPriv = Encryption.encryptBuffer(privBuf, pk.publicKey.toString(), wallet.unlocked.encryptionKey).toString('hex'); privBuf.fill(0); - sandbox.stub(wallet.storage, 'getKeys').resolves([ + sandbox.stub(wallet.storage, 'getStoredKeys').resolves([ { address, privKey: encPriv, @@ -461,8 +462,12 @@ describe('Wallet', function() { it('should decrypt stored ciphertext and hand hex privKey to Transactions.sign', async function() { const privHex = crypto.randomBytes(32).toString('hex'); + const privBufForPubKey = CWC.Deriver.privateKeyToBuffer('ETH', privHex); + const pubKey = CWC.Deriver.getPublicKey('ETH', wallet.network, privBufForPubKey); + privBufForPubKey.fill(0); const privBuf = CWC.Deriver.privateKeyToBuffer('ETH', privHex); - const encPriv = Encryption.encryptBuffer(privBuf, wallet.pubKey, wallet.unlocked.encryptionKey).toString('hex'); + // v2 key encryption uses the key's pubKey as the IV salt (not the wallet pubKey) + const encPriv = Encryption.encryptBuffer(privBuf, pubKey, wallet.unlocked.encryptionKey).toString('hex'); privBuf.fill(0); let capturedPayload; @@ -471,7 +476,7 @@ describe('Wallet', function() { return 'signed'; }); - const signingKeys = [{ address: '0xabc', privKey: encPriv }]; + const signingKeys = [{ address: '0xabc', privKey: encPriv, pubKey }]; await wallet.signTx({ tx: 'raw', signingKeys }); txStub.calledOnce.should.equal(true); diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index 7abdef8aa59..0e534a8ef1a 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -61,9 +61,9 @@ export class EthDeriver implements IDeriver { throw new Error('Expected privKey to be a Buffer'); } // Match the pubKey representation returned from derivePrivateKeyWithPath (hex string) - // Force compressed pubkey (buffer does not encode compression flag) - const bn = BitcoreLib.crypto.BN.fromBuffer(privKey); - const key = new BitcoreLib.PrivateKey({ bn, compressed: true }); + // Convert Buffer -> hex as the PrivateKey constructor input. + // This avoids bitcore-lib rejecting the `{ bn }` object form in some builds. + const key = new BitcoreLib.PrivateKey(privKey.toString('hex')); return key.publicKey.toString('hex'); } diff --git a/packages/crypto-wallet-core/src/derivation/xrp/index.ts b/packages/crypto-wallet-core/src/derivation/xrp/index.ts index b6d99549b52..1c906106eb1 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -41,9 +41,9 @@ export class XrpDeriver implements IDeriver { throw new Error('Expected privKey to be a Buffer'); } // Match the pubKey representation returned from derivePrivateKeyWithPath (uppercase hex string) - // Force compressed pubkey (buffer does not encode compression flag) - const bn = BitcoreLib.crypto.BN.fromBuffer(privKey); - const key = new BitcoreLib.PrivateKey({ bn, compressed: true }); + // Convert Buffer -> hex as the PrivateKey constructor input. + // This avoids bitcore-lib rejecting the `{ bn }` object form in some builds. + const key = new BitcoreLib.PrivateKey(privKey.toString('hex')); return key.publicKey.toString('hex').toUpperCase(); } From 2169c91b6d143be44b31a20ec6f6748fd315dad8 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 4 Feb 2026 09:24:21 -0500 Subject: [PATCH 26/51] add getPublicKey tests to derivers --- .../crypto-wallet-core/test/deriver.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/crypto-wallet-core/test/deriver.test.ts diff --git a/packages/crypto-wallet-core/test/deriver.test.ts b/packages/crypto-wallet-core/test/deriver.test.ts new file mode 100644 index 00000000000..4dd1a88441b --- /dev/null +++ b/packages/crypto-wallet-core/test/deriver.test.ts @@ -0,0 +1,66 @@ +import { expect } from 'chai'; +import crypto from 'crypto'; +import { Deriver } from '../src'; +import BitcoreLib from 'bitcore-lib'; +import { encoding } from 'bitcore-lib'; +import * as ed25519 from 'ed25519-hd-key'; + +describe('Deriver.getPublicKey (Buffer-first)', () => { + it('BTC: should derive pubKey from privKey buffer', () => { + const privHex = crypto.randomBytes(32).toString('hex'); + const privBuf = Deriver.privateKeyToBuffer('BTC', privHex); + try { + const pubKey = Deriver.getPublicKey('BTC', 'testnet', privBuf); + const expected = new BitcoreLib.PrivateKey(privHex).publicKey.toString(); + expect(pubKey).to.equal(expected); + } finally { + privBuf.fill(0); + } + }); + + it('ETH: privateKeyToBuffer should accept with/without 0x, and getPublicKey should match', () => { + const privHex = crypto.randomBytes(32).toString('hex'); + const privBufNo0x = Deriver.privateKeyToBuffer('ETH', privHex); + const privBuf0x = Deriver.privateKeyToBuffer('ETH', `0x${privHex}`); + try { + expect(privBuf0x.equals(privBufNo0x)).to.equal(true); + + const pubKey = Deriver.getPublicKey('ETH', 'mainnet', privBufNo0x); + const expected = new BitcoreLib.PrivateKey(privHex).publicKey.toString('hex'); + expect(pubKey).to.equal(expected); + } finally { + privBufNo0x.fill(0); + privBuf0x.fill(0); + } + }); + + it('XRP: should derive uppercase hex pubKey from privKey buffer', () => { + const privHex = crypto.randomBytes(32).toString('hex').toUpperCase(); + const privBuf = Deriver.privateKeyToBuffer('XRP', privHex); + try { + const pubKey = Deriver.getPublicKey('XRP', 'mainnet', privBuf); + const expected = new BitcoreLib.PrivateKey(privHex.toLowerCase()).publicKey.toString('hex').toUpperCase(); + expect(pubKey).to.equal(expected); + } finally { + privBuf.fill(0); + } + }); + + it('SOL: should derive pubKey hex from privKey buffer', () => { + const seed = crypto.randomBytes(32); + const seed58 = encoding.Base58.encode(seed); + const privBuf = Deriver.privateKeyToBuffer('SOL', seed58); + try { + // ensure the base58 path yields the original bytes + expect(Buffer.compare(privBuf, seed)).to.equal(0); + + const pubKey = Deriver.getPublicKey('SOL', 'mainnet', privBuf); + const expected = Buffer.from(ed25519.getPublicKey(seed, false)).toString('hex'); + expect(pubKey).to.equal(expected); + } finally { + seed.fill(0); + privBuf.fill(0); + } + }); +}); + From c476975792c3d75aed80e605bc2c523df58ed235 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 4 Feb 2026 10:01:40 -0500 Subject: [PATCH 27/51] add check to lock() to handle not unlocked cases --- packages/bitcore-client/src/wallet.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 4e31c8b3f4d..e36e5285e8e 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -285,6 +285,10 @@ export class Wallet { } lock() { + if (!this.unlocked) { + return this; + } + if (Buffer.isBuffer(this.unlocked.masterKey.xprivkey)) { this.unlocked.masterKey.xprivkey.fill(0); } From 2aa2e3571ca24e3542c9de283d655b5683594027 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Mon, 9 Feb 2026 15:13:25 -0500 Subject: [PATCH 28/51] address feedback --- packages/bitcore-client/src/encryption.ts | 39 ++++++++++++++++--- packages/bitcore-client/src/storage.ts | 7 ++-- .../bitcore-client/test/unit/wallet.test.ts | 3 +- .../src/derivation/index.ts | 2 +- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/bitcore-client/src/encryption.ts b/packages/bitcore-client/src/encryption.ts index b0a89565a07..503e6762dad 100644 --- a/packages/bitcore-client/src/encryption.ts +++ b/packages/bitcore-client/src/encryption.ts @@ -46,13 +46,40 @@ export function decryptEncryptionKey(encEncryptionKey, password, toBuffer?: bool } } +// @deprecated - Use encryptBuffer export function encryptPrivateKey(privKey, pubKey, encryptionKey) { - const key = Buffer.from(encryptionKey, 'hex'); - const doubleHash = Buffer.from(SHA256(SHA256(pubKey)), 'hex'); - const iv = doubleHash.subarray(0, 16); - const cipher = crypto.createCipheriv(algo, key, iv); - const encData = cipher.update(privKey, 'utf8', 'hex') + cipher.final('hex'); - return encData; + // Store buffers this method makes to sanitize + const createdBuffers: Buffer[] = []; + + try { + let encKey: Buffer | null = null; + if (Buffer.isBuffer(encryptionKey)) { + encKey = encryptionKey; + } else { + if (typeof encryptionKey !== 'string') { + throw new Error('encryptionKey should be Buffer or hex-encoded string'); + } + encKey = Buffer.from(encryptionKey, 'hex'); + createdBuffers.push(encKey); + } + + let unencryptedPrivKeyBuffer: Buffer | null = null; + if (Buffer.isBuffer(privKey)) { + unencryptedPrivKeyBuffer = privKey; + } else { + if (typeof privKey !== 'string') { + throw new Error('privKey should be Buffer or utf8-encoded string'); + } + unencryptedPrivKeyBuffer = Buffer.from(privKey); + createdBuffers.push(unencryptedPrivKeyBuffer); + } + + return encryptBuffer(unencryptedPrivKeyBuffer, pubKey, encKey).toString('hex'); + } finally { + for (const buf of createdBuffers) { + buf.fill(0); + } + } } function decryptPrivateKey(encPrivateKey: string, pubKey: string, encryptionKey: Buffer | string) { diff --git a/packages/bitcore-client/src/storage.ts b/packages/bitcore-client/src/storage.ts index 3887c94eb88..5db568d3ba8 100644 --- a/packages/bitcore-client/src/storage.ts +++ b/packages/bitcore-client/src/storage.ts @@ -116,6 +116,7 @@ export class Storage { return this.storageType.saveWallet({ wallet }); } + // @deprecated async getKey(params: { address: string; name: string; @@ -135,6 +136,7 @@ export class Storage { } } + // @deprecated async getKeys(params: { addresses: string[]; name: string; encryptionKey: string }): Promise> { const { addresses, name, encryptionKey } = params; const keys = new Array(); @@ -161,6 +163,7 @@ export class Storage { return keys; } + // @deprecated async addKeys(params: { name: string; keys: KeyImport[]; encryptionKey: string }) { const { name, keys, encryptionKey } = params; let open = true; @@ -193,10 +196,6 @@ export class Storage { 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; diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index b536c6846aa..9c73c04c88d 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -377,7 +377,7 @@ describe('Wallet', function() { describe('signTx v2 key handling', function() { let txStub: sinon.SinonStub; afterEach(async function() { - txStub?.restore(); + sandbox.restore(); }); describe('BTC (UTXO) decrypts ciphertext to WIF', function() { @@ -466,7 +466,6 @@ describe('Wallet', function() { const pubKey = CWC.Deriver.getPublicKey('ETH', wallet.network, privBufForPubKey); privBufForPubKey.fill(0); const privBuf = CWC.Deriver.privateKeyToBuffer('ETH', privHex); - // v2 key encryption uses the key's pubKey as the IV salt (not the wallet pubKey) const encPriv = Encryption.encryptBuffer(privBuf, pubKey, wallet.unlocked.encryptionKey).toString('hex'); privBuf.fill(0); diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index 6bddb225c82..bf21ccaf4ed 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -131,7 +131,7 @@ export class DeriverProxy { return this.get(chain).privateKeyToBuffer(privateKey); } - privateKeyBufferToNativePrivateKey(chain: string, network: string, buf: Buffer): any { + privateKeyBufferToNativePrivateKey(chain: string, network: string, buf: Buffer): string { return this.get(chain).privateKeyBufferToNativePrivateKey(buf, network); } } From cf2b8ce6dd62095eeb0feecbe4da670dc3f8cb5c Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 10 Feb 2026 12:49:56 -0500 Subject: [PATCH 29/51] fix @deprecated comment style (to jsdocs) --- packages/bitcore-client/src/encryption.ts | 2 +- packages/bitcore-client/src/storage.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bitcore-client/src/encryption.ts b/packages/bitcore-client/src/encryption.ts index 503e6762dad..a7d059ac4d7 100644 --- a/packages/bitcore-client/src/encryption.ts +++ b/packages/bitcore-client/src/encryption.ts @@ -46,7 +46,7 @@ export function decryptEncryptionKey(encEncryptionKey, password, toBuffer?: bool } } -// @deprecated - Use encryptBuffer +/** @deprecated - Use encryptBuffer */ export function encryptPrivateKey(privKey, pubKey, encryptionKey) { // Store buffers this method makes to sanitize const createdBuffers: Buffer[] = []; diff --git a/packages/bitcore-client/src/storage.ts b/packages/bitcore-client/src/storage.ts index 5db568d3ba8..5ca614f506a 100644 --- a/packages/bitcore-client/src/storage.ts +++ b/packages/bitcore-client/src/storage.ts @@ -116,7 +116,7 @@ export class Storage { return this.storageType.saveWallet({ wallet }); } - // @deprecated + /** @deprecated - Use getStoredKey */ async getKey(params: { address: string; name: string; @@ -136,7 +136,7 @@ export class Storage { } } - // @deprecated + /** @deprecated - Use getStoredKeys */ async getKeys(params: { addresses: string[]; name: string; encryptionKey: string }): Promise> { const { addresses, name, encryptionKey } = params; const keys = new Array(); @@ -163,7 +163,7 @@ export class Storage { return keys; } - // @deprecated + /** @deprecated - Use addKeysSafe */ async addKeys(params: { name: string; keys: KeyImport[]; encryptionKey: string }) { const { name, keys, encryptionKey } = params; let open = true; From 0b5e9ae86ff47f77a4e9efbc798f465aca680a9d Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 10 Feb 2026 13:52:11 -0500 Subject: [PATCH 30/51] fix naming issue --- packages/crypto-wallet-core/src/derivation/btc/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/eth/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/sol/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/xrp/index.ts | 2 +- packages/crypto-wallet-core/src/types/derivation.ts | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index 46095179cad..45e9537e39f 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -56,7 +56,7 @@ export abstract class AbstractBitcoreLibDeriver implements IDeriver { return key.toBuffer(); } - privateKeyBufferToNativePrivateKey(buf: Buffer, network: string): any { + bufferToPrivateKey_TEMP(buf: Buffer, network: string): any { // force compressed WIF without mutating instances const bn = this.bitcoreLib.crypto.BN.fromBuffer(buf); const key = new this.bitcoreLib.PrivateKey({ bn, network, compressed: true }); diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index 0e534a8ef1a..d9b032e312a 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -82,7 +82,7 @@ export class EthDeriver implements IDeriver { return Buffer.from(privKey, 'hex'); } - privateKeyBufferToNativePrivateKey(buf: Buffer, _network: string): any { + bufferToPrivateKey_TEMP(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 bf21ccaf4ed..aa544f4404a 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -132,7 +132,7 @@ export class DeriverProxy { } privateKeyBufferToNativePrivateKey(chain: string, network: string, buf: Buffer): string { - return this.get(chain).privateKeyBufferToNativePrivateKey(buf, network); + return this.get(chain).bufferToPrivateKey_TEMP(buf, network); } } diff --git a/packages/crypto-wallet-core/src/derivation/sol/index.ts b/packages/crypto-wallet-core/src/derivation/sol/index.ts index b277c594f43..5db4b3864a9 100644 --- a/packages/crypto-wallet-core/src/derivation/sol/index.ts +++ b/packages/crypto-wallet-core/src/derivation/sol/index.ts @@ -75,7 +75,7 @@ export class SolDeriver implements IDeriver { return encoding.Base58.decode(privKey); } - privateKeyBufferToNativePrivateKey(buf: Buffer, _network: string): any { + bufferToPrivateKey_TEMP(buf: Buffer, _network: string) { 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 1c906106eb1..48a53f4c934 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -59,7 +59,7 @@ export class XrpDeriver implements IDeriver { return Buffer.from(privKey, 'hex'); } - privateKeyBufferToNativePrivateKey(buf: Buffer, _network: string): any { + bufferToPrivateKey_TEMP(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 c1f833a2c59..447f25452d7 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -28,7 +28,7 @@ export interface IDeriver { privateKeyToBuffer(privKey: any): Buffer; /** - * Temporary - converts decrypted private key buffer to chain-native private key format + * Temporary - converts decrypted private key buffer to lib-specific private key format */ - privateKeyBufferToNativePrivateKey(buf: Buffer, network: string): any; + bufferToPrivateKey_TEMP(buf: Buffer, network: string): any; } \ No newline at end of file From f8930a63d9ea64920b9041236eef4e10e47117de Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 10 Feb 2026 13:53:01 -0500 Subject: [PATCH 31/51] simplify deprecated method implementation --- packages/bitcore-client/src/encryption.ts | 35 ++--------------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/packages/bitcore-client/src/encryption.ts b/packages/bitcore-client/src/encryption.ts index a7d059ac4d7..a6f47b2d873 100644 --- a/packages/bitcore-client/src/encryption.ts +++ b/packages/bitcore-client/src/encryption.ts @@ -48,38 +48,9 @@ export function decryptEncryptionKey(encEncryptionKey, password, toBuffer?: bool /** @deprecated - Use encryptBuffer */ export function encryptPrivateKey(privKey, pubKey, encryptionKey) { - // Store buffers this method makes to sanitize - const createdBuffers: Buffer[] = []; - - try { - let encKey: Buffer | null = null; - if (Buffer.isBuffer(encryptionKey)) { - encKey = encryptionKey; - } else { - if (typeof encryptionKey !== 'string') { - throw new Error('encryptionKey should be Buffer or hex-encoded string'); - } - encKey = Buffer.from(encryptionKey, 'hex'); - createdBuffers.push(encKey); - } - - let unencryptedPrivKeyBuffer: Buffer | null = null; - if (Buffer.isBuffer(privKey)) { - unencryptedPrivKeyBuffer = privKey; - } else { - if (typeof privKey !== 'string') { - throw new Error('privKey should be Buffer or utf8-encoded string'); - } - unencryptedPrivKeyBuffer = Buffer.from(privKey); - createdBuffers.push(unencryptedPrivKeyBuffer); - } - - return encryptBuffer(unencryptedPrivKeyBuffer, pubKey, encKey).toString('hex'); - } finally { - for (const buf of createdBuffers) { - buf.fill(0); - } - } + encryptionKey = Buffer.from(encryptionKey, 'hex'); + privKey = Buffer.from(privKey, 'utf8'); + return encryptBuffer(privKey, pubKey, encryptionKey).toString('hex'); } function decryptPrivateKey(encPrivateKey: string, pubKey: string, encryptionKey: Buffer | string) { From ab53de1bed0e15164e786d03c6c86301945687e9 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 10 Feb 2026 13:56:52 -0500 Subject: [PATCH 32/51] rename DeriverProxy method to match IDeriver implementation. --- packages/bitcore-client/src/wallet.ts | 2 +- packages/crypto-wallet-core/src/derivation/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index e36e5285e8e..56724ac177e 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -846,7 +846,7 @@ export class Wallet { 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); + const nativePrivKey = Deriver.bufferToPrivateKey_TEMP(this.chain, this.network, privKeyBuf); key.privKey = nativePrivKey; } catch (e) { diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index aa544f4404a..6994763a6cf 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -131,7 +131,7 @@ export class DeriverProxy { return this.get(chain).privateKeyToBuffer(privateKey); } - privateKeyBufferToNativePrivateKey(chain: string, network: string, buf: Buffer): string { + bufferToPrivateKey_TEMP(chain: string, network: string, buf: Buffer): string { return this.get(chain).bufferToPrivateKey_TEMP(buf, network); } } From 5a5bb0d0e8abdaea6c3c127b08cbf0ed26e4007c Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 10 Feb 2026 15:43:13 -0500 Subject: [PATCH 33/51] update method return type - temp buffer to private key --- packages/crypto-wallet-core/src/derivation/btc/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/eth/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/sol/index.ts | 2 +- packages/crypto-wallet-core/src/derivation/xrp/index.ts | 2 +- packages/crypto-wallet-core/src/types/derivation.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/crypto-wallet-core/src/derivation/btc/index.ts b/packages/crypto-wallet-core/src/derivation/btc/index.ts index 45e9537e39f..b6cc8447c07 100644 --- a/packages/crypto-wallet-core/src/derivation/btc/index.ts +++ b/packages/crypto-wallet-core/src/derivation/btc/index.ts @@ -56,7 +56,7 @@ export abstract class AbstractBitcoreLibDeriver implements IDeriver { return key.toBuffer(); } - bufferToPrivateKey_TEMP(buf: Buffer, network: string): any { + bufferToPrivateKey_TEMP(buf: Buffer, network: string): string { // force compressed WIF without mutating instances const bn = this.bitcoreLib.crypto.BN.fromBuffer(buf); const key = new this.bitcoreLib.PrivateKey({ bn, network, compressed: true }); diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index d9b032e312a..ed0bed324dc 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -82,7 +82,7 @@ export class EthDeriver implements IDeriver { return Buffer.from(privKey, 'hex'); } - bufferToPrivateKey_TEMP(buf: Buffer, _network: string): any { + bufferToPrivateKey_TEMP(buf: Buffer, _network: string): string { return buf.toString('hex'); } } diff --git a/packages/crypto-wallet-core/src/derivation/sol/index.ts b/packages/crypto-wallet-core/src/derivation/sol/index.ts index 5db4b3864a9..262960fb17e 100644 --- a/packages/crypto-wallet-core/src/derivation/sol/index.ts +++ b/packages/crypto-wallet-core/src/derivation/sol/index.ts @@ -75,7 +75,7 @@ export class SolDeriver implements IDeriver { return encoding.Base58.decode(privKey); } - bufferToPrivateKey_TEMP(buf: Buffer, _network: string) { + bufferToPrivateKey_TEMP(buf: Buffer, _network: string): string { 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 48a53f4c934..f0b75a25038 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -59,7 +59,7 @@ export class XrpDeriver implements IDeriver { return Buffer.from(privKey, 'hex'); } - bufferToPrivateKey_TEMP(buf: Buffer, _network: string): any { + bufferToPrivateKey_TEMP(buf: Buffer, _network: string): string { 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 447f25452d7..babb0a87890 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -30,5 +30,5 @@ export interface IDeriver { /** * Temporary - converts decrypted private key buffer to lib-specific private key format */ - bufferToPrivateKey_TEMP(buf: Buffer, network: string): any; + bufferToPrivateKey_TEMP(buf: Buffer, network: string): string; } \ No newline at end of file From aecdc186c8a93847b170e49a270149a4d77c62e3 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Fri, 13 Feb 2026 16:53:07 -0500 Subject: [PATCH 34/51] fix getPublicKey - rm private key to string --- packages/crypto-wallet-core/src/derivation/eth/index.ts | 5 +---- packages/crypto-wallet-core/src/derivation/xrp/index.ts | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index ed0bed324dc..78861f5e60e 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -60,10 +60,7 @@ export class EthDeriver implements IDeriver { if (!Buffer.isBuffer(privKey)) { throw new Error('Expected privKey to be a Buffer'); } - // Match the pubKey representation returned from derivePrivateKeyWithPath (hex string) - // Convert Buffer -> hex as the PrivateKey constructor input. - // This avoids bitcore-lib rejecting the `{ bn }` object form in some builds. - const key = new BitcoreLib.PrivateKey(privKey.toString('hex')); + const key = BitcoreLib.PrivateKey.fromBuffer(privKey); return key.publicKey.toString('hex'); } diff --git a/packages/crypto-wallet-core/src/derivation/xrp/index.ts b/packages/crypto-wallet-core/src/derivation/xrp/index.ts index f0b75a25038..2a8f4a58d0c 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -40,10 +40,7 @@ export class XrpDeriver implements IDeriver { if (!Buffer.isBuffer(privKey)) { throw new Error('Expected privKey to be a Buffer'); } - // Match the pubKey representation returned from derivePrivateKeyWithPath (uppercase hex string) - // Convert Buffer -> hex as the PrivateKey constructor input. - // This avoids bitcore-lib rejecting the `{ bn }` object form in some builds. - const key = new BitcoreLib.PrivateKey(privKey.toString('hex')); + const key = BitcoreLib.PrivateKey.fromBuffer(privKey); return key.publicKey.toString('hex').toUpperCase(); } From e7ff3c0a11c8d076447e046e2b166de79a4152f6 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 10 Mar 2026 12:23:28 -0400 Subject: [PATCH 35/51] update encrypt/decrypt methods to handle unspecified error conditions by wrapping in try/finally for buffer clearing --- packages/bitcore-client/src/encryption.ts | 39 +++++++++++++++-------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/bitcore-client/src/encryption.ts b/packages/bitcore-client/src/encryption.ts index a6f47b2d873..8241a2782d4 100644 --- a/packages/bitcore-client/src/encryption.ts +++ b/packages/bitcore-client/src/encryption.ts @@ -31,10 +31,13 @@ export function decryptEncryptionKey(encEncryptionKey, password, toBuffer?: bool const iv = password_hash.subarray(32, 48); const decipher = crypto.createDecipheriv(algo, key, iv); - const payload = decipher.update(encEncryptionKey, 'hex'); - const final = decipher.final(); - const output = Buffer.concat([payload, final]); + let payload: Buffer | undefined; + let final: Buffer | undefined; + let output: Buffer | undefined; try { + payload = decipher.update(encEncryptionKey, 'hex'); + final = decipher.final(); + output = Buffer.concat([payload, final]); return toBuffer ? output : output.toString('hex'); } finally { payload.fill(0); @@ -65,27 +68,35 @@ function decryptPrivateKey(encPrivateKey: string, pubKey: string, encryptionKey: } function encryptBuffer(data: Buffer, pubKey: string, encryptionKey: Buffer): Buffer { - const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); - const cipher = crypto.createCipheriv(algo, encryptionKey, iv); - const payload = cipher.update(data); + let payload: Buffer | undefined; try { + const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); + const cipher = crypto.createCipheriv(algo, encryptionKey, iv); + payload = cipher.update(data); return Buffer.concat([payload, cipher.final()]); } finally { - payload.fill(0); + if (Buffer.isBuffer(payload)) { + payload.fill(0); + } } } function decryptToBuffer(encHex: string, pubKey: string, encryptionKey: Buffer): Buffer { - const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); - const decipher = crypto.createDecipheriv(algo, encryptionKey, iv); - - const decrypted = decipher.update(encHex, 'hex'); - const final = decipher.final(); + let decrypted: Buffer | undefined; + let final: Buffer | undefined; try { + const iv = Buffer.from(SHA256(SHA256(pubKey)), 'hex').subarray(0, 16); + const decipher = crypto.createDecipheriv(algo, encryptionKey, iv); + decrypted = decipher.update(encHex, 'hex'); + final = decipher.final(); return Buffer.concat([decrypted, final]); } finally { - decrypted.fill(0); - final.fill(0); + if (Buffer.isBuffer(decrypted)) { + decrypted.fill(0); + } + if (Buffer.isBuffer(final)) { + final.fill(0); + } } } From b2e30fa6dcd780f0714b78c53af90641b4816cdf Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 11 Mar 2026 14:23:56 -0400 Subject: [PATCH 36/51] add log statements for migration process and update file path for backups --- packages/bitcore-client/src/wallet.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 56724ac177e..f9e2d88fad6 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -1,5 +1,7 @@ import { writeFile } from 'fs/promises'; import 'source-map-support/register'; +import os from 'os'; +import path from 'path'; import * as Bcrypt from 'bcrypt'; import Mnemonic from 'bitcore-mnemonic'; import { @@ -342,6 +344,8 @@ export class Wallet { return this; } + console.log(`Migrating wallet from version ${this.version} to version ${CURRENT_WALLET_VERSION}`); + /** * 1: Wallet to .bak */ @@ -349,8 +353,18 @@ export class Wallet { if (!rawWallet) { throw new Error('Migration failed - wallet not found'); } + + const backupDir = path.join( + os.homedir(), + '.bitcore', + 'bitcoreWallets', + 'backup' + ); + + const walletFilePath = path.join(backupDir, `${this.name}.bak`); - await writeFile(`${this.name}.bak`, rawWallet, 'utf8') + await writeFile(walletFilePath, rawWallet, 'utf8') + .then(() => console.log(`Pre-migration wallet backup written to ${walletFilePath}`)) .catch(err => { console.error('Wallet backup failed, aborting migration', err.msg); throw new Error('Migration failure: failed to write wallet backup file. Aborting.'); @@ -367,7 +381,9 @@ export class Wallet { // Back up keys (enc) const backupKeysStr = JSON.stringify(storedKeys); - await writeFile(`${this.name}_keys.bak`, backupKeysStr, 'utf8') + const keysFilePath = path.join(backupDir, `${this.name}_keys.bak`); + await writeFile(keysFilePath, backupKeysStr, 'utf8') + .then(() => console.log(`Pre-migration keys backup written to ${keysFilePath}`)) .catch(err => { console.error('Keys backup failed, aborting migration', err.msg); throw new Error('Migration failure: failed to write keys backup file. Aborting.'); @@ -442,6 +458,7 @@ export class Wallet { throw new Error('Migration failure: wallet not successfully saved. Use backups to restore prior wallet and keys'); }); + console.log('Migration succeeded'); return this; } From bf9bc36d27bba49e7e47b2888407ad94f686cfe6 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 11 Mar 2026 14:26:04 -0400 Subject: [PATCH 37/51] wallet test stubs --- .../bitcore-client/test/unit/wallet.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index 9c73c04c88d..d050cab20dd 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -484,6 +484,29 @@ describe('Wallet', function() { }); }); + describe('signTx', function () {}); + + describe('derivePrivateKey', function () {}); + + describe('unlock', function () { + it('performs wallet migration for previous wallet versions', async () => {}); + }); + + describe('migrateWallet', function () { + let wallet: Wallet; + let decryptedEncryptionKey: Buffer | undefined; + + + beforeEach(async function () { + const password = 'password'; + // + }); + + it('should back up existing raw wallet & keys (separately) before migration', async () => {}); + it('should overwrite existing keys', async () => {}); + it('should overwrite existing wallet', async () => {}); + }); + describe('getBalance', function() { walletName = 'BitcoreClientTestGetBalance'; beforeEach(async function() { From 5e779ff7afe377b599bf2c2012c9ef2f06d20c9f Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 11 Mar 2026 16:39:48 -0400 Subject: [PATCH 38/51] implement unlock & migrateWallet tests & add test fixture --- .../data/ethMigrationTestWallet.fixture.ts | 68 +++++++ .../bitcore-client/test/unit/wallet.test.ts | 174 +++++++++++++++++- 2 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 packages/bitcore-client/test/unit/data/ethMigrationTestWallet.fixture.ts diff --git a/packages/bitcore-client/test/unit/data/ethMigrationTestWallet.fixture.ts b/packages/bitcore-client/test/unit/data/ethMigrationTestWallet.fixture.ts new file mode 100644 index 00000000000..7732350e26c --- /dev/null +++ b/packages/bitcore-client/test/unit/data/ethMigrationTestWallet.fixture.ts @@ -0,0 +1,68 @@ +export const ethMigrationTestWalletFixture = { + exportedAt: '2026-03-11T19:39:04.994Z', + name: 'ETH_migration_test_wallet', + storageType: 'Level', + password: 'password', + wallet: { + name: 'ETH_migration_test_wallet', + chain: 'ETH', + network: 'regtest', + baseUrl: 'http://localhost:3000/api', + encryptionKey: '7eab496f0253b5395844356faa629b48ff063fb4186c51e8e448ee7db0eeb9b24afd041bad14c7153dd35a2755b5438f', + authKey: { + bn: '1a0202578666668324a496bd06b9bbf1c197ec5a6b2f0c3ccd6470abbc5328a4', + compressed: true, + network: 'livenet' + }, + authPubKey: '03a7107731216187960c8b7436e3cdc5d4d1b7004e21d85907a402838b5702a118', + masterKey: 'e8daacd89b584a43c5e14b9baba7b94c358d4e271fa6eaa3fd140bebc7507a2f80e88a460042a785adf07660b5126fb05016416fa1544c5f20f1cb9a9bb25c4cca198b9162075e902143b5dfebfcb0e88c305fc08ab0f672f5cdfaeb0ee501a4ba5a1d22f586f69ab4625a719b1b9a4ce5f217574417892fc8748b08db5a4b76938e470e768054e3758071e6de291c66d372c30613a02d0c2a63647bbfa4a305f094138d728e78b6f7f05017bc0c0c8bd390f9837e56fa463f6edeb4761fe645c53c482cae062af7fe4a01c5045fee2fbcc93b56e4362d9f0b9aaa4bf4352e3abd430cd967747fc68c2515aa088a6b5f34751a2cfaa34ef02eca74d5d7aaca2d5ed109348d2e85a22ae08a665543649279b405dac3e8879e0e1f5a784e6af1aaa5b1253bca30cbb2197346e9ce13ff358963cce0f11f46c798dc25ebfda9d920e374c785ae8983b30f661ba9b26806def02553d85533225b394847269861c310dc8c19914348219ff2d59b8d9e7ed22ee79c7552353b22eebe7e6a568e04371071932c1f5331009455b0e7944bcfa5d9e685a44f637aa53d9eafc7d3b62eafdee70543732d5d646f60108fd2c0bdef36', + password: '$2b$10$EgUUeV0zld1TOQvjMOmmPuyWQxF9hwpCBe1G4UiaAW1XYNY5/wMDm', + xPubKey: 'tpubDDT9XiD9hNQpHjpCizEvPq2TqDwZYwtKKiKHJuC5AAV2vbfooRVg2Mi7WogY8ygCTve5NgAjDXWEo3JF6sPayayYM2kNL3sQMeb6bDbv1Lk', + pubKey: '0332f26acf0584acb762eb99e638190e6ea1ddbae6bb59f79d8611d2412a50791d', + tokens: [], + storageType: 'Level', + addressType: 'pubkeyhash', + addressZero: '0xAf4a2A1c3cc0cbE9FF17B04dD91692F7686696Af', + client: { + apiUrl: 'http://localhost:3000/api/ETH/regtest', + authKey: { + bn: '1a0202578666668324a496bd06b9bbf1c197ec5a6b2f0c3ccd6470abbc5328a4', + compressed: true, + network: 'livenet' + } + }, + addressIndex: 2, + lite: false + }, + rawWallet: + '{"name":"ETH_migration_test_wallet","chain":"ETH","network":"regtest","baseUrl":"http://localhost:3000/api","encryptionKey":"7eab496f0253b5395844356faa629b48ff063fb4186c51e8e448ee7db0eeb9b24afd041bad14c7153dd35a2755b5438f","authKey":{"bn":"1a0202578666668324a496bd06b9bbf1c197ec5a6b2f0c3ccd6470abbc5328a4","compressed":true,"network":"livenet"},"authPubKey":"03a7107731216187960c8b7436e3cdc5d4d1b7004e21d85907a402838b5702a118","masterKey":"e8daacd89b584a43c5e14b9baba7b94c358d4e271fa6eaa3fd140bebc7507a2f80e88a460042a785adf07660b5126fb05016416fa1544c5f20f1cb9a9bb25c4cca198b9162075e902143b5dfebfcb0e88c305fc08ab0f672f5cdfaeb0ee501a4ba5a1d22f586f69ab4625a719b1b9a4ce5f217574417892fc8748b08db5a4b76938e470e768054e3758071e6de291c66d372c30613a02d0c2a63647bbfa4a305f094138d728e78b6f7f05017bc0c0c8bd390f9837e56fa463f6edeb4761fe645c53c482cae062af7fe4a01c5045fee2fbcc93b56e4362d9f0b9aaa4bf4352e3abd430cd967747fc68c2515aa088a6b5f34751a2cfaa34ef02eca74d5d7aaca2d5ed109348d2e85a22ae08a665543649279b405dac3e8879e0e1f5a784e6af1aaa5b1253bca30cbb2197346e9ce13ff358963cce0f11f46c798dc25ebfda9d920e374c785ae8983b30f661ba9b26806def02553d85533225b394847269861c310dc8c19914348219ff2d59b8d9e7ed22ee79c7552353b22eebe7e6a568e04371071932c1f5331009455b0e7944bcfa5d9e685a44f637aa53d9eafc7d3b62eafdee70543732d5d646f60108fd2c0bdef36","password":"$2b$10$EgUUeV0zld1TOQvjMOmmPuyWQxF9hwpCBe1G4UiaAW1XYNY5/wMDm","xPubKey":"tpubDDT9XiD9hNQpHjpCizEvPq2TqDwZYwtKKiKHJuC5AAV2vbfooRVg2Mi7WogY8ygCTve5NgAjDXWEo3JF6sPayayYM2kNL3sQMeb6bDbv1Lk","pubKey":"0332f26acf0584acb762eb99e638190e6ea1ddbae6bb59f79d8611d2412a50791d","tokens":[],"storageType":"Level","addressType":"pubkeyhash","addressZero":"0xAf4a2A1c3cc0cbE9FF17B04dD91692F7686696Af","client":{"apiUrl":"http://localhost:3000/api/ETH/regtest","authKey":{"bn":"1a0202578666668324a496bd06b9bbf1c197ec5a6b2f0c3ccd6470abbc5328a4","compressed":true,"network":"livenet"}},"addressIndex":2,"lite":false}', + localAddresses: [ + { + address: '0x021e018Ae71D1A9136cf15B3aF4be9E023EE8A70', + pubKey: '02ed66479c577e81ba397fa4498934172a8f6502b836a0d7ba340032a940cfcfd1', + path: 'm/0/1' + }, + { + address: '0xAf4a2A1c3cc0cbE9FF17B04dD91692F7686696Af', + pubKey: '030cd384611d36e243a4cdf9cca229dbc62bef0dfb11abe4a928cf8576b4bc79f7', + path: 'm/0/0' + } + ], + addresses: ['0x021e018Ae71D1A9136cf15B3aF4be9E023EE8A70', '0xAf4a2A1c3cc0cbE9FF17B04dD91692F7686696Af'], + storedKeys: [ + { + encKey: + '788e8e1d3f1b263307f2e28c76e8a842f38e162db17d02e136b333228044e0f9b16b10ce6d19c035e727826419b2707728647feaf92aeb63890941bcec2c7d1e46e655e5ad4c76e0f1dd7935e7a0b1f77a5fa28a00719b635d4744790e1c5d63857d57e83fc66a7073659e302e710b625cedfc9f35b9b78dc8e032e6f70db55f672699d388ce957a59266bfcdb29e4b78267854698c730d295686a513a66773a9395d4edf6e9acd4815e58125479adb4cf271114add96df038de5e754b786c4db0e22bb5411e46f870c7f699d0d9ee10644c0bbe29b003641945390d10b6322a2859c8dfff3312daeedc87fbbf64cdcd', + pubKey: '02ed66479c577e81ba397fa4498934172a8f6502b836a0d7ba340032a940cfcfd1', + path: 'm/0/1' + }, + { + encKey: + 'c416e891b133305bd5294cd079be851bf75f5a4c2a0b04e3e09f0ce91f0b2175a61e9e451f48af3202314886006bc9d378741c82987189abe8f01f187df34bdde9519dac08d8d327b6ccd825b70eda155f316d581bb339e1d8b2311ae8f5666de1716ce8a3d1400550d070b641d684bf04906c2b9c573af64c02baf7ccb83f7997a9295e075d314708c06106a8544bab5a7d6326a00b85bf4b961e7492adfcecd84d41ffe2e95c2b4475a500a2d27ef6368ce8998974cc46e953b93a52f25b629d24ae61c719e1c5cdb6a6848bcf4d0e8770d1df2a47e12a113409e158719aded23ccafc42a8f95b63029b735f76d3b6', + pubKey: '030cd384611d36e243a4cdf9cca229dbc62bef0dfb11abe4a928cf8576b4bc79f7', + path: 'm/0/0' + } + ] +}; + +export default ethMigrationTestWalletFixture; diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index d050cab20dd..4f129fa736b 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -1,5 +1,8 @@ import * as chai from 'chai'; import * as CWC from 'crypto-wallet-core'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; import { AddressTypes, Wallet } from '../../src/wallet'; import { Encryption } from '../../src/encryption'; import { Api as bcnApi } from '../../../bitcore-node/build/src/services/api'; @@ -13,6 +16,7 @@ import sinon from 'sinon'; import { StorageType } from '../../src/types/storage'; import supertest from 'supertest'; import { utils } from '../../src/utils'; +import ethMigrationTestWalletFixture from './data/ethMigrationTestWallet.fixture'; const should = chai.should(); @@ -65,7 +69,10 @@ describe('Wallet', function() { }); }); afterEach(async function() { - await Wallet.deleteWallet({ name: walletName, storageType }); + if (walletName) { + await Wallet.deleteWallet({ name: walletName, storageType }); + } + walletName = undefined; sandbox.restore(); }); for (const chain of ['BTC', 'BCH', 'LTC', 'DOGE', 'ETH', 'XRP', 'MATIC']) { @@ -489,22 +496,175 @@ describe('Wallet', function() { describe('derivePrivateKey', function () {}); describe('unlock', function () { - it('performs wallet migration for previous wallet versions', async () => {}); + it('performs wallet migration for previous wallet versions', async () => { + const fixture = ethMigrationTestWalletFixture; + const wallet = new Wallet(fixture.wallet as any); + const tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitcore-client-migration-unlock-')); + const backupDir = path.join(tempHomeDir, '.bitcore', 'bitcoreWallets', 'backup'); + const loadWalletStub = sandbox.stub(); + const getStoredKeysStub = sandbox.stub(); + const addKeysSafeStub = sandbox.stub(); + const saveWalletStub = sandbox.stub(); + + wallet.storage = { + loadWallet: loadWalletStub, + getStoredKeys: getStoredKeysStub, + addKeysSafe: addKeysSafeStub, + saveWallet: saveWalletStub + } as any; + + loadWalletStub.resolves(fixture.rawWallet); + getStoredKeysStub.resolves(fixture.storedKeys); + addKeysSafeStub.resolves(); + saveWalletStub.resolves(); + + fs.mkdirSync(backupDir, { recursive: true }); + const homedirStub = sandbox.stub(os, 'homedir').returns(tempHomeDir); + sandbox.stub(wallet, 'getAddresses').resolves(fixture.addresses); + + await wallet.unlock(fixture.password); + + // Assert wallet.storage methods are called (from migrateWallet) + expect(addKeysSafeStub.calledOnce).to.equal(true); + expect(saveWalletStub.calledOnce).to.equal(true); + + expect(wallet.version).to.equal(2); + expect(wallet.unlocked).to.exist; + expect(Buffer.isBuffer(wallet.unlocked?.encryptionKey)).to.equal(true); + expect(Buffer.isBuffer(wallet.unlocked?.masterKey?.privateKey)).to.equal(true); + expect(Buffer.isBuffer(wallet.unlocked?.masterKey?.xprivkey)).to.equal(true); + expect(fs.readFileSync(path.join(backupDir, `${fixture.name}.bak`), 'utf8')).to.equal(fixture.rawWallet); + expect(fs.readFileSync(path.join(backupDir, `${fixture.name}_keys.bak`), 'utf8')).to.equal(JSON.stringify(fixture.storedKeys)); + homedirStub.restore(); + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + }); }); describe('migrateWallet', function () { let wallet: Wallet; let decryptedEncryptionKey: Buffer | undefined; + let loadWalletStub: sinon.SinonStub; + let getStoredKeysStub: sinon.SinonStub; + let addKeysSafeStub: sinon.SinonStub; + let saveWalletStub: sinon.SinonStub; + let homedirStub: sinon.SinonStub; + let tempHomeDir: string; + let backupDir: string; beforeEach(async function () { - const password = 'password'; - // + wallet = new Wallet(ethMigrationTestWalletFixture.wallet as any); + tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitcore-client-migration-')); + backupDir = path.join(tempHomeDir, '.bitcore', 'bitcoreWallets', 'backup'); + fs.mkdirSync(backupDir, { recursive: true }); + wallet.storage = { + loadWallet: async () => undefined, + getStoredKeys: async () => [], + addKeysSafe: async () => undefined, + saveWallet: async () => undefined + } as any; + + loadWalletStub = sandbox.stub(wallet.storage, 'loadWallet').resolves(ethMigrationTestWalletFixture.rawWallet); + getStoredKeysStub = sandbox.stub(wallet.storage, 'getStoredKeys').resolves(ethMigrationTestWalletFixture.storedKeys); + addKeysSafeStub = sandbox.stub(wallet.storage, 'addKeysSafe').resolves(); + saveWalletStub = sandbox.stub(wallet.storage, 'saveWallet').resolves(); + homedirStub = sandbox.stub(os, 'homedir').returns(tempHomeDir); + sandbox.stub(wallet, 'getAddresses').resolves(ethMigrationTestWalletFixture.addresses); + + decryptedEncryptionKey = Encryption.decryptEncryptionKey( + ethMigrationTestWalletFixture.wallet.encryptionKey, + ethMigrationTestWalletFixture.password, + true + ) as Buffer; + }); + + afterEach(function () { + homedirStub?.restore(); + if (tempHomeDir) { + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + } + }); + + it('should back up existing raw wallet & keys (separately) before migration', async () => { + await wallet.migrateWallet(decryptedEncryptionKey); + + expect(loadWalletStub.calledOnceWithExactly({ name: ethMigrationTestWalletFixture.name, raw: true })).to.be.true; + expect(getStoredKeysStub.calledOnceWithExactly({ + addresses: ethMigrationTestWalletFixture.addresses, + name: ethMigrationTestWalletFixture.name + })).to.be.true; + expect(fs.readFileSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.bak`), 'utf8')).to.equal( + ethMigrationTestWalletFixture.rawWallet + ); + expect(fs.readFileSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}_keys.bak`), 'utf8')).to.equal( + JSON.stringify(ethMigrationTestWalletFixture.storedKeys) + ); + }); + + it('should overwrite existing keys', async () => { + await wallet.migrateWallet(decryptedEncryptionKey); + + expect(addKeysSafeStub.calledOnce).to.equal(true); + const { name, keys } = addKeysSafeStub.firstCall.args[0]; + expect(name).to.equal(ethMigrationTestWalletFixture.name); + expect(keys).to.have.length(ethMigrationTestWalletFixture.storedKeys.length); + + const expectedKeys = ethMigrationTestWalletFixture.storedKeys.map(storedKey => { + // Added keys encrypt privKey, convert to hex, and store that on key.privKey + const decryptedKey = JSON.parse( + Encryption.decryptPrivateKey(storedKey.encKey, storedKey.pubKey, decryptedEncryptionKey) + ); + const privKeyBuffer = CWC.Deriver.privateKeyToBuffer(wallet.chain, decryptedKey.privKey); + const encryptedPrivKey = Encryption.encryptBuffer(privKeyBuffer, storedKey.pubKey, decryptedEncryptionKey).toString('hex'); + privKeyBuffer.fill(0); + return { + ...decryptedKey, + privKey: encryptedPrivKey + }; + }); + + // Stored keys exhibit new encryption process + expect(keys).to.deep.equal(expectedKeys); }); - it('should back up existing raw wallet & keys (separately) before migration', async () => {}); - it('should overwrite existing keys', async () => {}); - it('should overwrite existing wallet', async () => {}); + it('should overwrite existing wallet', async () => { + await wallet.migrateWallet(decryptedEncryptionKey); + + expect(saveWalletStub.calledOnce).to.equal(true); + const savedWallet = saveWalletStub.firstCall.args[0].wallet; + + expect(savedWallet.version).to.equal(2); + expect(savedWallet.password).to.equal(ethMigrationTestWalletFixture.wallet.password); + expect(savedWallet.masterKey).to.not.equal(ethMigrationTestWalletFixture.wallet.masterKey); + + const migratedMasterKey = JSON.parse(savedWallet.masterKey); + const originalMasterKey = JSON.parse( + Encryption.decryptPrivateKey( + ethMigrationTestWalletFixture.wallet.masterKey, + ethMigrationTestWalletFixture.wallet.pubKey, + decryptedEncryptionKey + ) + ); + + const migratedXpriv = Encryption.decryptToBuffer( + migratedMasterKey.xprivkey, + ethMigrationTestWalletFixture.wallet.pubKey, + decryptedEncryptionKey + ); + const migratedPrivateKey = Encryption.decryptToBuffer( + migratedMasterKey.privateKey, + ethMigrationTestWalletFixture.wallet.pubKey, + decryptedEncryptionKey + ); + + expect(wallet.version).to.equal(2); + + // Decrypted master keys same as prior master keys + expect(migratedXpriv.toString('hex')).to.equal( + CWC.BitcoreLib.encoding.Base58Check.decode(originalMasterKey.xprivkey).toString('hex') + ); + expect(migratedPrivateKey.toString('hex')).to.equal(originalMasterKey.privateKey); + }); }); describe('getBalance', function() { From 6e7bc5df8966d615cb5b94dce1c0d17b70b98d5c Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Wed, 11 Mar 2026 16:57:03 -0400 Subject: [PATCH 39/51] add edge case tests for migrateWallet --- .../bitcore-client/test/unit/wallet.test.ts | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index 4f129fa736b..f328563264f 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -665,6 +665,87 @@ describe('Wallet', function() { ); expect(migratedPrivateKey.toString('hex')).to.equal(originalMasterKey.privateKey); }); + + it('should throw if the raw wallet cannot be loaded', async () => { + loadWalletStub.resolves(undefined); + + try { + await wallet.migrateWallet(decryptedEncryptionKey); + expect.fail('Expected migrateWallet to throw'); + } catch (err) { + expect(err.message).to.equal('Migration failed - wallet not found'); + } + + expect(getStoredKeysStub.called).to.equal(false); + expect(addKeysSafeStub.called).to.equal(false); + expect(saveWalletStub.called).to.equal(false); + }); + + it('should throw if the decrypted masterKey is malformed', async () => { + wallet.masterKey = Encryption.encryptPrivateKey( + JSON.stringify({ invalid: true }), + wallet.pubKey, + decryptedEncryptionKey.toString('hex') + ); + + try { + await wallet.migrateWallet(decryptedEncryptionKey); + expect.fail('Expected migrateWallet to throw'); + } catch (err) { + expect(err.message).to.equal('Migration failure: masterKey is not formatted as expected'); + } + + expect(addKeysSafeStub.called).to.equal(false); + expect(saveWalletStub.called).to.equal(false); + }); + + it('should throw if addKeysSafe fails and should not save the wallet', async () => { + addKeysSafeStub.rejects(new Error('write failed')); + + try { + await wallet.migrateWallet(decryptedEncryptionKey); + expect.fail('Expected migrateWallet to throw'); + } catch (err) { + expect(err.message).to.equal( + 'Migration failure: keys not successfully stored. Use backups to restore prior wallet and keys.' + ); + } + + expect(addKeysSafeStub.calledOnce).to.equal(true); + expect(saveWalletStub.called).to.equal(false); + }); + + it('should no-op when the wallet is already on the current version', async () => { + wallet.version = 2; + const warnStub = sandbox.stub(console, 'warn'); + + const migratedWallet = await wallet.migrateWallet(decryptedEncryptionKey); + + expect(migratedWallet).to.equal(wallet); + expect(warnStub.calledOnceWithExactly('Wallet migration unnecessarily called - wallet is current version')).to.equal(true); + expect(loadWalletStub.called).to.equal(false); + expect(getStoredKeysStub.called).to.equal(false); + expect(addKeysSafeStub.called).to.equal(false); + expect(saveWalletStub.called).to.equal(false); + expect(fs.existsSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.bak`))).to.equal(false); + }); + + it('should no-op when the wallet version is newer than the current version', async () => { + wallet.version = 3; + const warnStub = sandbox.stub(console, 'warn'); + + const migratedWallet = await wallet.migrateWallet(decryptedEncryptionKey); + + expect(migratedWallet).to.equal(wallet); + expect( + warnStub.calledOnceWithExactly('Wallet version 3 greater than expected current wallet version 2') + ).to.equal(true); + expect(loadWalletStub.called).to.equal(false); + expect(getStoredKeysStub.called).to.equal(false); + expect(addKeysSafeStub.called).to.equal(false); + expect(saveWalletStub.called).to.equal(false); + expect(fs.existsSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.bak`))).to.equal(false); + }); }); describe('getBalance', function() { From 0fd4944768f7863f24a0782606372572d8e63012 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Fri, 13 Mar 2026 14:07:06 -0400 Subject: [PATCH 40/51] refactor importKeys and write unit tests --- packages/bitcore-client/src/wallet.ts | 46 +++++------ .../bitcore-client/test/unit/wallet.test.ts | 77 +++++++++++++++++-- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index f9e2d88fad6..48e526518b7 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -758,42 +758,34 @@ export class Wallet { let { keys } = params; // Avoid mutating caller-owned references (we'll encrypt privKeys below) keys = keys.map(k => ({ ...k })); - let keysToSave = keys.filter(key => typeof key.privKey === 'string'); - - if (rederiveAddys) { - keys = keys.map(key => { - let pubKey = key.pubKey; - if (!pubKey && typeof key.privKey === 'string') { - const privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, key.privKey); - try { - pubKey = Deriver.getPublicKey(this.chain, this.network, privKeyBuffer); - } finally { - privKeyBuffer.fill(0); - } - } - return { - ...key, - pubKey, - address: pubKey ? Deriver.getAddress(this.chain, this.network, pubKey, this.addressType) : key.address - } as KeyImport; - }); - keysToSave = keys.filter(key => typeof key.privKey === 'string'); - } - - for (const key of keysToSave) { - const privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, key.privKey); + + const keysToSave: KeyImport[] = []; + for (const key of keys) { + if (typeof key.privKey !== 'string') { + continue; + } + + let privKeyBuffer: Buffer | undefined; try { - if (!key.pubKey) { + privKeyBuffer = Deriver.privateKeyToBuffer(this.chain, key.privKey); + if (typeof key.pubKey !== 'string') { key.pubKey = Deriver.getPublicKey(this.chain, this.network, privKeyBuffer); } if (!key.pubKey) { throw new Error(`pubKey is undefined for ${this.name}. Keys not added to storage`); } + + if (rederiveAddys) { + key.address = Deriver.getAddress(this.chain, this.network, key.pubKey, this.addressType); + } + key.privKey = Encryption.encryptBuffer(privKeyBuffer, key.pubKey, this.unlocked.encryptionKey).toString('hex'); } finally { - // Buffer creator should sanitize - privKeyBuffer.fill(0); + if (Buffer.isBuffer(privKeyBuffer)) { + privKeyBuffer.fill(0); + } } + keysToSave.push(key); } if (keysToSave.length) { diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index f328563264f..b5a3babd16a 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -72,7 +72,6 @@ describe('Wallet', function() { if (walletName) { await Wallet.deleteWallet({ name: walletName, storageType }); } - walletName = undefined; sandbox.restore(); }); for (const chain of ['BTC', 'BCH', 'LTC', 'DOGE', 'ETH', 'XRP', 'MATIC']) { @@ -133,7 +132,7 @@ describe('Wallet', function() { password: 'abc123', storageType, baseUrl, - version: 0 + // version: 0 }); await wallet.unlock('abc123'); }); @@ -379,9 +378,79 @@ describe('Wallet', function() { sleepStub.callCount.should.equal(1); requestStub.args.flatMap(arg => arg[0].body).should.deep.equal(keys.map(k => ({ address: k.address }))); }); + + it('can derive the public key if not included', async function () { + const keys = []; + for (let i = 0; i < 101; i++) { + const pk = crypto.randomBytes(32).toString('hex'); + keys.push({ + privKey: pk, + address: libMap.BTC.PrivateKey(pk).toAddress().toString() + }); + } + const getPublicKeyStub = sandbox.stub(CWC.Deriver, 'getPublicKey').returns('mockedPubKey'); + sandbox.stub(wallet.storage, 'addKeysSafe').resolves(); + + await wallet.importKeys({ + keys, + rederiveAddys: false + }); + + getPublicKeyStub.callCount.should.be.greaterThan(0); + }); + + it('encrypts key.privKey only', async function () { + const keys = []; + for (let i = 0; i < 1; i++) { + const pk = crypto.randomBytes(32).toString('hex'); + keys.push({ + privKey: pk, + address: libMap.BTC.PrivateKey(pk).toAddress().toString() + }); + } + const addKeysSafeStub = sandbox.stub(wallet.storage, 'addKeysSafe').resolves(); + + await wallet.importKeys({ + keys, + rederiveAddys: false + }); + + addKeysSafeStub.calledOnce.should.equal(true); + const savedKeys = addKeysSafeStub.firstCall.args[0].keys; + + for (const originalKey of keys) { + const matchingSavedKey = savedKeys.find(sk => { + // Match on pubKey only if it was in originalKey + return !(originalKey.pubKey && sk.pubKey === originalKey.pubKey) && sk.address === originalKey.address; + }); + expect(matchingSavedKey).to.exist; + matchingSavedKey.privKey.should.not.equal(originalKey.privKey); + } + }); + + it('can rederive addresses', async function () { + const keys = []; + for (let i = 0; i < 1; i++) { + const pk = crypto.randomBytes(32).toString('hex'); + keys.push({ + privKey: pk, + address: libMap.BTC.PrivateKey(pk).toAddress().toString() + }); + } + const getPublicKeyStub = sandbox.stub(CWC.Deriver, 'getPublicKey').returns('mockedPubKey'); + const getAddressStub = sandbox.stub(CWC.Deriver, 'getAddress').returns('mockedAddress'); + sandbox.stub(wallet.storage, 'addKeysSafe').resolves(); + + await wallet.importKeys({ + keys, + rederiveAddys: true + }); + + getAddressStub.callCount.should.be.greaterThan(0); + }); }); - describe('signTx v2 key handling', function() { + describe('signTx', function() { let txStub: sinon.SinonStub; afterEach(async function() { sandbox.restore(); @@ -491,8 +560,6 @@ describe('Wallet', function() { }); }); - describe('signTx', function () {}); - describe('derivePrivateKey', function () {}); describe('unlock', function () { From 05e4d3323d9eb12ddaf732d6b16a87bcceebf00d Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Fri, 13 Mar 2026 15:21:37 -0400 Subject: [PATCH 41/51] refactor derivePrivateKey and test it --- packages/bitcore-client/src/wallet.ts | 16 +--- .../bitcore-client/test/unit/wallet.test.ts | 92 ++++++++++++++++++- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 48e526518b7..23641546de9 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -944,26 +944,14 @@ 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( + return Deriver.derivePrivateKey( this.chain, this.network, - masterKeyForDeriver, + this.unlocked.masterKey, addressIndex || 0, isChange, this.addressType ); - return keyToImport; } async nextAddressPair(withChangeAddress?: boolean) { diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index b5a3babd16a..fd30d4efb46 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -29,7 +29,7 @@ const libMap = { DOGE: CWC.BitcoreLibDoge }; -describe('Wallet', function() { +describe.only('Wallet', function() { const sandbox = sinon.createSandbox(); const storageType = 'Level'; const baseUrl = 'http://127.0.0.1:3000/api'; @@ -560,7 +560,95 @@ describe('Wallet', function() { }); }); - describe('derivePrivateKey', function () {}); + describe('derivePrivateKey', function () { + let wallet: Wallet; + const createUnlockedWallet = async (params: { chain: string; network: string; xpriv: string }) => { + walletName = `BitcoreClientTestDerivePrivateKey-${params.chain}`; + wallet = await Wallet.create({ + name: walletName, + chain: params.chain, + network: params.network, + xpriv: params.xpriv, + password: 'abc123', + storageType, + baseUrl + }); + await wallet.unlock('abc123'); + expect(Buffer.isBuffer(wallet.unlocked?.masterKey?.xprivkey)).to.equal(true); + expect(Buffer.isBuffer(wallet.unlocked?.masterKey?.privateKey)).to.equal(true); + return wallet; + }; + + const walletVectors = { + BTC: { + chain: 'BTC', + network: 'mainnet', + xpriv: 'xprv9s21ZrQH143K3aKdQ6kXF1vj7R6LtkoLCiUXfM5bdbGXmhQkC1iXdnFfrxAAtaTunPUCCLwUQ3cpNixGLMbLAH1gzeCr8VZDe4gPgmKLb2X', + expected: { + address: '14FubqQhpG1dhTSgD5nRsiQRJEEcxVojRf', + privKey: '79cab08ffc77750721329a0033c43fd1e5c32e9e2da273c18e5e36abb05cca32', + pubKey: '03d69dd136b999433a9f6c8f38076831ec0d3a3cf7a555bec8bc8c6d76fc266231', + path: 'm/0/0' + } + }, + ETH: { + chain: 'ETH', + network: 'mainnet', + xpriv: 'xprv9ypBjKErGMqCdzd44hfSdy1Vk6PGtU3si8ogZcow7rA23HTxMi9XfT99EKmiNdLMr9BAZ9S8ZKCYfN1eCmzYSmXYHje1jnYQseV1VJDDfdS', + expected: { + address: '0xb497281830dE4F19a3482AbF3D5C35c514e6fB36', + privKey: '62b8311c71f355c5c07f6bffe9b1ae60aa20d90e2e2ec93ec11b6014b2ae6340', + pubKey: '0386d153aad9395924631dbc78fa560107123a759eaa3e105958248c60cd4472ad', + path: 'm/0/0' + } + }, + XRP: { + chain: 'XRP', + network: 'mainnet', + xpriv: 'xprvA58pn8bWSyoRGvEY97ALTHP4Dj6t47Q3PTBUEw78CF91kALMwhs7D2GutQSvpRN6ACR4RX4HbF3KmF7zDf48gR8nwG7DqLp6ezUcMiPHDtV', + expected: { + address: 'r9dmAJBfBe7JL2RRLiFWGJ8kM4CHEeTpgN', + privKey: 'D02C6801D8F328FF2EAD51D01F9580AF36C8D74E2BD463963AC4ADBE51AE5F2C', + pubKey: '03DBEEC5E9E76DA09C5B502A67136BC2D73423E8902A7C35A8CBC0C5A6AC0469E8', + path: 'm/0/0' + } + }, + SOL: { + chain: 'SOL', + network: 'mainnet', + xpriv: 'xprv9s21ZrQH143K3aKdQ6kXF1vj7R6LtkoLCiUXfM5bdbGXmhQkC1iXdnFfrxAAtaTunPUCCLwUQ3cpNixGLMbLAH1gzeCr8VZDe4gPgmKLb2X', + expected: { + address: '7EWwMxKQa5Gru7oTcS1Wi3AaEgTfA6MU3z7MaLUT6hnD', + privKey: 'E4Tp4nTgMCa5dtGwqvkWoMGrJC7FKRNjcpeFFXi4nNb9', + pubKey: '5c9c85b20525ee81d3cc56da1f8307ec169086ae41458c5458519aced7683b66' + } + } + }; + + it('derives the expected BTC key material', async function () { + await createUnlockedWallet(walletVectors.BTC); + const result = await wallet.derivePrivateKey(false, 0); + expect(result).to.deep.equal(walletVectors.BTC.expected); + }); + + it('derives the expected ETH key material', async function () { + await createUnlockedWallet(walletVectors.ETH); + const result = await wallet.derivePrivateKey(false, 0); + expect(result).to.deep.equal(walletVectors.ETH.expected); + }); + + it('derives the expected XRP key material', async function () { + await createUnlockedWallet(walletVectors.XRP); + const result = await wallet.derivePrivateKey(false, 0); + expect(result).to.deep.equal(walletVectors.XRP.expected); + }); + + it('derives the expected SOL key material', async function () { + await createUnlockedWallet(walletVectors.SOL); + const result = await wallet.derivePrivateKey(false, 0); + expect(result).to.deep.equal(walletVectors.SOL.expected); + }); + }); describe('unlock', function () { it('performs wallet migration for previous wallet versions', async () => { From 49bc4aeccbadeec307973d935fc910f62f33ced4 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Fri, 13 Mar 2026 15:24:54 -0400 Subject: [PATCH 42/51] add XRP and SOL signing tests --- .../bitcore-client/test/unit/wallet.test.ts | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index fd30d4efb46..b8ff6c10de0 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -29,7 +29,7 @@ const libMap = { DOGE: CWC.BitcoreLibDoge }; -describe.only('Wallet', function() { +describe('Wallet', function() { const sandbox = sinon.createSandbox(); const storageType = 'Level'; const baseUrl = 'http://127.0.0.1:3000/api'; @@ -558,6 +558,94 @@ describe.only('Wallet', function() { capturedPayload.keys[0].privKey.should.equal(privHex); }); }); + + describe('XRP (account) decrypts ciphertext to uppercase hex and skips plaintext', function() { + walletName = 'BitcoreClientTestSignTxV2-XRP'; + let wallet: Wallet; + + beforeEach(async function() { + wallet = await Wallet.create({ + name: walletName, + chain: 'XRP', + 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 uppercase hex privKey to Transactions.sign', async function() { + const privHex = crypto.randomBytes(32).toString('hex').toUpperCase(); + const privBufForPubKey = CWC.Deriver.privateKeyToBuffer('XRP', privHex); + const pubKey = CWC.Deriver.getPublicKey('XRP', wallet.network, privBufForPubKey); + privBufForPubKey.fill(0); + const privBuf = CWC.Deriver.privateKeyToBuffer('XRP', privHex); + const encPriv = Encryption.encryptBuffer(privBuf, 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: 'rabc', privKey: encPriv, pubKey }]; + await wallet.signTx({ tx: 'raw', signingKeys }); + + txStub.calledOnce.should.equal(true); + capturedPayload.keys[0].privKey.should.equal(privHex); + }); + }); + + describe('SOL (account) decrypts ciphertext to base58 and skips plaintext', function() { + walletName = 'BitcoreClientTestSignTxV2-SOL'; + let wallet: Wallet; + + beforeEach(async function() { + wallet = await Wallet.create({ + name: walletName, + chain: 'SOL', + network: 'devnet', + 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 base58 privKey to Transactions.sign', async function() { + const privBufForPubKey = crypto.randomBytes(32); + const pubKey = CWC.Deriver.getPublicKey('SOL', wallet.network, privBufForPubKey); + const privBuf = Buffer.from(privBufForPubKey); + privBufForPubKey.fill(0); + const encPriv = Encryption.encryptBuffer(privBuf, pubKey, wallet.unlocked.encryptionKey).toString('hex'); + const expectedPrivKey = CWC.Deriver.bufferToPrivateKey_TEMP('SOL', wallet.network, Buffer.from(privBuf)); + privBuf.fill(0); + + let capturedPayload; + txStub = sandbox.stub(CWC.Transactions, 'sign').callsFake(payload => { + capturedPayload = payload; + return Promise.resolve('signed'); + }); + + const signingKeys = [{ address: pubKey, privKey: encPriv, pubKey }]; + await wallet.signTx({ tx: 'raw', signingKeys }); + + txStub.calledOnce.should.equal(true); + capturedPayload.keys[0].privKey.should.equal(expectedPrivKey); + }); + }); }); describe('derivePrivateKey', function () { From 1a2b91b78b291f6d7d8fcabc7872279d023a5c2f Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 17 Mar 2026 09:33:53 -0400 Subject: [PATCH 43/51] remove duplicate import statement --- packages/bitcore-client/src/wallet.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 4cfed6facfc..75e5964f4fc 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -17,7 +17,6 @@ import { xrpl } from '@bitpay-labs/crypto-wallet-core'; import * as Bcrypt from 'bcrypt'; -import * as Bcrypt from 'bcrypt'; import Mnemonic from 'bitcore-mnemonic'; import { Client } from './client'; import { Encryption } from './encryption'; From b5640ebe14b4237ed52092ccb8091c3fc6ab536d Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 17 Mar 2026 09:53:05 -0400 Subject: [PATCH 44/51] fix import statement --- packages/crypto-wallet-core/test/deriver.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/crypto-wallet-core/test/deriver.test.ts b/packages/crypto-wallet-core/test/deriver.test.ts index 4dd1a88441b..d3b34f68a51 100644 --- a/packages/crypto-wallet-core/test/deriver.test.ts +++ b/packages/crypto-wallet-core/test/deriver.test.ts @@ -1,8 +1,7 @@ import { expect } from 'chai'; import crypto from 'crypto'; import { Deriver } from '../src'; -import BitcoreLib from 'bitcore-lib'; -import { encoding } from 'bitcore-lib'; +import BitcoreLib, { encoding } from '@bitpay-labs/bitcore-lib'; import * as ed25519 from 'ed25519-hd-key'; describe('Deriver.getPublicKey (Buffer-first)', () => { From a1ad584ac28b630892d2a265d14282802817ce9f Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 17 Mar 2026 09:59:33 -0400 Subject: [PATCH 45/51] fix import statements --- packages/bitcore-client/src/wallet.ts | 2 +- packages/bitcore-client/test/unit/wallet.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index 75e5964f4fc..f7e34759261 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -2,6 +2,7 @@ import { writeFile } from 'fs/promises'; import 'source-map-support/register'; import os from 'os'; import path from 'path'; +import Mnemonic from '@bitpay-labs/bitcore-mnemonic'; import { BitcoreLib, BitcoreLibCash, @@ -17,7 +18,6 @@ import { xrpl } from '@bitpay-labs/crypto-wallet-core'; import * as Bcrypt from 'bcrypt'; -import Mnemonic from 'bitcore-mnemonic'; import { Client } from './client'; import { Encryption } from './encryption'; import { Storage } from './storage'; diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index b8ff6c10de0..1253b2eecb4 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -1,5 +1,5 @@ import * as chai from 'chai'; -import * as CWC from 'crypto-wallet-core'; +import * as CWC from '@bitpay-labs/crypto-wallet-core'; import fs from 'fs'; import os from 'os'; import path from 'path'; From 7745ae4160c45df59578a855dcf8598400726362 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 17 Mar 2026 13:24:13 -0400 Subject: [PATCH 46/51] fix getPublicKey methods to return compressed public keys --- .../src/derivation/eth/index.ts | 5 +- .../src/derivation/xrp/index.ts | 5 +- .../crypto-wallet-core/test/deriver.test.ts | 165 ++++++++++++------ 3 files changed, 119 insertions(+), 56 deletions(-) diff --git a/packages/crypto-wallet-core/src/derivation/eth/index.ts b/packages/crypto-wallet-core/src/derivation/eth/index.ts index 92e4e8d094a..3b24c53a441 100644 --- a/packages/crypto-wallet-core/src/derivation/eth/index.ts +++ b/packages/crypto-wallet-core/src/derivation/eth/index.ts @@ -56,11 +56,12 @@ export class EthDeriver implements IDeriver { return this.addressFromPublicKeyBuffer(pubKey.toBuffer()); } - getPublicKey(_network: string, privKey: Buffer): string { + getPublicKey(network: string, privKey: Buffer): string { if (!Buffer.isBuffer(privKey)) { throw new Error('Expected privKey to be a Buffer'); } - const key = BitcoreLib.PrivateKey.fromBuffer(privKey); + const bn = BitcoreLib.crypto.BN.fromBuffer(privKey); + const key = new BitcoreLib.PrivateKey({ bn, network, compressed: true }); return key.publicKey.toString('hex'); } diff --git a/packages/crypto-wallet-core/src/derivation/xrp/index.ts b/packages/crypto-wallet-core/src/derivation/xrp/index.ts index 5bbe8f058df..a5050cb27bb 100644 --- a/packages/crypto-wallet-core/src/derivation/xrp/index.ts +++ b/packages/crypto-wallet-core/src/derivation/xrp/index.ts @@ -36,11 +36,12 @@ export class XrpDeriver implements IDeriver { return address; } - getPublicKey(_network: string, privKey: Buffer): string { + getPublicKey(network: string, privKey: Buffer): string { if (!Buffer.isBuffer(privKey)) { throw new Error('Expected privKey to be a Buffer'); } - const key = BitcoreLib.PrivateKey.fromBuffer(privKey); + const bn = BitcoreLib.crypto.BN.fromBuffer(privKey); + const key = new BitcoreLib.PrivateKey({ bn, network, compressed: true }); return key.publicKey.toString('hex').toUpperCase(); } diff --git a/packages/crypto-wallet-core/test/deriver.test.ts b/packages/crypto-wallet-core/test/deriver.test.ts index d3b34f68a51..f046d370694 100644 --- a/packages/crypto-wallet-core/test/deriver.test.ts +++ b/packages/crypto-wallet-core/test/deriver.test.ts @@ -1,65 +1,126 @@ import { expect } from 'chai'; import crypto from 'crypto'; import { Deriver } from '../src'; -import BitcoreLib, { encoding } from '@bitpay-labs/bitcore-lib'; -import * as ed25519 from 'ed25519-hd-key'; -describe('Deriver.getPublicKey (Buffer-first)', () => { - it('BTC: should derive pubKey from privKey buffer', () => { - const privHex = crypto.randomBytes(32).toString('hex'); - const privBuf = Deriver.privateKeyToBuffer('BTC', privHex); - try { - const pubKey = Deriver.getPublicKey('BTC', 'testnet', privBuf); - const expected = new BitcoreLib.PrivateKey(privHex).publicKey.toString(); - expect(pubKey).to.equal(expected); - } finally { - privBuf.fill(0); - } - }); +describe.only('IDeriver', function () { + describe('getPublicKey (Buffer-first)', () => { + it('BTC: should derive the compressed secp256k1 public key', () => { + // Well-known secp256k1 test vector: private key 1 maps to generator point G. + const privHex = '0000000000000000000000000000000000000000000000000000000000000001'; + const expectedPubKey = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'; + const privBuf = Deriver.privateKeyToBuffer('BTC', privHex); + try { + const pubKey = Deriver.getPublicKey('BTC', 'testnet', privBuf); - it('ETH: privateKeyToBuffer should accept with/without 0x, and getPublicKey should match', () => { - const privHex = crypto.randomBytes(32).toString('hex'); - const privBufNo0x = Deriver.privateKeyToBuffer('ETH', privHex); - const privBuf0x = Deriver.privateKeyToBuffer('ETH', `0x${privHex}`); - try { - expect(privBuf0x.equals(privBufNo0x)).to.equal(true); + expect(pubKey).to.equal(expectedPubKey); + expect(pubKey).to.match(/^(02|03)/); + expect(pubKey).to.have.length(66); + } finally { + privBuf.fill(0); + } + }); + + it('ETH: should derive the compressed pubKey used by wallet imports', () => { + // Vetted fixture from `test/address.test.ts`. + const privHex = '62b8311c71f355c5c07f6bffe9b1ae60aa20d90e2e2ec93ec11b6014b2ae6340'; + const expectedPubKey = '0386d153aad9395924631dbc78fa560107123a759eaa3e105958248c60cd4472ad'; + const expectedAddress = '0xb497281830dE4F19a3482AbF3D5C35c514e6fB36'; + const privBuf = Deriver.privateKeyToBuffer('ETH', privHex); + try { + const pubKey = Deriver.getPublicKey('ETH', 'mainnet', privBuf); - const pubKey = Deriver.getPublicKey('ETH', 'mainnet', privBufNo0x); - const expected = new BitcoreLib.PrivateKey(privHex).publicKey.toString('hex'); - expect(pubKey).to.equal(expected); - } finally { - privBufNo0x.fill(0); - privBuf0x.fill(0); - } - }); + expect(pubKey).to.equal(expectedPubKey); + expect(pubKey).to.match(/^(02|03)/); + expect(pubKey).to.have.length(66); + expect(Deriver.getAddress('ETH', 'mainnet', pubKey)).to.equal(expectedAddress); + } finally { + privBuf.fill(0); + } + }); + + it('XRP: should derive the compressed pubKey used by wallet imports', () => { + // Vetted fixture from `test/address.test.ts`. + const privHex = 'D02C6801D8F328FF2EAD51D01F9580AF36C8D74E2BD463963AC4ADBE51AE5F2C'; + const expectedPubKey = '03DBEEC5E9E76DA09C5B502A67136BC2D73423E8902A7C35A8CBC0C5A6AC0469E8'; + const expectedAddress = 'r9dmAJBfBe7JL2RRLiFWGJ8kM4CHEeTpgN'; + const privBuf = Deriver.privateKeyToBuffer('XRP', privHex); + try { + const pubKey = Deriver.getPublicKey('XRP', 'mainnet', privBuf); + + expect(pubKey).to.equal(expectedPubKey); + expect(pubKey).to.match(/^(02|03)/); + expect(pubKey).to.have.length(66); + expect(Deriver.getAddress('XRP', 'mainnet', pubKey)).to.equal(expectedAddress); + } finally { + privBuf.fill(0); + } + }); + + it('SOL: should derive the public key used by wallet imports', () => { + // Vetted fixture from `test/address.test.ts`. + const privKey = 'E4Tp4nTgMCa5dtGwqvkWoMGrJC7FKRNjcpeFFXi4nNb9'; + const expectedPubKey = '5c9c85b20525ee81d3cc56da1f8307ec169086ae41458c5458519aced7683b66'; + const expectedAddress = '7EWwMxKQa5Gru7oTcS1Wi3AaEgTfA6MU3z7MaLUT6hnD'; + const privBuf = Deriver.privateKeyToBuffer('SOL', privKey); + try { + const pubKey = Deriver.getPublicKey('SOL', 'mainnet', privBuf); - it('XRP: should derive uppercase hex pubKey from privKey buffer', () => { - const privHex = crypto.randomBytes(32).toString('hex').toUpperCase(); - const privBuf = Deriver.privateKeyToBuffer('XRP', privHex); - try { - const pubKey = Deriver.getPublicKey('XRP', 'mainnet', privBuf); - const expected = new BitcoreLib.PrivateKey(privHex.toLowerCase()).publicKey.toString('hex').toUpperCase(); - expect(pubKey).to.equal(expected); - } finally { - privBuf.fill(0); - } + expect(pubKey).to.equal(expectedPubKey); + expect(pubKey).to.have.length(64); + expect(Deriver.getAddress('SOL', 'mainnet', pubKey)).to.equal(expectedAddress); + } finally { + privBuf.fill(0); + } + }); }); - it('SOL: should derive pubKey hex from privKey buffer', () => { - const seed = crypto.randomBytes(32); - const seed58 = encoding.Base58.encode(seed); - const privBuf = Deriver.privateKeyToBuffer('SOL', seed58); - try { - // ensure the base58 path yields the original bytes - expect(Buffer.compare(privBuf, seed)).to.equal(0); + describe('privateKeyToBuffer', function () { + it('ETH: should accept with/without 0x', () => { + const privHex = crypto.randomBytes(32).toString('hex'); + const privBufNo0x = Deriver.privateKeyToBuffer('ETH', privHex); + const privBuf0x = Deriver.privateKeyToBuffer('ETH', `0x${privHex}`); + try { + expect(privBuf0x.equals(privBufNo0x)).to.be.true; + } finally { + privBufNo0x.fill(0); + privBuf0x.fill(0); + } + }); + it('ETH: hex-stringified buffer output equals input', () => { + const privHex = crypto.randomBytes(32).toString('hex'); + const privBuf = Deriver.privateKeyToBuffer('ETH', privHex); + try { + expect(privBuf.toString('hex')).to.equal(privHex); + } finally { + privBuf.fill(0); + } + }); - const pubKey = Deriver.getPublicKey('SOL', 'mainnet', privBuf); - const expected = Buffer.from(ed25519.getPublicKey(seed, false)).toString('hex'); - expect(pubKey).to.equal(expected); - } finally { - seed.fill(0); - privBuf.fill(0); - } + it('XRP: should accept uppercase/lowercase hex', () => { + // Vetted fixture from `test/address.test.ts`. + const privHexUpper = 'D02C6801D8F328FF2EAD51D01F9580AF36C8D74E2BD463963AC4ADBE51AE5F2C'; + const privHexLower = privHexUpper.toLowerCase(); + const privBufUpper = Deriver.privateKeyToBuffer('XRP', privHexUpper); + const privBufLower = Deriver.privateKeyToBuffer('XRP', privHexLower); + try { + expect(privBufUpper.equals(privBufLower)).to.be.true; + } finally { + privBufUpper.fill(0); + privBufLower.fill(0); + } + }); + + it('XRP: hex-stringified buffer output equals fixture bytes', () => { + // Vetted fixture from `test/address.test.ts`. + const privHexUpper = 'D02C6801D8F328FF2EAD51D01F9580AF36C8D74E2BD463963AC4ADBE51AE5F2C'; + const privBuf = Deriver.privateKeyToBuffer('XRP', privHexUpper); + try { + expect(privBuf.toString('hex')).to.equal(privHexUpper.toLowerCase()); + } finally { + privBuf.fill(0); + } + }); }); }); + From b07f3a5ebb5a3dfbf03dba7cb892f3ef7459db7a Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Tue, 17 Mar 2026 14:43:01 -0400 Subject: [PATCH 47/51] rm .only on test --- packages/crypto-wallet-core/test/deriver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/crypto-wallet-core/test/deriver.test.ts b/packages/crypto-wallet-core/test/deriver.test.ts index f046d370694..9fea11f2d2d 100644 --- a/packages/crypto-wallet-core/test/deriver.test.ts +++ b/packages/crypto-wallet-core/test/deriver.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import crypto from 'crypto'; import { Deriver } from '../src'; -describe.only('IDeriver', function () { +describe('IDeriver', function () { describe('getPublicKey (Buffer-first)', () => { it('BTC: should derive the compressed secp256k1 public key', () => { // Well-known secp256k1 test vector: private key 1 maps to generator point G. From 476375785007f4791d9d00b5bc5c065641883eaf Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 19 Mar 2026 17:36:15 -0400 Subject: [PATCH 48/51] fix lack of mkdir in wallet migration and update tests --- packages/bitcore-client/src/wallet.ts | 22 +++++++++-------- .../bitcore-client/test/unit/wallet.test.ts | 24 ++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index f7e34759261..b9b2a19b2f6 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -1,4 +1,4 @@ -import { writeFile } from 'fs/promises'; +import { mkdir, writeFile } from 'fs/promises'; import 'source-map-support/register'; import os from 'os'; import path from 'path'; @@ -331,20 +331,21 @@ export class Wallet { } async migrateWallet(encryptionKey: Buffer): Promise { + const preMigrationVersion = this.version ?? 1; /** * 0: Checks */ - if (this.version == CURRENT_WALLET_VERSION) { + if (preMigrationVersion == 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}`); + if (preMigrationVersion > CURRENT_WALLET_VERSION) { + console.warn(`Wallet version ${preMigrationVersion} greater than expected current wallet version ${CURRENT_WALLET_VERSION}`); return this; } - console.log(`Migrating wallet from version ${this.version} to version ${CURRENT_WALLET_VERSION}`); + console.log(`Migrating wallet from version ${preMigrationVersion} to version ${CURRENT_WALLET_VERSION}`); /** * 1: Wallet to .bak @@ -357,16 +358,17 @@ export class Wallet { const backupDir = path.join( os.homedir(), '.bitcore', - 'bitcoreWallets', + 'bitcoreWallet', 'backup' ); + await mkdir(backupDir, { recursive: true }); - const walletFilePath = path.join(backupDir, `${this.name}.bak`); + const walletFilePath = path.join(backupDir, `${this.name}.v${preMigrationVersion}.bak`); await writeFile(walletFilePath, rawWallet, 'utf8') .then(() => console.log(`Pre-migration wallet backup written to ${walletFilePath}`)) .catch(err => { - console.error('Wallet backup failed, aborting migration', err.msg); + console.error('Wallet backup failed, aborting migration', err.message); throw new Error('Migration failure: failed to write wallet backup file. Aborting.'); }); @@ -381,11 +383,11 @@ export class Wallet { // Back up keys (enc) const backupKeysStr = JSON.stringify(storedKeys); - const keysFilePath = path.join(backupDir, `${this.name}_keys.bak`); + const keysFilePath = path.join(backupDir, `${this.name}_keys.v${preMigrationVersion}.bak`); await writeFile(keysFilePath, backupKeysStr, 'utf8') .then(() => console.log(`Pre-migration keys backup written to ${keysFilePath}`)) .catch(err => { - console.error('Keys backup failed, aborting migration', err.msg); + console.error('Keys backup failed, aborting migration', err.message); throw new Error('Migration failure: failed to write keys backup file. Aborting.'); }); diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index 1253b2eecb4..0fd4938864e 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -34,6 +34,7 @@ describe('Wallet', function() { const storageType = 'Level'; const baseUrl = 'http://127.0.0.1:3000/api'; let walletName; + let skipWalletCleanup = false; let wallet: Wallet; let api; before(async function() { @@ -69,9 +70,10 @@ describe('Wallet', function() { }); }); afterEach(async function() { - if (walletName) { + if (walletName && !skipWalletCleanup) { await Wallet.deleteWallet({ name: walletName, storageType }); } + skipWalletCleanup = false; sandbox.restore(); }); for (const chain of ['BTC', 'BCH', 'LTC', 'DOGE', 'ETH', 'XRP', 'MATIC']) { @@ -740,10 +742,11 @@ describe('Wallet', function() { describe('unlock', function () { it('performs wallet migration for previous wallet versions', async () => { + skipWalletCleanup = true; const fixture = ethMigrationTestWalletFixture; const wallet = new Wallet(fixture.wallet as any); const tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitcore-client-migration-unlock-')); - const backupDir = path.join(tempHomeDir, '.bitcore', 'bitcoreWallets', 'backup'); + const backupDir = path.join(tempHomeDir, '.bitcore', 'bitcoreWallet', 'backup'); const loadWalletStub = sandbox.stub(); const getStoredKeysStub = sandbox.stub(); const addKeysSafeStub = sandbox.stub(); @@ -761,7 +764,6 @@ describe('Wallet', function() { addKeysSafeStub.resolves(); saveWalletStub.resolves(); - fs.mkdirSync(backupDir, { recursive: true }); const homedirStub = sandbox.stub(os, 'homedir').returns(tempHomeDir); sandbox.stub(wallet, 'getAddresses').resolves(fixture.addresses); @@ -776,8 +778,8 @@ describe('Wallet', function() { expect(Buffer.isBuffer(wallet.unlocked?.encryptionKey)).to.equal(true); expect(Buffer.isBuffer(wallet.unlocked?.masterKey?.privateKey)).to.equal(true); expect(Buffer.isBuffer(wallet.unlocked?.masterKey?.xprivkey)).to.equal(true); - expect(fs.readFileSync(path.join(backupDir, `${fixture.name}.bak`), 'utf8')).to.equal(fixture.rawWallet); - expect(fs.readFileSync(path.join(backupDir, `${fixture.name}_keys.bak`), 'utf8')).to.equal(JSON.stringify(fixture.storedKeys)); + expect(fs.readFileSync(path.join(backupDir, `${fixture.name}.v1.bak`), 'utf8')).to.equal(fixture.rawWallet); + expect(fs.readFileSync(path.join(backupDir, `${fixture.name}_keys.v1.bak`), 'utf8')).to.equal(JSON.stringify(fixture.storedKeys)); homedirStub.restore(); fs.rmSync(tempHomeDir, { recursive: true, force: true }); }); @@ -796,10 +798,10 @@ describe('Wallet', function() { beforeEach(async function () { + skipWalletCleanup = true; wallet = new Wallet(ethMigrationTestWalletFixture.wallet as any); tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitcore-client-migration-')); - backupDir = path.join(tempHomeDir, '.bitcore', 'bitcoreWallets', 'backup'); - fs.mkdirSync(backupDir, { recursive: true }); + backupDir = path.join(tempHomeDir, '.bitcore', 'bitcoreWallet', 'backup'); wallet.storage = { loadWallet: async () => undefined, getStoredKeys: async () => [], @@ -836,10 +838,10 @@ describe('Wallet', function() { addresses: ethMigrationTestWalletFixture.addresses, name: ethMigrationTestWalletFixture.name })).to.be.true; - expect(fs.readFileSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.bak`), 'utf8')).to.equal( + expect(fs.readFileSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.v1.bak`), 'utf8')).to.equal( ethMigrationTestWalletFixture.rawWallet ); - expect(fs.readFileSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}_keys.bak`), 'utf8')).to.equal( + expect(fs.readFileSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}_keys.v1.bak`), 'utf8')).to.equal( JSON.stringify(ethMigrationTestWalletFixture.storedKeys) ); }); @@ -970,7 +972,7 @@ describe('Wallet', function() { expect(getStoredKeysStub.called).to.equal(false); expect(addKeysSafeStub.called).to.equal(false); expect(saveWalletStub.called).to.equal(false); - expect(fs.existsSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.bak`))).to.equal(false); + expect(fs.existsSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.v2.bak`))).to.equal(false); }); it('should no-op when the wallet version is newer than the current version', async () => { @@ -987,7 +989,7 @@ describe('Wallet', function() { expect(getStoredKeysStub.called).to.equal(false); expect(addKeysSafeStub.called).to.equal(false); expect(saveWalletStub.called).to.equal(false); - expect(fs.existsSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.bak`))).to.equal(false); + expect(fs.existsSync(path.join(backupDir, `${ethMigrationTestWalletFixture.name}.v3.bak`))).to.equal(false); }); }); From 0266e6a5f4c832804c8ae0fbcbce0a3d6199c266 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 19 Mar 2026 17:41:30 -0400 Subject: [PATCH 49/51] refactor wallet tests to make them less brittle --- .../bitcore-client/test/unit/wallet.test.ts | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index 0fd4938864e..9f9a4a2dead 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -3,7 +3,7 @@ import * as CWC from '@bitpay-labs/crypto-wallet-core'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { AddressTypes, Wallet } from '../../src/wallet'; +import { AddressTypes, IWalletExt, 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'; @@ -34,9 +34,21 @@ describe('Wallet', function() { const storageType = 'Level'; const baseUrl = 'http://127.0.0.1:3000/api'; let walletName; - let skipWalletCleanup = false; let wallet: Wallet; let api; + const walletsToCleanup: Array<{ name: string; storageType?: StorageType; path?: string }> = []; + const createWalletForTest = async (params: Partial) => { + if (!params.name) { + throw new Error('Tests must provide a wallet name'); + } + const createdWallet = await Wallet.create(params); + walletsToCleanup.push({ + name: params.name, + storageType: params.storageType, + path: params.path + }); + return createdWallet; + }; before(async function() { this.timeout(20000); await bcnStorage.start({ @@ -70,10 +82,12 @@ describe('Wallet', function() { }); }); afterEach(async function() { - if (walletName && !skipWalletCleanup) { - await Wallet.deleteWallet({ name: walletName, storageType }); + while (walletsToCleanup.length) { + const walletToCleanup = walletsToCleanup.pop(); + if (walletToCleanup) { + await Wallet.deleteWallet(walletToCleanup); + } } - skipWalletCleanup = false; sandbox.restore(); }); for (const chain of ['BTC', 'BCH', 'LTC', 'DOGE', 'ETH', 'XRP', 'MATIC']) { @@ -82,7 +96,7 @@ describe('Wallet', function() { it(`should create a wallet for chain and addressType: ${chain} ${addressType}`, async function() { walletName = 'BitcoreClientTest' + chain + addressType; - wallet = await Wallet.create({ + wallet = await createWalletForTest({ chain, network: 'mainnet', name: walletName, @@ -126,7 +140,7 @@ describe('Wallet', function() { walletName = 'BitcoreClientTestBumpFee-UTXO'; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'BTC', network: 'testnet', @@ -203,7 +217,7 @@ describe('Wallet', function() { walletName = 'BitcoreClientTestBumpFee-EVM'; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'ETH', network: 'testnet', @@ -309,7 +323,7 @@ describe('Wallet', function() { let sleepStub; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'BTC', network: 'testnet', @@ -463,7 +477,7 @@ describe('Wallet', function() { let wallet: Wallet; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'BTC', network: 'testnet', @@ -475,10 +489,6 @@ describe('Wallet', function() { 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(); @@ -522,7 +532,7 @@ describe('Wallet', function() { let wallet: Wallet; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'ETH', network: 'testnet', @@ -534,10 +544,6 @@ describe('Wallet', function() { 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 privBufForPubKey = CWC.Deriver.privateKeyToBuffer('ETH', privHex); @@ -566,7 +572,7 @@ describe('Wallet', function() { let wallet: Wallet; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'XRP', network: 'testnet', @@ -578,10 +584,6 @@ describe('Wallet', function() { await wallet.unlock('abc123'); }); - afterEach(async function() { - await Wallet.deleteWallet({ name: walletName, storageType }); - }); - it('should decrypt stored ciphertext and hand uppercase hex privKey to Transactions.sign', async function() { const privHex = crypto.randomBytes(32).toString('hex').toUpperCase(); const privBufForPubKey = CWC.Deriver.privateKeyToBuffer('XRP', privHex); @@ -610,7 +612,7 @@ describe('Wallet', function() { let wallet: Wallet; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'SOL', network: 'devnet', @@ -622,10 +624,6 @@ describe('Wallet', function() { await wallet.unlock('abc123'); }); - afterEach(async function() { - await Wallet.deleteWallet({ name: walletName, storageType }); - }); - it('should decrypt stored ciphertext and hand base58 privKey to Transactions.sign', async function() { const privBufForPubKey = crypto.randomBytes(32); const pubKey = CWC.Deriver.getPublicKey('SOL', wallet.network, privBufForPubKey); @@ -654,7 +652,7 @@ describe('Wallet', function() { let wallet: Wallet; const createUnlockedWallet = async (params: { chain: string; network: string; xpriv: string }) => { walletName = `BitcoreClientTestDerivePrivateKey-${params.chain}`; - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: params.chain, network: params.network, @@ -742,7 +740,6 @@ describe('Wallet', function() { describe('unlock', function () { it('performs wallet migration for previous wallet versions', async () => { - skipWalletCleanup = true; const fixture = ethMigrationTestWalletFixture; const wallet = new Wallet(fixture.wallet as any); const tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitcore-client-migration-unlock-')); @@ -798,7 +795,6 @@ describe('Wallet', function() { beforeEach(async function () { - skipWalletCleanup = true; wallet = new Wallet(ethMigrationTestWalletFixture.wallet as any); tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitcore-client-migration-')); backupDir = path.join(tempHomeDir, '.bitcore', 'bitcoreWallet', 'backup'); @@ -996,7 +992,7 @@ describe('Wallet', function() { describe('getBalance', function() { walletName = 'BitcoreClientTestGetBalance'; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'MATIC', network: 'testnet', @@ -1046,7 +1042,7 @@ describe('Wallet', function() { describe('getTokenObj', function() { walletName = 'BitcoreClientTestGetTokenObj'; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ name: walletName, chain: 'MATIC', network: 'testnet', @@ -1125,7 +1121,7 @@ describe('Wallet', function() { }; beforeEach(async function() { - wallet = await Wallet.create({ + wallet = await createWalletForTest({ chain: 'ETH', network: 'mainnet', name: walletName, From 05b7f7c3f50b82a91aa40abe5f0827bd2029b2e3 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 9 Apr 2026 09:23:06 -0400 Subject: [PATCH 50/51] Merge branch 'fix/address-migration' --- packages/bitcore-client/src/storage/mongo.ts | 6 +- .../bitcore-client/src/storage/textFile.ts | 102 ++++++++++++++++++ packages/bitcore-client/src/wallet.ts | 21 +++- 3 files changed, 123 insertions(+), 6 deletions(-) diff --git a/packages/bitcore-client/src/storage/mongo.ts b/packages/bitcore-client/src/storage/mongo.ts index d0dc10d78c4..bce051a483e 100644 --- a/packages/bitcore-client/src/storage/mongo.ts +++ b/packages/bitcore-client/src/storage/mongo.ts @@ -157,7 +157,11 @@ export class Mongo { await this.init(); } const { name, key, toStore } = params; - await this.addressCollection.insertOne({ name, address: key.address, data: toStore }); + await this.addressCollection.updateOne( + { name, address: key.address }, + { $set: { name, address: key.address, data: toStore } }, + { upsert: true } + ); if (!params.keepAlive) { await this.close(); } diff --git a/packages/bitcore-client/src/storage/textFile.ts b/packages/bitcore-client/src/storage/textFile.ts index fec046cd6a6..3eda8bd3a2c 100644 --- a/packages/bitcore-client/src/storage/textFile.ts +++ b/packages/bitcore-client/src/storage/textFile.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as stream from 'stream'; +import { pipeline } from 'stream/promises'; import { IWallet } from 'src/types/wallet'; import { StreamUtil } from '../stream-util'; import { Wallet } from '../wallet'; @@ -193,6 +194,14 @@ export class TextFile { async addKeys(params: { name: string; key: any; toStore: string; keepAlive: boolean; open: boolean }) { const { name, key, toStore } = params; + /** + * `addresses.txt` is stored as JSONL: one address record per line. + * + * That line-oriented contract matters beyond normal reads/writes. The migration + * rewrite path reparses the file as individual JSON records, transforms selected + * records, and writes the file back out. If this writer ever stops emitting exactly + * one serialized address object per line, `migrate()` would need to change too. + */ const objectToSave = { name, address: key.address, toStore }; return new Promise(resolve => { const readStream = new stream.Readable({ objectMode: true }); @@ -206,6 +215,99 @@ export class TextFile { }); } + /** + * Rewrite the address file in place for a specific migration version. + * + * Why a transformer? + * The TextFile backend is append-only. That is simple for normal writes, but it means + * migration cannot "overwrite" an existing address entry by appending a new one because + * `getKey()` still resolves the first matching line it encounters. For migration we + * therefore treat `addresses.txt` like a stream of JSONL records: + * + * 1. Read bytes from the existing file. + * 2. Parse each JSONL line into an object. + * 3. Pass each object through a Transform stream that replaces only the records that + * belong to this wallet/address set. + * 4. Serialize the transformed objects back to JSONL. + * 5. Write the result to a temporary file and atomically rename it into place. + * + * This lets us preserve the file order and all unrelated records while updating the + * migrated addresses in a single pass. + */ + async migrate(params: { version: number; name: string; keys: any[] }) { + const { version, name, keys } = params; + if (version !== 1) { + throw new Error(`TextFile migration for wallet version ${version} is not supported`); + } + + const replacementRecords = new Map( + keys.map(key => { + const { path, pubKey } = key; + if (!pubKey) { + throw new Error(`pubKey is undefined for ${name}. TextFile migration aborted`); + } + const toStore = JSON.stringify({ key: JSON.stringify(key), pubKey, path }); + return [key.address, { name, address: key.address, toStore }]; + }) + ); + + const tempAddressFileName = `${this.addressFileName}.v${version}.migrating`; + + /** + * The transform stream is the "rewrite policy" for migration. + * + * Each record coming in is one parsed JSON object from the source file. + * We either: + * - push it through unchanged, or + * - replace it with the migrated version for the same wallet/address. + * + * The important thing to notice is that Transform streams do not need to change the + * number of records. They can simply emit a modified version of the incoming record, + * which makes them a nice fit for "rewrite this file in one pass" jobs like this. + */ + const migrationTransform = new stream.Transform({ + writableObjectMode: true, + readableObjectMode: true, + transform(record, _encoding, callback) { + try { + // Not target wallet - pass through unchanged + if (record.name !== name) { + this.push(record); + callback(); + return; + } + + const replacement = replacementRecords.get(record.address); + /** + * If replacement not in map, indicates a prior-existing address which was not included in the keys to migrate. + * Fall back to prior. + */ + if (!replacement) { + console.warn(`TextFile migration warning: found address ${record.address} for wallet ${name}, but no replacement key was provided. Keeping original record.`); + } + this.push(replacement ?? record); + callback(); + } catch (err) { + callback(err as Error); + } + } + }); + + try { + await pipeline( + fs.createReadStream(this.addressFileName, { flags: 'r', encoding: 'utf8' }), + StreamUtil.jsonlBufferToObjectMode(), + migrationTransform, + StreamUtil.objectModeToJsonlBuffer(), + fs.createWriteStream(tempAddressFileName, { flags: 'w', encoding: 'utf8' }) + ); + await fs.promises.rename(tempAddressFileName, this.addressFileName); + } catch (err) { + await fs.promises.rm(tempAddressFileName, { force: true }).catch(() => undefined); + throw err; + } + } + async getAddress(params: { name: string; address: string; keepAlive: boolean; open: boolean }) { const { name, address, keepAlive, open } = params; const key: any = await this.getKey({ name, address, keepAlive, open }); diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index b9b2a19b2f6..450adb97f10 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -21,6 +21,7 @@ import * as Bcrypt from 'bcrypt'; import { Client } from './client'; import { Encryption } from './encryption'; import { Storage } from './storage'; +import { TextFile } from './storage/textFile'; import { ParseApiStream } from './stream-util'; import { StorageType } from './types/storage'; import { BumpTxFeeType, IWallet, KeyImport } from './types/wallet'; @@ -230,6 +231,14 @@ export class Wallet { storageType }); + if (!xpriv) { + process.stdout.write(Buffer.from(mnemonic.phrase)); + process.stdout.write(os.EOL); + } else { + process.stdout.write(hdPrivKey.toBuffer()); + process.stdout.write(os.EOL); + } + await loadedWallet.register().catch(e => { console.debug(e); console.error('Failed to register wallet with bitcore-node.'); @@ -443,11 +452,13 @@ export class Wallet { * 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.'); - }); + const overwriteKeys = this.storage.storageType instanceof TextFile + ? this.storage.storageType.migrate({ version: preMigrationVersion, name: this.name, keys: newKeys }) + : this.storage.addKeysSafe({ name: this.name, keys: newKeys }); + await overwriteKeys.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; From 24462f068c7bcd8040d9a7ce22e4c422cb210339 Mon Sep 17 00:00:00 2001 From: Michael Jay Date: Thu, 9 Apr 2026 09:26:34 -0400 Subject: [PATCH 51/51] clean up documentation --- .../bitcore-client/src/storage/textFile.ts | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/packages/bitcore-client/src/storage/textFile.ts b/packages/bitcore-client/src/storage/textFile.ts index 3eda8bd3a2c..d7c3ef6b906 100644 --- a/packages/bitcore-client/src/storage/textFile.ts +++ b/packages/bitcore-client/src/storage/textFile.ts @@ -217,22 +217,6 @@ export class TextFile { /** * Rewrite the address file in place for a specific migration version. - * - * Why a transformer? - * The TextFile backend is append-only. That is simple for normal writes, but it means - * migration cannot "overwrite" an existing address entry by appending a new one because - * `getKey()` still resolves the first matching line it encounters. For migration we - * therefore treat `addresses.txt` like a stream of JSONL records: - * - * 1. Read bytes from the existing file. - * 2. Parse each JSONL line into an object. - * 3. Pass each object through a Transform stream that replaces only the records that - * belong to this wallet/address set. - * 4. Serialize the transformed objects back to JSONL. - * 5. Write the result to a temporary file and atomically rename it into place. - * - * This lets us preserve the file order and all unrelated records while updating the - * migrated addresses in a single pass. */ async migrate(params: { version: number; name: string; keys: any[] }) { const { version, name, keys } = params; @@ -253,18 +237,6 @@ export class TextFile { const tempAddressFileName = `${this.addressFileName}.v${version}.migrating`; - /** - * The transform stream is the "rewrite policy" for migration. - * - * Each record coming in is one parsed JSON object from the source file. - * We either: - * - push it through unchanged, or - * - replace it with the migrated version for the same wallet/address. - * - * The important thing to notice is that Transform streams do not need to change the - * number of records. They can simply emit a modified version of the incoming record, - * which makes them a nice fit for "rewrite this file in one pass" jobs like this. - */ const migrationTransform = new stream.Transform({ writableObjectMode: true, readableObjectMode: true, @@ -280,7 +252,7 @@ export class TextFile { const replacement = replacementRecords.get(record.address); /** * If replacement not in map, indicates a prior-existing address which was not included in the keys to migrate. - * Fall back to prior. + * Warn & fall back to prior. */ if (!replacement) { console.warn(`TextFile migration warning: found address ${record.address} for wallet ${name}, but no replacement key was provided. Keeping original record.`);