diff --git a/modules/abstract-lightning/src/codecs/api/wallet.ts b/modules/abstract-lightning/src/codecs/api/wallet.ts index b81505a093..69b813d959 100644 --- a/modules/abstract-lightning/src/codecs/api/wallet.ts +++ b/modules/abstract-lightning/src/codecs/api/wallet.ts @@ -88,6 +88,7 @@ export const UpdateLightningWalletClientRequest = t.intersection([ signerMacaroon: t.string, signerAdminMacaroon: t.string, signerTlsKey: t.string, + encryptionVersion: t.union([t.literal(1), t.literal(2)]), }), ]); diff --git a/modules/abstract-lightning/src/wallet/selfCustodialLightning.ts b/modules/abstract-lightning/src/wallet/selfCustodialLightning.ts index aed41c6620..a8419074ef 100644 --- a/modules/abstract-lightning/src/wallet/selfCustodialLightning.ts +++ b/modules/abstract-lightning/src/wallet/selfCustodialLightning.ts @@ -24,6 +24,7 @@ async function encryptWalletUpdateRequest( requestWithEncryption.encryptedSignerTlsKey = await wallet.bitgo.encryptAsync({ password: params.passphrase, input: params.signerTlsKey, + encryptionVersion: params.encryptionVersion, }); } @@ -31,6 +32,7 @@ async function encryptWalletUpdateRequest( requestWithEncryption.encryptedSignerAdminMacaroon = await wallet.bitgo.encryptAsync({ password: params.passphrase, input: params.signerAdminMacaroon, + encryptionVersion: params.encryptionVersion, }); } @@ -38,6 +40,7 @@ async function encryptWalletUpdateRequest( requestWithEncryption.encryptedSignerMacaroon = await wallet.bitgo.encryptAsync({ password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'), input: params.signerMacaroon, + encryptionVersion: params.encryptionVersion, }); } diff --git a/modules/express/src/fetchEncryptedPrivKeys.ts b/modules/express/src/fetchEncryptedPrivKeys.ts index 00e22521c9..5edfcdd179 100644 --- a/modules/express/src/fetchEncryptedPrivKeys.ts +++ b/modules/express/src/fetchEncryptedPrivKeys.ts @@ -20,6 +20,7 @@ type Credentials = { walletId: string; // Id of the BitGo wallet. walletPassword: string; // Password used for the wallet. secret: string; // xprv of user key or backup key. + encryptionVersion?: 1 | 2; }; type WalletIds = { @@ -77,7 +78,11 @@ export async function fetchKeys(ids: WalletIds, token: string, accessToken?: str if (keychain.encryptedPrv === undefined) { if (typeof credential === 'object') { - const encryptedPrv = await bg.encryptAsync({ password: credential.walletPassword, input: credential.secret }); + const encryptedPrv = await bg.encryptAsync({ + password: credential.walletPassword, + input: credential.secret, + encryptionVersion: credential.encryptionVersion, + }); output[id] = encryptedPrv; } else { console.warn(`could not find a ${coinName} encrypted user private key for wallet id ${id}, skipping`); diff --git a/modules/key-card/src/generateQrData.ts b/modules/key-card/src/generateQrData.ts index a91f02c808..2646bb22b8 100644 --- a/modules/key-card/src/generateQrData.ts +++ b/modules/key-card/src/generateQrData.ts @@ -138,8 +138,12 @@ function generatePasscodeQrData(passphrase: string, passcodeEncryptionCode: stri }; } -async function generatePasscodeQrDataAsync(passphrase: string, passcodeEncryptionCode: string): Promise { - const encryptedWalletPasscode = await encryptAsync(passcodeEncryptionCode, passphrase); +async function generatePasscodeQrDataAsync( + passphrase: string, + passcodeEncryptionCode: string, + encryptionVersion?: 1 | 2 +): Promise { + const encryptedWalletPasscode = await encryptAsync(passcodeEncryptionCode, passphrase, { encryptionVersion }); return { title: 'D: Encrypted wallet Password', description: 'This is the wallet password, encrypted client-side with a key held by BitGo.', @@ -211,7 +215,11 @@ export async function generateQrDataAsync(params: GenerateQrDataParams): Promise const qrData = buildWalletQrData(params); if (params.passphrase && params.passcodeEncryptionCode) { - qrData.passcode = await generatePasscodeQrDataAsync(params.passphrase, params.passcodeEncryptionCode); + qrData.passcode = await generatePasscodeQrDataAsync( + params.passphrase, + params.passcodeEncryptionCode, + params.encryptionVersion + ); } return qrData; @@ -234,7 +242,11 @@ export async function generateLightningQrDataAsync(params: GenerateLightningQrDa const qrData = buildLightningQrData(params); if (params.passphrase && params.passcodeEncryptionCode) { - qrData.passcode = await generatePasscodeQrDataAsync(params.passphrase, params.passcodeEncryptionCode); + qrData.passcode = await generatePasscodeQrDataAsync( + params.passphrase, + params.passcodeEncryptionCode, + params.encryptionVersion + ); } return qrData; diff --git a/modules/key-card/src/types.ts b/modules/key-card/src/types.ts index 074762b1ca..49d1523e78 100644 --- a/modules/key-card/src/types.ts +++ b/modules/key-card/src/types.ts @@ -1,4 +1,4 @@ -import { Keychain } from '@bitgo/sdk-core'; +import { EncryptionVersion, Keychain } from '@bitgo/sdk-core'; import { BaseCoin, KeyCurve } from '@bitgo/statics'; export interface GenerateQrDataBaseParams { @@ -25,6 +25,7 @@ export interface GenerateQrDataCoinParams { // If both the passphrase and passcodeEncryptionCode are passed, then this code encrypts the passphrase with the // passcodeEncryptionCode and puts the result into Box D. Allows recoveries of the wallet password. passphrase?: string; + encryptionVersion?: EncryptionVersion; } export interface GenerateQrDataParams extends GenerateQrDataCoinParams { diff --git a/modules/key-card/test/unit/generateQrData.ts b/modules/key-card/test/unit/generateQrData.ts index 44efa91387..c03bcfc36f 100644 --- a/modules/key-card/test/unit/generateQrData.ts +++ b/modules/key-card/test/unit/generateQrData.ts @@ -247,6 +247,72 @@ describe('generateQrDataAsync', function () { const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data); decryptedData.should.equal(passphrase); }); + + it('produces a v1 Box D when encryptionVersion is not set', async function () { + const passphrase = 'testingIsFun'; + const passcodeEncryptionCode = '123456'; + const qrData = await generateQrDataAsync({ + backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }), + bitgoKeychain: createKeychain({ pub: 'bitgoPub' }), + coin: coins.get('btc'), + passcodeEncryptionCode, + passphrase, + userKeychain: createKeychain({ encryptedPrv: 'userPrv' }), + }); + + assert.ok(qrData.passcode); + const envelope = JSON.parse(qrData.passcode.data); + assert.notStrictEqual(envelope.v, 2, 'should default to v1 envelope'); + }); + + it('produces a v2 Box D when encryptionVersion: 2', async function () { + const passphrase = 'testingIsFun'; + const passcodeEncryptionCode = '123456'; + const qrData = await generateQrDataAsync({ + backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }), + bitgoKeychain: createKeychain({ pub: 'bitgoPub' }), + coin: coins.get('btc'), + passcodeEncryptionCode, + passphrase, + userKeychain: createKeychain({ encryptedPrv: 'userPrv' }), + encryptionVersion: 2, + }); + + assert.ok(qrData.passcode); + const envelope = JSON.parse(qrData.passcode.data); + assert.strictEqual(envelope.v, 2, 'should produce v2 envelope'); + const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data); + decryptedData.should.equal(passphrase); + }); + + it('produces a v1 Box D when encryptionVersion: 1 is explicit', async function () { + const passphrase = 'testingIsFun'; + const passcodeEncryptionCode = '123456'; + const qrData = await generateQrDataAsync({ + backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }), + bitgoKeychain: createKeychain({ pub: 'bitgoPub' }), + coin: coins.get('btc'), + passcodeEncryptionCode, + passphrase, + userKeychain: createKeychain({ encryptedPrv: 'userPrv' }), + encryptionVersion: 1, + }); + + assert.ok(qrData.passcode); + const envelope = JSON.parse(qrData.passcode.data); + assert.notStrictEqual(envelope.v, 2, 'should produce v1 envelope'); + }); + + it('omits Box D when passphrase or passcodeEncryptionCode is missing', async function () { + const qrData = await generateQrDataAsync({ + backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }), + bitgoKeychain: createKeychain({ pub: 'bitgoPub' }), + coin: coins.get('btc'), + userKeychain: createKeychain({ encryptedPrv: 'userPrv' }), + encryptionVersion: 2, + }); + assert.strictEqual(qrData.passcode, undefined); + }); }); describe('generateLightningQrDataAsync', function () { @@ -264,4 +330,22 @@ describe('generateLightningQrDataAsync', function () { const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data); decryptedData.should.equal(passphrase); }); + + it('produces a v2 Box D when encryptionVersion: 2', async function () { + const passphrase = 'testingIsFun'; + const passcodeEncryptionCode = '123456'; + const qrData = await generateLightningQrDataAsync({ + userAuthKeychain: createKeychain({ encryptedPrv: 'userAuthPrv' }), + coin: coins.get('lnbtc'), + passcodeEncryptionCode, + passphrase, + encryptionVersion: 2, + }); + + assert.ok(qrData.passcode); + const envelope = JSON.parse(qrData.passcode.data); + assert.strictEqual(envelope.v, 2); + const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data); + decryptedData.should.equal(passphrase); + }); }); diff --git a/modules/passkey-crypto/src/attachPasskeyToWallet.ts b/modules/passkey-crypto/src/attachPasskeyToWallet.ts index 72faed15ab..537faeb149 100644 --- a/modules/passkey-crypto/src/attachPasskeyToWallet.ts +++ b/modules/passkey-crypto/src/attachPasskeyToWallet.ts @@ -1,4 +1,4 @@ -import { BitGoBase, Keychain } from '@bitgo/sdk-core'; +import { BitGoBase, EncryptionVersion, Keychain } from '@bitgo/sdk-core'; import { base64UrlToBuffer } from './base64url'; import { deriveEnterpriseSalt } from './deriveEnterpriseSalt'; import { derivePassword } from './derivePassword'; @@ -11,8 +11,9 @@ export async function attachPasskeyToWallet(params: { device: WebAuthnOtpDevice; existingPassphrase: string; provider: WebAuthnProvider; + encryptionVersion?: EncryptionVersion; }): Promise { - const { bitgo, coin, walletId, device, existingPassphrase, provider } = params; + const { bitgo, coin, walletId, device, existingPassphrase, provider, encryptionVersion } = params; // Throw early if PRF extension is not supported if (!device.prfSalt) { @@ -66,7 +67,7 @@ export async function attachPasskeyToWallet(params: { } const prfPassword = derivePassword(authResult.prfResult); - const encryptedPrv = await bitgo.encryptAsync({ password: prfPassword, input: privateKey, encryptionVersion: 2 }); + const encryptedPrv = await bitgo.encryptAsync({ password: prfPassword, input: privateKey, encryptionVersion }); const updatedKeychain = await bitgo .put(bitgo.url(`/${coin}/key/${keychainId}`, 2)) diff --git a/modules/passkey-crypto/test/unit/attachPasskeyToWallet.test.ts b/modules/passkey-crypto/test/unit/attachPasskeyToWallet.test.ts index 5996b7b3a9..f51d8573d2 100644 --- a/modules/passkey-crypto/test/unit/attachPasskeyToWallet.test.ts +++ b/modules/passkey-crypto/test/unit/attachPasskeyToWallet.test.ts @@ -114,6 +114,7 @@ describe('attachPasskeyToWallet', function () { device, existingPassphrase, provider: mockProvider as unknown as WebAuthnProvider, + encryptionVersion: 2, ...overrides, }); } diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index feba1f8d83..6eeb41b6d5 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -10,6 +10,7 @@ import { DecryptOptions, defaultConstants, EcdhDerivedKeypair, + EncryptionVersion, EncryptOptions, EnvironmentName, generateRandomPassword, @@ -1125,7 +1126,7 @@ export class BitGoAPI implements BitGoBase { * @returns {Promise} - A promise that resolves with the new ECDH keychain data. * @throws {Error} - Throws an error if there is an issue creating the keychain. */ - public async createUserEcdhKeychain(loginPassword: string): Promise { + public async createUserEcdhKeychain(loginPassword: string, encryptionVersion?: EncryptionVersion): Promise { const keyData = this.keychains().create(); const hdNode = bitcoin.HDNode.fromBase58(keyData.xprv); @@ -1139,6 +1140,7 @@ export class BitGoAPI implements BitGoBase { encryptedXprv: await this.encryptAsync({ password: loginPassword, input: hdNode.toBase58(), + encryptionVersion, }), }); } @@ -1160,7 +1162,10 @@ export class BitGoAPI implements BitGoBase { * @returns {Promise} - A promise that resolves with the user's settings ensuring we have the ecdhKeychain in there. * @throws {Error} - Throws an error if there is an issue creating the keychain or updating the user's settings. */ - private async ensureUserEcdhKeychainIsCreated(loginPassword: string): Promise { + private async ensureUserEcdhKeychainIsCreated( + loginPassword: string, + encryptionVersion?: EncryptionVersion + ): Promise { /** * Get the user's current settings. */ @@ -1169,7 +1174,7 @@ export class BitGoAPI implements BitGoBase { * If the user's ECDH keychain does not exist, create a new keychain and update the user's settings. */ if (!userSettings.settings.ecdhKeychain) { - const newKeychain = await this.createUserEcdhKeychain(loginPassword); + const newKeychain = await this.createUserEcdhKeychain(loginPassword, encryptionVersion); await this.updateUserSettings({ settings: { ecdhKeychain: newKeychain.xpub, @@ -1251,7 +1256,9 @@ export class BitGoAPI implements BitGoBase { await this._hmacAuthStrategy.setToken?.(this._token); } - const userSettings = params.ensureEcdhKeychain ? await this.ensureUserEcdhKeychainIsCreated(password) : undefined; + const userSettings = params.ensureEcdhKeychain + ? await this.ensureUserEcdhKeychainIsCreated(password, params.encryptionVersion) + : undefined; if (userSettings?.ecdhKeychain) { response.body.user.ecdhKeychain = userSettings.ecdhKeychain; } @@ -1932,11 +1939,11 @@ export class BitGoAPI implements BitGoBase { * @param passwords * @param m */ - async splitSecretAsync({ seed, passwords, m }: SplitSecretOptions): Promise { + async splitSecretAsync({ seed, passwords, m, encryptionVersion }: SplitSecretOptions): Promise { const n = validateSplitSecretInputs({ seed, passwords, m }); const secrets: string[] = shamir.share(seed, n, m); const shards = await Promise.all( - secrets.map((shard, i) => this.encryptAsync({ input: shard, password: passwords[i] })) + secrets.map((shard, i) => this.encryptAsync({ input: shard, password: passwords[i], encryptionVersion })) ); return buildSplitSecretResult(seed, shards, m, n); } diff --git a/modules/sdk-api/src/types.ts b/modules/sdk-api/src/types.ts index c5efb02aa3..c7f2a32e09 100644 --- a/modules/sdk-api/src/types.ts +++ b/modules/sdk-api/src/types.ts @@ -1,4 +1,4 @@ -import { EnvironmentName, IRequestTracer, V1Network } from '@bitgo/sdk-core'; +import { EncryptionVersion, EnvironmentName, IRequestTracer, V1Network } from '@bitgo/sdk-core'; import { ECPairInterface } from '@bitgo/utxo-lib'; import { type Agent } from 'http'; @@ -104,6 +104,11 @@ export interface AuthenticateOptions { * It is highly recommended that this is always set to avoid any issues when using a BitGo wallet */ ensureEcdhKeychain?: boolean; + /** + * Encryption version to use when creating the ECDH keychain if it does not already exist. + * Only applies when `ensureEcdhKeychain` is true and no ECDH keychain exists for the user yet. + */ + encryptionVersion?: EncryptionVersion; forReset2FA?: boolean; /** * The initial stage fingerprint hash used for device identification and verification. @@ -226,6 +231,7 @@ export interface SplitSecretOptions { seed: string; passwords: string[]; m: number; + encryptionVersion?: EncryptionVersion; } export interface SplitSecret { diff --git a/modules/sdk-api/src/v1/keychains.ts b/modules/sdk-api/src/v1/keychains.ts index 718048e63d..f9ce3fdaf3 100644 --- a/modules/sdk-api/src/v1/keychains.ts +++ b/modules/sdk-api/src/v1/keychains.ts @@ -13,7 +13,7 @@ import { bip32 } from '@bitgo/utxo-lib'; import { randomBytes } from 'crypto'; -import { common, Util, sanitizeLegacyPath } from '@bitgo/sdk-core'; +import { common, isV2Envelope, Util, sanitizeLegacyPath } from '@bitgo/sdk-core'; const _ = require('lodash'); // @@ -194,7 +194,12 @@ Keychains.prototype.updatePassword = function (params, callback) { input: oldEncryptedXprv as string, password: params.oldPassword, }); - const newEncryptedPrv = await self.bitgo.encryptAsync({ input: decryptedPrv, password: params.newPassword }); + const encryptionVersion = isV2Envelope(oldEncryptedXprv as string) ? 2 : 1; + const newEncryptedPrv = await self.bitgo.encryptAsync({ + input: decryptedPrv, + password: params.newPassword, + encryptionVersion, + }); newKeychains[xpub] = newEncryptedPrv; } catch (e) { // decrypting the keychain with the old password didn't work so we just keep it the way it is diff --git a/modules/sdk-api/src/v1/travelRule.ts b/modules/sdk-api/src/v1/travelRule.ts index 878558360d..b234ac2797 100644 --- a/modules/sdk-api/src/v1/travelRule.ts +++ b/modules/sdk-api/src/v1/travelRule.ts @@ -270,6 +270,7 @@ TravelRule.prototype.prepareParamsAsync = async function (params) { const encryptedTravelInfo = await this.bitgo.encryptAsync({ input: prepared.travelInfoJSON, password: prepared.sharedSecret, + encryptionVersion: params.encryptionVersion, }); return buildTravelRuleSendParams(prepared, encryptedTravelInfo); diff --git a/modules/sdk-api/src/v1/wallet.ts b/modules/sdk-api/src/v1/wallet.ts index 6c98a22fc8..59405802fb 100644 --- a/modules/sdk-api/src/v1/wallet.ts +++ b/modules/sdk-api/src/v1/wallet.ts @@ -2318,7 +2318,11 @@ Wallet.prototype.shareWallet = function (params, callback) { const eckey = makeRandomKey(); const secret = getSharedSecret(eckey, Buffer.from(sharing.pubkey, 'hex')).toString('hex'); - const newEncryptedXprv = await self.bitgo.encryptAsync({ password: secret, input: keychain.xprv }); + const newEncryptedXprv = await self.bitgo.encryptAsync({ + password: secret, + input: keychain.xprv, + encryptionVersion: params.encryptionVersion, + }); sharedKeychain = { xpub: keychain.xpub, diff --git a/modules/sdk-api/src/v1/wallets.ts b/modules/sdk-api/src/v1/wallets.ts index 0c01a78ca5..ffeeff737e 100644 --- a/modules/sdk-api/src/v1/wallets.ts +++ b/modules/sdk-api/src/v1/wallets.ts @@ -275,6 +275,7 @@ Wallets.prototype.acceptShare = function (params, callback) { encryptedXprv = await self.bitgo.encryptAsync({ password: newWalletPassphrase, input: decryptedSharedWalletXprv, + encryptionVersion: params.encryptionVersion, }); // Carry on to the next block where we will post the acceptance of the share with the encrypted xprv @@ -368,7 +369,13 @@ Wallets.prototype.createWalletWithKeychains = function (params, callback) { let bitgoKeychain; return Promise.resolve() - .then(() => self.bitgo.encryptAsync({ password: params.passphrase, input: userKeychain.xprv })) + .then(() => + self.bitgo.encryptAsync({ + password: params.passphrase, + input: userKeychain.xprv, + encryptionVersion: params.encryptionVersion, + }) + ) .then(function (encryptedXprv) { userKeychain.encryptedXprv = encryptedXprv; diff --git a/modules/sdk-api/test/unit/bitgoAPI.ts b/modules/sdk-api/test/unit/bitgoAPI.ts index 9734483caa..7717fa7ac2 100644 --- a/modules/sdk-api/test/unit/bitgoAPI.ts +++ b/modules/sdk-api/test/unit/bitgoAPI.ts @@ -1,3 +1,4 @@ +import * as assert from 'assert'; import 'should'; import { BitGoAPI } from '../../src/bitgoAPI'; import { ProxyAgent } from 'proxy-agent'; @@ -1040,4 +1041,82 @@ describe('Constructor', function () { legacyScope.isDone().should.be.true(); }); }); + + describe('createUserEcdhKeychain - encryptionVersion threading', function () { + const ROOT = 'https://app.bitgo-test.com'; + let bitgo: BitGoAPI; + + beforeEach(function () { + bitgo = new BitGoAPI({ env: 'test' }); + }); + + afterEach(function () { + nock.cleanAll(); + sinon.restore(); + }); + + it('passes encryptionVersion: 2 to encryptAsync', async function () { + const encryptAsyncSpy = sinon.spy(bitgo, 'encryptAsync'); + nock(ROOT).post('/api/v1/keychain').reply(200, { xpub: 'xpub123', id: 'key-id' }); + + await bitgo.createUserEcdhKeychain('loginPassword', 2); + + assert.ok(encryptAsyncSpy.calledOnce); + assert.strictEqual(encryptAsyncSpy.firstCall.args[0].encryptionVersion, 2); + }); + + it('passes encryptionVersion: undefined when not set (defaults to v1)', async function () { + const encryptAsyncSpy = sinon.spy(bitgo, 'encryptAsync'); + nock(ROOT).post('/api/v1/keychain').reply(200, { xpub: 'xpub123', id: 'key-id' }); + + await bitgo.createUserEcdhKeychain('loginPassword'); + + assert.ok(encryptAsyncSpy.calledOnce); + assert.strictEqual(encryptAsyncSpy.firstCall.args[0].encryptionVersion, undefined); + }); + }); + + describe('splitSecretAsync - encryptionVersion threading', function () { + let bitgo: BitGoAPI; + + beforeEach(function () { + bitgo = new BitGoAPI({ env: 'test' }); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('passes encryptionVersion: 2 to every encryptAsync shard call', async function () { + const encryptAsyncSpy = sinon.spy(bitgo, 'encryptAsync'); + const passwords = ['pw1', 'pw2', 'pw3']; + + await bitgo.splitSecretAsync({ + seed: 'a'.repeat(64), + passwords, + m: 2, + encryptionVersion: 2, + }); + + assert.strictEqual(encryptAsyncSpy.callCount, 3, 'should encrypt each shard'); + for (const call of encryptAsyncSpy.getCalls()) { + assert.strictEqual(call.args[0].encryptionVersion, 2); + } + }); + + it('passes encryptionVersion: undefined when not set', async function () { + const encryptAsyncSpy = sinon.spy(bitgo, 'encryptAsync'); + + await bitgo.splitSecretAsync({ + seed: 'b'.repeat(64), + passwords: ['pw1', 'pw2'], + m: 2, + }); + + assert.strictEqual(encryptAsyncSpy.callCount, 2); + for (const call of encryptAsyncSpy.getCalls()) { + assert.strictEqual(call.args[0].encryptionVersion, undefined); + } + }); + }); }); diff --git a/modules/sdk-core/src/api/types.ts b/modules/sdk-core/src/api/types.ts index c5d945989e..0a6613f6a5 100644 --- a/modules/sdk-core/src/api/types.ts +++ b/modules/sdk-core/src/api/types.ts @@ -40,7 +40,11 @@ export interface EncryptOptions { /** Sync encrypt callback — used by v1 (SJCL) code paths. */ export type EncryptFn = (params: { input: string; password: string }) => string; /** Async encrypt callback — used by v2 (Argon2id) code paths. */ -export type EncryptFnAsync = (params: { input: string; password: string }) => Promise; +export type EncryptFnAsync = (params: { + input: string; + password: string; + encryptionVersion?: EncryptionVersion; +}) => Promise; export interface GetSharingKeyOptions { email: string; diff --git a/modules/sdk-core/src/bitgo/internal/keycard.ts b/modules/sdk-core/src/bitgo/internal/keycard.ts index e64dbb370e..4f7fa37fbf 100644 --- a/modules/sdk-core/src/bitgo/internal/keycard.ts +++ b/modules/sdk-core/src/bitgo/internal/keycard.ts @@ -6,7 +6,7 @@ */ import { isUndefined } from 'lodash'; import { Keychain } from '../keychain'; -import { EncryptFn, EncryptFnAsync } from '../../api'; +import { EncryptFn, EncryptFnAsync, EncryptionVersion } from '../../api'; /** * Return the list of questions that will appear on the second page of the keycard @@ -95,6 +95,7 @@ interface GetKeyDataOptions { type GetKeyDataAsyncOptions = Omit & { encrypt: EncryptFnAsync; + encryptionVersion?: EncryptionVersion; }; interface BuildKeycardQrDataOptions { @@ -216,12 +217,13 @@ function getKeyData(options: GetKeyDataOptions): any { * @param options */ async function getKeyDataAsync(options: GetKeyDataAsyncOptions): Promise { - const { encrypt, backupKeychain, passphrase, passcodeEncryptionCode, ...qrOptions } = options; + const { encrypt, backupKeychain, passphrase, passcodeEncryptionCode, encryptionVersion, ...qrOptions } = options; if (backupKeychain.prv && passphrase) { backupKeychain.encryptedPrv = await encrypt({ input: backupKeychain.prv, password: passphrase, + encryptionVersion, }); } @@ -230,6 +232,7 @@ async function getKeyDataAsync(options: GetKeyDataAsyncOptions): Promise { encryptedWalletPasscode = await encrypt({ input: passphrase, password: passcodeEncryptionCode, + encryptionVersion, }); } @@ -250,6 +253,7 @@ interface DrawKeycardOptions extends GetKeyDataOptions, DrawKeycardLayoutOptions export type DrawKeycardAsyncOptions = Omit & { encrypt: EncryptFnAsync; + encryptionVersion?: EncryptionVersion; }; /** @@ -465,6 +469,7 @@ export async function drawKeycardAsync(options: DrawKeycardAsyncOptions): Promis activationCode, walletLabel, coinName, + encryptionVersion, } = options; // Get the data for the first page (qr codes) @@ -478,6 +483,7 @@ export async function drawKeycardAsync(options: DrawKeycardAsyncOptions): Promis userKeychain, bitgoKeychain, backupKeychain, + encryptionVersion, }); return renderKeycardPdf({ jsPDF, QRCode, activationCode, walletLabel, coinName }, keyData); diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index 7f40a86fd7..e6c7d3dcb9 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -110,6 +110,7 @@ export type RotateKeychainOptions = id: string; password: string; reqId?: IRequestTracer; + encryptionVersion?: EncryptionVersion; } | { id: string; @@ -254,6 +255,6 @@ export interface IKeychains { createMpc(params: CreateMpcOptions): Promise; recreateMpc(params: RecreateMpcOptions): Promise; createTssBitGoKeyFromOvcShares(ovcOutput: OvcToBitGoJSON, enterprise?: string): Promise; - createUserKeychain(userPassword: string): Promise; + createUserKeychain(userPassword: string, encryptionVersion?: EncryptionVersion): Promise; rotateKeychain(params: RotateKeychainOptions): Promise; } diff --git a/modules/sdk-core/src/bitgo/keychain/keychains.ts b/modules/sdk-core/src/bitgo/keychain/keychains.ts index a2f13b9b6c..1b2dd307f1 100644 --- a/modules/sdk-core/src/bitgo/keychain/keychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/keychains.ts @@ -567,7 +567,7 @@ export class Keychains implements IKeychains { * @param walletPassphrase * @returns Keychain including the decrypted private key */ - async createUserKeychain(walletPassphrase: string): Promise { + async createUserKeychain(walletPassphrase: string, encryptionVersion?: EncryptionVersion): Promise { const keychains = this.baseCoin.keychains(); const newKeychain = keychains.create(); const originalPasscodeEncryptionCode = generateRandomPassword(5); @@ -575,6 +575,7 @@ export class Keychains implements IKeychains { const encryptedPrv = await this.bitgo.encryptAsync({ password: walletPassphrase, input: newKeychain.prv, + encryptionVersion, }); return { @@ -614,7 +615,11 @@ export class Keychains implements IKeychains { throw Error('Expected a public key to be generated'); } pub = keyPub; - encryptedPrv = await this.bitgo.encryptAsync({ input: keyPrv, password: params.password }); + encryptedPrv = await this.bitgo.encryptAsync({ + input: keyPrv, + password: params.password, + encryptionVersion: params.encryptionVersion, + }); } return this.bitgo diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts index d38dbc5898..3a3f7a5099 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts @@ -110,6 +110,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { enterprise?: string | undefined; originalPasscodeEncryptionCode?: string | undefined; webauthnInfo?: WebauthnKeyEncryptionInfo; + encryptionVersion?: EncryptionVersion; }): Promise { const MPC = new Ecdsa(); const m = 2; @@ -143,6 +144,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase: params.passphrase, originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, webauthnInfo: params.webauthnInfo, + encryptionVersion: params.encryptionVersion, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, @@ -152,6 +154,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { backupKeyShare, bitgoKeychain, passphrase: params.passphrase, + encryptionVersion: params.encryptionVersion, }); const [userKeychain, backupKeychain] = await Promise.all([userKeychainPromise, backupKeychainPromise]); @@ -424,6 +427,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { encryptedPrv: await this.bitgo.encryptAsync({ input: prv, password: webauthnInfo.passphrase, + encryptionVersion, }), }, ] @@ -444,6 +448,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { prv: string; derivationPath: string; walletPassphrase?: string; + encryptionVersion?: EncryptionVersion; }): Promise { const { challenges, derivationPath, prv } = params; const userSigningMaterial: ECDSAMethodTypes.SigningMaterial = JSON.parse(prv); @@ -510,6 +515,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { ? await this.bitgo.encryptAsync({ input: JSON.stringify(userSignShare.wShare), password: params.walletPassphrase, + encryptionVersion: params.encryptionVersion, }) : userSignShare.wShare, }; @@ -520,6 +526,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { wShare: WShare; aShareFromBitgo: Omit; walletPassphrase?: string; + encryptionVersion?: EncryptionVersion; }): Promise { // Append the BitGo challenge to the Ashare to be used in subsequent proofs const bitgoToUserAShareWithNtilde: AShare = { @@ -543,6 +550,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { ? await this.bitgo.encryptAsync({ input: JSON.stringify(userOmicronAndDeltaShare.oShare), password: params.walletPassphrase, + encryptionVersion: params.encryptionVersion, }) : userOmicronAndDeltaShare.oShare, }; @@ -563,6 +571,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { requestType: RequestType; prv: string; walletPassphrase: string; + encryptionVersion?: EncryptionVersion; }): Promise { const { tssParams, prv, requestType, challenges } = params; assert(typeof tssParams.txRequest !== 'string', 'Invalid txRequest type'); @@ -586,6 +595,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { challenges: challenges, derivationPath: derivationPath, walletPassphrase: params.walletPassphrase, + encryptionVersion: params.encryptionVersion, }); } @@ -594,6 +604,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { bitgoChallenge: TxRequestChallengeResponse; encryptedWShare: string; walletPassphrase: string; + encryptionVersion?: EncryptionVersion; }): Promise { const decryptedWShare = await this.bitgo.decryptAsync({ input: params.encryptedWShare, @@ -604,6 +615,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { bitgoChallenge: params.bitgoChallenge, wShare: JSON.parse(decryptedWShare), walletPassphrase: params.walletPassphrase, + encryptionVersion: params.encryptionVersion, }); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index a10e2b0aac..337d853157 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -335,7 +335,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { params.passphrase, params.originalPasscodeEncryptionCode, params.webauthnInfo, - encryptionSession + encryptionSession, + params.encryptionVersion ); const backupKeychainPromise = this.addBackupKeychain( bitgoCommonKeychain, @@ -343,7 +344,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { backupReducedPrivateMaterial, params.passphrase, params.originalPasscodeEncryptionCode, - encryptionSession + encryptionSession, + params.encryptionVersion ); const bitgoKeychainPromise = this.addBitgoKeychain(bitgoCommonKeychain); @@ -377,7 +379,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { encrypt(plaintext: string): Promise; decrypt(ciphertext: string): Promise; destroy(): void; - } + }, + encryptionVersion?: EncryptionVersion ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -400,6 +403,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { encryptedPrv = await this.bitgo.encryptAsync({ input: privateMaterialBase64, password: passphrase, + encryptionVersion, }); // Encrypts the CBOR-encoded ReducedKeyShare (which contains the party's private // scalar s_i) with the wallet passphrase. The result is stored as reducedEncryptedPrv @@ -410,6 +414,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { // The browser deals with a Buffer as Uint8Array, therefore in the browser .toString('base64') just creates a comma seperated string of the array values. input: btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(reducedPrivateMaterial)))), password: passphrase, + encryptionVersion, }); } break; @@ -437,6 +442,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { encryptedPrv: await this.bitgo.encryptAsync({ input: privateMaterialBase64, password: webauthnInfo.passphrase, + encryptionVersion, }), }, ]; @@ -567,7 +573,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { encrypt(plaintext: string): Promise; decrypt(ciphertext: string): Promise; destroy(): void; - } + }, + encryptionVersion?: EncryptionVersion ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -577,7 +584,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionSession + encryptionSession, + encryptionVersion ); } @@ -591,7 +599,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { encrypt(plaintext: string): Promise; decrypt(ciphertext: string): Promise; destroy(): void; - } + }, + encryptionVersion?: EncryptionVersion ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.BACKUP, @@ -601,7 +610,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { passphrase, originalPasscodeEncryptionCode, undefined, - encryptionSession + encryptionSession, + encryptionVersion ); } @@ -1214,11 +1224,13 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { input: sessionData, password: walletPassphrase, adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND1_STATE}:${adata}`, + encryptionVersion: 1, }); const encryptedUserGpgPrvKey = await this.bitgo.encryptAsync({ input: userGpgKey.privateKey, password: walletPassphrase, adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_USER_GPG_KEY}:${adata}`, + encryptionVersion: 1, }); return { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey }; @@ -1315,6 +1327,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { input: sessionData, password: walletPassphrase, adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND2_STATE}:${adata}`, + encryptionVersion: 1, }); return { diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index 9a2a45d648..a439e47dae 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -134,6 +134,7 @@ export class EddsaUtils extends baseTSSUtils { originalPasscodeEncryptionCode, webauthnInfo, encryptionSession, + encryptionVersion, }: CreateEddsaKeychainParams): Promise { const MPC = await Eddsa.initialize(); const bitgoKeyShares = bitgoKeychain.keyShares; @@ -196,6 +197,7 @@ export class EddsaUtils extends baseTSSUtils { userKeychainParams.encryptedPrv = await this.bitgo.encryptAsync({ input: JSON.stringify(userSigningMaterial), password: passphrase, + encryptionVersion, }); } } @@ -207,6 +209,7 @@ export class EddsaUtils extends baseTSSUtils { encryptedPrv: await this.bitgo.encryptAsync({ input: JSON.stringify(userSigningMaterial), password: webauthnInfo.passphrase, + encryptionVersion, }), }, ]; @@ -234,6 +237,7 @@ export class EddsaUtils extends baseTSSUtils { bitgoKeychain, passphrase, encryptionSession, + encryptionVersion, }: CreateEddsaKeychainParams): Promise { const MPC = await Eddsa.initialize(); const bitgoKeyShares = bitgoKeychain.keyShares; @@ -295,7 +299,7 @@ export class EddsaUtils extends baseTSSUtils { if (encryptionSession) { params.encryptedPrv = await encryptionSession.encrypt(prv); } else { - params.encryptedPrv = await this.bitgo.encryptAsync({ input: prv, password: passphrase }); + params.encryptedPrv = await this.bitgo.encryptAsync({ input: prv, password: passphrase, encryptionVersion }); } } @@ -407,6 +411,7 @@ export class EddsaUtils extends baseTSSUtils { originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, webauthnInfo: params.webauthnInfo, encryptionSession, + encryptionVersion: params.encryptionVersion, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, @@ -416,6 +421,7 @@ export class EddsaUtils extends baseTSSUtils { bitgoKeychain, passphrase: params.passphrase, encryptionSession, + encryptionVersion: params.encryptionVersion, }); const [userKeychain, backupKeychain] = await Promise.all([userKeychainPromise, backupKeychainPromise]); @@ -489,7 +495,11 @@ export class EddsaUtils extends baseTSSUtils { session.destroy(); } } else { - encryptedRShare = await this.bitgo.encryptAsync({ input: stringifiedRShare, password: params.walletPassphrase }); + encryptedRShare = await this.bitgo.encryptAsync({ + input: stringifiedRShare, + password: params.walletPassphrase, + encryptionVersion: 1, + }); } const encryptedUserToBitgoRShare = this.createUserToBitgoEncryptedRShare(encryptedRShare); diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index f780cc9fc1..e746fbe282 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -40,6 +40,7 @@ import { TxRequest, isV2Envelope, } from '../baseTypes'; +import { EncryptionVersion } from '../../../../api'; import { BaseEddsaUtils } from './base'; import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender'; @@ -68,6 +69,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { enterprise: string; originalPasscodeEncryptionCode?: string; webauthnInfo?: WebauthnKeyEncryptionInfo; + encryptionVersion?: EncryptionVersion; }): Promise { const userKeyPair = await generateGPGKeyPair('ed25519'); const userGpgKey = await pgp.readPrivateKey({ armoredKey: userKeyPair.privateKey }); @@ -192,14 +194,16 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { userReducedPrivateMaterial, params.passphrase, params.originalPasscodeEncryptionCode, - params.webauthnInfo + params.webauthnInfo, + params.encryptionVersion ); const backupKeychainPromise = this.addBackupKeychain( backupCommonKeychain, backupPrivateMaterial, backupReducedPrivateMaterial, params.passphrase, - params.originalPasscodeEncryptionCode + params.originalPasscodeEncryptionCode, + params.encryptionVersion ); const bitgoKeychainPromise = this.addBitgoKeychain(userCommonKeychain); @@ -225,7 +229,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { reducedPrivateMaterial?: Buffer, passphrase?: string, originalPasscodeEncryptionCode?: string, - webauthnInfo?: WebauthnKeyEncryptionInfo + webauthnInfo?: WebauthnKeyEncryptionInfo, + encryptionVersion?: EncryptionVersion ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -243,6 +248,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { encryptedPrv = await this.bitgo.encryptAsync({ input: privateMaterialBase64, password: passphrase, + encryptionVersion, }); // Encrypts the CBOR-encoded ReducedKeyShare (which contains the party's public // key) with the wallet passphrase. The result is stored as reducedEncryptedPrv @@ -253,6 +259,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { // The browser deals with a Buffer as Uint8Array, therefore in the browser .toString('base64') just creates a comma separated string of the array values. input: btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(reducedPrivateMaterial)))), password: passphrase, + encryptionVersion, }); break; case MPCv2PartiesEnum.BITGO: @@ -279,6 +286,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { encryptedPrv: await this.bitgo.encryptAsync({ input: privateMaterialBase64, password: webauthnInfo.passphrase, + encryptionVersion, }), }, ]; @@ -294,7 +302,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { reducedPrivateMaterial: Buffer, passphrase: string, originalPasscodeEncryptionCode?: string, - webauthnInfo?: WebauthnKeyEncryptionInfo + webauthnInfo?: WebauthnKeyEncryptionInfo, + encryptionVersion?: EncryptionVersion ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -303,7 +312,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { reducedPrivateMaterial, passphrase, originalPasscodeEncryptionCode, - webauthnInfo + webauthnInfo, + encryptionVersion ); } @@ -312,7 +322,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { privateMaterial: Buffer, reducedPrivateMaterial: Buffer, passphrase: string, - originalPasscodeEncryptionCode?: string + originalPasscodeEncryptionCode?: string, + encryptionVersion?: EncryptionVersion ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.BACKUP, @@ -320,7 +331,9 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { privateMaterial, reducedPrivateMaterial, passphrase, - originalPasscodeEncryptionCode + originalPasscodeEncryptionCode, + undefined, + encryptionVersion ); } @@ -604,11 +617,13 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { input: sessionPayload, password: walletPassphrase, adata: `${EddsaMPCv2Utils.MPS_DSG_SIGNING_ROUND1_STATE}:${adata}`, + encryptionVersion: 1, }); const encryptedUserGpgPrvKey = await this.bitgo.encryptAsync({ input: userGpgKey.privateKey, password: walletPassphrase, adata: `${EddsaMPCv2Utils.MPS_DSG_SIGNING_USER_GPG_KEY}:${adata}`, + encryptionVersion: 1, }); return { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey }; @@ -715,6 +730,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { input: sessionPayload, password: walletPassphrase, adata: `${EddsaMPCv2Utils.MPS_DSG_SIGNING_ROUND2_STATE}:${adata}`, + encryptionVersion: 1, }); return { signatureShareRound2, encryptedRound2Session }; diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 9583cee260..82e1c66fec 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -1,4 +1,4 @@ -import { IRequestTracer } from '../../api'; +import { EncryptionVersion, IRequestTracer } from '../../api'; import { CreateLightningInvoiceParams, LightningInvoiceResponse } from '../../lightning'; import { IBaseCoin, @@ -771,6 +771,7 @@ export interface ShareWalletOptions { */ skipKeychain?: boolean; disableEmail?: boolean; + encryptionVersion?: EncryptionVersion; } export interface BulkCreateShareOption { @@ -787,6 +788,7 @@ export interface BulkWalletShareOptions { path: string; permissions: string[]; }>; + encryptionVersion?: EncryptionVersion; } export type WalletShareState = 'active' | 'accepted' | 'canceled' | 'rejected' | 'pendingapproval'; @@ -1057,6 +1059,7 @@ export interface DownloadKeycardOptions { activationCode?: string; walletKeyID?: string; backupKeyID?: string; + encryptionVersion?: EncryptionVersion; } export interface ChallengeVerifiers { diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index ac82fc56bd..88476e42a2 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -155,6 +155,7 @@ export interface AcceptShareOptions { walletShareId?: string; userPassword?: string; newWalletPassphrase?: string; + encryptionVersion?: EncryptionVersion; } export interface BulkAcceptShareOptions { @@ -162,6 +163,7 @@ export interface BulkAcceptShareOptions { userLoginPassword: string; newWalletPassphrase?: string; webauthnInfo?: WebauthnKeyEncryptionInfo; + encryptionVersion?: EncryptionVersion; } export interface AcceptShareOptionsRequest { @@ -188,6 +190,7 @@ export interface BulkUpdateWalletShareOptions { }[]; userLoginPassword?: string; newWalletPassphrase?: string; + encryptionVersion?: EncryptionVersion; } export interface BulkUpdateWalletShareOptionsRequest { diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index bd8e5ed909..9fb0914c42 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import BigNumber from 'bignumber.js'; import * as t from 'io-ts'; import * as _ from 'lodash'; -import { IRequestTracer } from '../../api'; +import { EncryptionVersion, IRequestTracer } from '../../api'; import * as common from '../../common'; import { AddressBook, IAddressBook } from '../address-book'; import { @@ -1850,7 +1850,8 @@ export class Wallet implements IWallet { decryptedKeychain.prv, decryptedKeychain.pub, shareOption.pubKey, - shareOption.path + shareOption.path, + params.encryptionVersion ); bulkCreateShareOptions.push({ @@ -1990,11 +1991,12 @@ export class Wallet implements IWallet { decryptedPrv: string, pub: string, userPubkey: string, - path: string + path: string, + encryptionVersion?: EncryptionVersion ): Promise { const eckey = makeRandomKey(); const secret = getSharedSecret(eckey, Buffer.from(userPubkey, 'hex')).toString('hex'); - const newEncryptedPrv = await this.bitgo.encryptAsync({ password: secret, input: decryptedPrv }); + const newEncryptedPrv = await this.bitgo.encryptAsync({ password: secret, input: decryptedPrv, encryptionVersion }); const keychain: BulkWalletShareKeychain = { pub, @@ -2025,14 +2027,21 @@ export class Wallet implements IWallet { async prepareSharedKeychain( walletPassphrase: string | undefined, pubkey: string, - path: string + path: string, + encryptionVersion?: EncryptionVersion ): Promise { try { const decryptedKeychain = await this.getDecryptedKeychainForSharing(walletPassphrase); if (!decryptedKeychain) { return {}; } - return await this.encryptPrvForUserAsync(decryptedKeychain.prv, decryptedKeychain.pub, pubkey, path); + return await this.encryptPrvForUserAsync( + decryptedKeychain.prv, + decryptedKeychain.pub, + pubkey, + path, + encryptionVersion + ); } catch (e) { if (e instanceof MissingEncryptedKeychainError) { // ignore this error because this looks like a cold wallet @@ -2078,7 +2087,12 @@ export class Wallet implements IWallet { })) as any; let sharedKeychain; if (needsKeychain) { - sharedKeychain = await this.prepareSharedKeychain(params.walletPassphrase, sharing.pubkey, sharing.path); + sharedKeychain = await this.prepareSharedKeychain( + params.walletPassphrase, + sharing.pubkey, + sharing.path, + params.encryptionVersion + ); } const options: CreateShareOptions = { @@ -3441,6 +3455,7 @@ export class Wallet implements IWallet { backupKeyID, activationCode, } = validateDownloadKeycardParams(params); + const { encryptionVersion } = params; const coinShortName = this.baseCoin.type; const coinName = this.baseCoin.getFullName(); @@ -3449,7 +3464,8 @@ export class Wallet implements IWallet { const doc = await drawKeycardAsync({ jsPDF, QRCode, - encrypt: (p: { input: string; password: string }) => this.bitgo.encryptAsync(p), + encrypt: (p) => this.bitgo.encryptAsync(p), + encryptionVersion, coinShortName, coinName, activationCode, diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 2a6916707b..377fdfe5f6 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -7,7 +7,7 @@ import { bip32 } from '@bitgo/utxo-lib'; import * as _ from 'lodash'; import { CoinFeature } from '@bitgo/statics'; -import { sanitizeLegacyPath } from '../../api'; +import { EncryptionVersion, sanitizeLegacyPath } from '../../api'; import * as common from '../../common'; import { IBaseCoin, KeychainsTriplet, SupplementGenerateWalletOptions } from '../baseCoin'; import { BitGoBase } from '../bitgoBase'; @@ -873,7 +873,11 @@ export class Wallets implements IWallets { * @param walletId * @param userPassword */ - async reshareWalletWithSpenders(walletId: string, userPassword: string): Promise { + async reshareWalletWithSpenders( + walletId: string, + userPassword: string, + encryptionVersion?: EncryptionVersion + ): Promise { const wallet = await this.get({ id: walletId }); if (!wallet?._wallet?.enterprise) { throw new Error('Enterprise not found for the wallet'); @@ -899,6 +903,7 @@ export class Wallets implements IWallets { email: userObject.email.email, reshare: true, skipKeychain: false, + encryptionVersion, }; await wallet.shareWallet(shareParams); } @@ -934,6 +939,7 @@ export class Wallets implements IWallets { const encryptedPrv = await this.bitgo.encryptAsync({ password: params.newWalletPassphrase || params.userPassword, input: walletKeychain.prv, + encryptionVersion: params.encryptionVersion, }); const updateParams: UpdateShareOptions = { @@ -958,7 +964,9 @@ export class Wallets implements IWallets { throw new Error('userPassword param must be provided to decrypt shared key'); } - const walletKeychain = await this.baseCoin.keychains().createUserKeychain(params.userPassword); + const walletKeychain = await this.baseCoin + .keychains() + .createUserKeychain(params.userPassword, params.encryptionVersion); if (_.isUndefined(walletKeychain.encryptedPrv)) { throw new Error('encryptedPrv was not found on wallet keychain'); } @@ -986,7 +994,7 @@ export class Wallets implements IWallets { // If the wallet share was accepted successfully (changed=true), reshare the wallet with the spenders if (response.changed && response.state === 'accepted') { try { - await this.reshareWalletWithSpenders(walletShare.wallet, params.userPassword); + await this.reshareWalletWithSpenders(walletShare.wallet, params.userPassword, params.encryptionVersion); } catch (e) { // TODO: PX-3826 // Do nothing @@ -1035,6 +1043,7 @@ export class Wallets implements IWallets { encryptedPrv = await this.bitgo.encryptAsync({ password: newWalletPassphrase, input: decryptedSharedWalletPrv, + encryptionVersion: params.encryptionVersion, }); const updateParams: UpdateShareOptions = { walletShareId: params.walletShareId, @@ -1108,6 +1117,7 @@ export class Wallets implements IWallets { const encryptedPrv = await this.bitgo.encryptAsync({ password: newWalletPassphrase, input: walletKeychain.prv, + encryptionVersion: params.encryptionVersion, }); return [ { @@ -1134,6 +1144,7 @@ export class Wallets implements IWallets { const newEncryptedPrv = await this.bitgo.encryptAsync({ password: newWalletPassphrase, input: decryptedSharedWalletPrv, + encryptionVersion: params.encryptionVersion, }); const entry: AcceptShareOptionsRequest = { walletShareId: walletShare.id, @@ -1146,6 +1157,7 @@ export class Wallets implements IWallets { encryptedPrv: await this.bitgo.encryptAsync({ password: webauthnInfo.passphrase, input: decryptedSharedWalletPrv, + encryptionVersion: params.encryptionVersion, }), }; } @@ -1208,7 +1220,7 @@ export class Wallets implements IWallets { } assert(params.shares.length > 0, 'no shares are passed'); - const { shares: inputShares, userLoginPassword, newWalletPassphrase } = params; + const { shares: inputShares, userLoginPassword, newWalletPassphrase, encryptionVersion } = params; const allWalletShares = await this.listSharesV2(); @@ -1275,7 +1287,8 @@ export class Wallets implements IWallets { walletShare, userLoginPassword, newWalletPassphrase, - sharingKeychainPrv + sharingKeychainPrv, + encryptionVersion ); } @@ -1317,7 +1330,7 @@ export class Wallets implements IWallets { if (specialOverrideCases.has(walletShareId)) { const walletId = specialOverrideCases.get(walletShareId); try { - await this.reshareWalletWithSpenders(walletId, userLoginPassword); + await this.reshareWalletWithSpenders(walletId, userLoginPassword, encryptionVersion); } catch (e) { // Log error but continue processing other shares console.error(`Error resharing wallet ${walletId} with spenders: ${e?.message}`); @@ -1353,7 +1366,8 @@ export class Wallets implements IWallets { walletShare: WalletShare, userLoginPassword?: string, newWalletPassphrase?: string, - sharingKeychainPrv?: string + sharingKeychainPrv?: string, + encryptionVersion?: EncryptionVersion ): Promise { // Special override case: requires user keychain and signing if ( @@ -1367,7 +1381,7 @@ export class Wallets implements IWallets { const walletKeychain = await this.baseCoin .keychains() - .createUserKeychain(newWalletPassphrase || userLoginPassword); + .createUserKeychain(newWalletPassphrase || userLoginPassword, encryptionVersion); if (!walletKeychain.encryptedPrv) { throw new Error('encryptedPrv was not found on wallet keychain'); } @@ -1406,6 +1420,7 @@ export class Wallets implements IWallets { const encryptedPrv = await this.bitgo.encryptAsync({ password: newWalletPassphrase || userLoginPassword, input: walletKeychain.prv, + encryptionVersion, }); return [ @@ -1451,6 +1466,7 @@ export class Wallets implements IWallets { const encryptedPrv = await this.bitgo.encryptAsync({ password: newWalletPassphrase || userLoginPassword, input: decryptedPrv, + encryptionVersion, }); return [ diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaEncryptionVersion.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaEncryptionVersion.ts new file mode 100644 index 0000000000..828a2a4ce3 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaEncryptionVersion.ts @@ -0,0 +1,74 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { EcdsaUtils } from '../../../../../../src/bitgo/utils/tss/ecdsa/ecdsa'; +import { BitGoBase, IBaseCoin } from '../../../../../../src'; + +/** + * Regression tests for the ECDSA MPCv1 createKeychains encryptionVersion bug. + * + * Previously, EcdsaUtils.createKeychains dropped `encryptionVersion` silently — + * it was not declared in the params and was never forwarded to createUserKeychain + * or createBackupKeychain, causing a version mismatch within a wallet: v2 for + * encryptedWalletPassphrase but v1 for the user/backup keychains. + */ +describe('EcdsaUtils.createKeychains - encryptionVersion forwarding', function () { + let ecdsaUtils: EcdsaUtils; + let createUserKeychainStub: sinon.SinonStub; + let createBackupKeychainStub: sinon.SinonStub; + + const fakeKeychain = { id: 'key-id', pub: 'pub', commonKeychain: 'ckc', type: 'tss' as const }; + const fakeGpgKey = { publicKey: 'pub', privateKey: 'prv' }; + const fakeKeyShare = { userHeldKeyShare: {} }; + + beforeEach(function () { + const mockBitgo = { getEnv: sinon.stub().returns('test') } as unknown as BitGoBase; + const mockCoin = {} as unknown as IBaseCoin; + + ecdsaUtils = new EcdsaUtils(mockBitgo, mockCoin); + + createUserKeychainStub = sinon.stub(ecdsaUtils, 'createUserKeychain').resolves(fakeKeychain); + createBackupKeychainStub = sinon.stub(ecdsaUtils, 'createBackupKeychain').resolves(fakeKeychain); + sinon.stub(ecdsaUtils, 'createBitgoKeychain').resolves(fakeKeychain); + sinon.stub(ecdsaUtils, 'createBackupKeyShares').resolves(fakeKeyShare as any); + sinon.stub(ecdsaUtils, 'getBitgoGpgPubkeyBasedOnFeatureFlags').resolves({ mpcv2PublicKey: undefined } as any); + + sinon.stub(ecdsaUtils as any, 'getBackupGpgPubKey').resolves(fakeGpgKey); + sinon.stub(require('../../../../../../src/bitgo/utils/opengpgUtils'), 'generateGPGKeyPair').resolves(fakeGpgKey); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('forwards encryptionVersion: 2 to createUserKeychain', async function () { + await ecdsaUtils.createKeychains({ passphrase: 'pass', encryptionVersion: 2 }); + + assert.ok(createUserKeychainStub.calledOnce); + const userParams = createUserKeychainStub.firstCall.args[0]; + assert.strictEqual(userParams.encryptionVersion, 2); + }); + + it('forwards encryptionVersion: 2 to createBackupKeychain', async function () { + await ecdsaUtils.createKeychains({ passphrase: 'pass', encryptionVersion: 2 }); + + assert.ok(createBackupKeychainStub.calledOnce); + const backupParams = createBackupKeychainStub.firstCall.args[0]; + assert.strictEqual(backupParams.encryptionVersion, 2); + }); + + it('forwards encryptionVersion: undefined when not set', async function () { + await ecdsaUtils.createKeychains({ passphrase: 'pass' }); + + assert.ok(createUserKeychainStub.calledOnce); + assert.ok(createBackupKeychainStub.calledOnce); + assert.strictEqual(createUserKeychainStub.firstCall.args[0].encryptionVersion, undefined); + assert.strictEqual(createBackupKeychainStub.firstCall.args[0].encryptionVersion, undefined); + }); + + it('forwards encryptionVersion: 1 explicitly', async function () { + await ecdsaUtils.createKeychains({ passphrase: 'pass', encryptionVersion: 1 }); + + assert.strictEqual(createUserKeychainStub.firstCall.args[0].encryptionVersion, 1); + assert.strictEqual(createBackupKeychainStub.firstCall.args[0].encryptionVersion, 1); + }); +}); diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index b9269d3d22..12e2f5b09c 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -880,6 +880,66 @@ describe('ECDSA MPC v2', async () => { assert.strictEqual(sigParts[1].length, 64, 'Signature R must be 32 bytes hex'); assert.strictEqual(sigParts[2].length, 64, 'Signature S must be 32 bytes hex'); }); + + describe('createOfflineRound1Share and createOfflineRound2Share - encryptionVersion: 1 in v1 path', async () => { + let ecdsaMPCv2UtilsWithSpy: EcdsaMPCv2Utils; + let encryptAsyncSpy: sinon.SinonStub; + + before(async () => { + const mockBg = {} as BitGoBase; + mockBg.getEnv = sinon.stub().returns('test'); + const encryptImpl = (params: { password: string; input: string; adata?: string }) => { + const salt = randomBytes(8); + const iv = randomBytes(16); + return sjcl.encrypt(params.password, params.input, { + salt: [bytesToWord(salt.subarray(0, 4)), bytesToWord(salt.subarray(4))], + iv: [ + bytesToWord(iv.subarray(0, 4)), + bytesToWord(iv.subarray(4, 8)), + bytesToWord(iv.subarray(8, 12)), + bytesToWord(iv.subarray(12, 16)), + ], + adata: params.adata, + }); + }; + encryptAsyncSpy = sinon.stub().callsFake(async (params) => encryptImpl(params)); + mockBg.encrypt = sinon.stub().callsFake(encryptImpl); + mockBg.encryptAsync = encryptAsyncSpy; + mockBg.decrypt = sinon.stub().callsFake((params) => sjcl.decrypt(params.password, params.input)); + mockBg.decryptAsync = sinon.stub().callsFake(async (params) => sjcl.decrypt(params.password, params.input)); + + const mockCoin = {} as IBaseCoin; + mockCoin.getHashFunction = sinon.stub().callsFake(() => createKeccakHash('keccak256') as Hash); + ecdsaMPCv2UtilsWithSpy = new EcdsaMPCv2Utils(mockBg, mockCoin); + }); + + it('createOfflineRound1Share uses encryptionVersion: 1 in v1 (non-v2 envelope) path', async () => { + const txRequest = { + txRequestId: 'req-id', + walletId: walletID, + transactions: [{ unsignedTx: { signableHex: 'deadbeef01', derivationPath: 'm/0', serializedTxHex: '' } }], + apiVersion: 'full', + } as any; + + encryptAsyncSpy.resetHistory(); + + await ecdsaMPCv2UtilsWithSpy.createOfflineRound1Share({ + txRequest, + prv: Buffer.from(userShare).toString('base64'), + walletPassphrase, + encryptedPrv: undefined, + }); + + assert.ok(encryptAsyncSpy.called, 'encryptAsync should be called in v1 path'); + for (const call of encryptAsyncSpy.getCalls()) { + assert.strictEqual( + call.args[0].encryptionVersion, + 1, + 'encryptionVersion should be hardcoded to 1 in the v1 signing session path' + ); + } + }); + }); }); function bytesToWord(bytes?: Uint8Array | number[]): number { diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsEncryptionVersion.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsEncryptionVersion.ts new file mode 100644 index 0000000000..4e558e0b16 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsEncryptionVersion.ts @@ -0,0 +1,226 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import 'should'; +import { Wallets } from '../../../../src/bitgo/wallet/wallets'; +import { Wallet } from '../../../../src/bitgo/wallet/wallet'; + +describe('Wallets - encryptionVersion threading', function () { + let wallets: Wallets; + let mockBitGo: any; + let mockBaseCoin: any; + let mockKeychains: any; + + const userPrv = 'xprvSomeUserPrivateKey'; + const userPub = + 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; + + beforeEach(function () { + mockKeychains = { + create: sinon.stub().returns({ pub: userPub, prv: userPrv }), + add: sinon.stub().resolves({ id: 'user-key-id', pub: userPub, encryptedPrv: 'encrypted-prv' }), + }; + + mockBitGo = { + encryptAsync: sinon + .stub() + .callsFake(async ({ password, input }: { password: string; input: string }) => `enc:${password}:${input}`), + decryptAsync: sinon.stub().resolves('decryptedPrv'), + get: sinon.stub().returns({ result: sinon.stub(), query: sinon.stub().returnsThis() }), + post: sinon.stub().returns({ send: sinon.stub().returns({ result: sinon.stub().resolves({}) }) }), + put: sinon.stub().returns({ + send: sinon + .stub() + .returns({ result: sinon.stub().resolves({ acceptedWalletShares: [], walletShareUpdateErrors: [] }) }), + }), + getECDHKeychain: sinon.stub().resolves({ encryptedXprv: 'encXprv' }), + setRequestTracer: sinon.stub(), + url: sinon.stub().returns('/test/url'), + }; + + mockBaseCoin = { + keychains: sinon.stub().returns(mockKeychains), + url: sinon.stub().callsFake((path: string) => path), + getFamily: sinon.stub().returns('btc'), + getChain: sinon.stub().returns('btc'), + supportsTss: sinon.stub().returns(false), + getMPCAlgorithm: sinon.stub().returns('ecdsa'), + }; + + wallets = new Wallets(mockBitGo, mockBaseCoin); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('acceptShare', function () { + it('passes encryptionVersion: 2 to encryptAsync on the multiUserKeyRotationRequired path', async function () { + mockBitGo.get.returns({ + result: sinon.stub().resolves({ + userMultiKeyRotationRequired: true, + keychain: null, + permissions: ['spend'], + wallet: 'wallet-id', + }), + }); + + await wallets.acceptShare({ + walletShareId: 'share-id', + userPassword: 'my-password', + encryptionVersion: 2, + }); + + assert.ok(mockBitGo.encryptAsync.called, 'encryptAsync should have been called'); + const call = mockBitGo.encryptAsync.firstCall; + assert.strictEqual(call.args[0].encryptionVersion, 2); + }); + + it('passes encryptionVersion: undefined when not set', async function () { + mockBitGo.get.returns({ + result: sinon.stub().resolves({ + userMultiKeyRotationRequired: true, + keychain: null, + permissions: ['spend'], + wallet: 'wallet-id', + }), + }); + + await wallets.acceptShare({ + walletShareId: 'share-id', + userPassword: 'my-password', + }); + + assert.ok(mockBitGo.encryptAsync.called); + const call = mockBitGo.encryptAsync.firstCall; + assert.strictEqual(call.args[0].encryptionVersion, undefined); + }); + }); + + describe('bulkAcceptShare', function () { + const walletSharesList = { + incoming: [ + { + id: 'share-id', + userMultiKeyRotationRequired: true, + keychain: null, + permissions: ['spend'], + }, + ], + outgoing: [], + }; + + beforeEach(function () { + mockBitGo.get.returns({ result: sinon.stub().resolves(walletSharesList) }); + }); + + it('passes encryptionVersion: 2 to encryptAsync on the multiUserKeyRotationRequired path', async function () { + await wallets.bulkAcceptShare({ + walletShareIds: ['share-id'], + userLoginPassword: 'login-password', + encryptionVersion: 2, + }); + + assert.ok(mockBitGo.encryptAsync.called); + const call = mockBitGo.encryptAsync.firstCall; + assert.strictEqual(call.args[0].encryptionVersion, 2); + }); + + it('passes encryptionVersion: undefined when not set', async function () { + await wallets.bulkAcceptShare({ + walletShareIds: ['share-id'], + userLoginPassword: 'login-password', + }); + + assert.ok(mockBitGo.encryptAsync.called); + const call = mockBitGo.encryptAsync.firstCall; + assert.strictEqual(call.args[0].encryptionVersion, undefined); + }); + }); + + describe('Wallet.shareWallet / createBulkWalletShare', function () { + let wallet: Wallet; + + beforeEach(function () { + const mockWalletData = { + id: 'wallet-id', + keys: ['key-1', 'key-2', 'key-3'], + coin: 'btc', + label: 'Test Wallet', + users: [], + multisigType: 'onchain', + type: 'hot', + }; + mockBaseCoin.supportsTss = sinon.stub().returns(false); + mockBaseCoin.getMPCAlgorithm = sinon.stub().returns('ecdsa'); + wallet = new Wallet(mockBitGo, mockBaseCoin, mockWalletData); + }); + + it('shareWallet passes encryptionVersion to prepareSharedKeychain', async function () { + const prepareStub = sinon.stub(wallet, 'prepareSharedKeychain').resolves({}); + mockBitGo.getSharingKey = sinon.stub().resolves({ userId: 'user-id', pubkey: 'recvPub', path: 'm/0' }); + mockBitGo.post.returns({ send: sinon.stub().returns({ result: sinon.stub().resolves({}) }) }); + + await wallet.shareWallet({ + email: 'test@test.com', + permissions: 'spend', + walletPassphrase: 'passphrase', + encryptionVersion: 2, + }); + + assert.ok(prepareStub.calledOnce, 'prepareSharedKeychain should be called'); + assert.strictEqual(prepareStub.firstCall.args[3], 2, 'encryptionVersion should be forwarded'); + }); + + it('shareWallet passes encryptionVersion: undefined when not set', async function () { + const prepareStub = sinon.stub(wallet, 'prepareSharedKeychain').resolves({}); + mockBitGo.getSharingKey = sinon.stub().resolves({ userId: 'user-id', pubkey: 'recvPub', path: 'm/0' }); + mockBitGo.post.returns({ send: sinon.stub().returns({ result: sinon.stub().resolves({}) }) }); + + await wallet.shareWallet({ + email: 'test@test.com', + permissions: 'spend', + walletPassphrase: 'passphrase', + }); + + assert.ok(prepareStub.calledOnce); + assert.strictEqual(prepareStub.firstCall.args[3], undefined); + }); + + it('createBulkWalletShare passes encryptionVersion to encryptPrvForUserAsync', async function () { + const encryptPrvStub = sinon + .stub(wallet, 'encryptPrvForUserAsync') + .resolves({ encryptedPrv: 'enc', pub: 'pub', fromPubKey: 'fpk', toPubKey: 'tpk', path: 'm/0' }); + sinon + .stub(wallet as any, 'getDecryptedKeychainForSharing') + .resolves({ prv: 'prv', pub: 'pub', encryptedPrv: 'encPrv' }); + sinon.stub(wallet as any, 'createBulkKeyShares').resolves({ shares: [] }); + + await wallet.createBulkWalletShare({ + walletPassphrase: 'passphrase', + keyShareOptions: [{ userId: 'user-1', pubKey: 'pubKey', path: 'm/0', permissions: ['spend'] }], + encryptionVersion: 2, + }); + + assert.ok(encryptPrvStub.calledOnce); + assert.strictEqual(encryptPrvStub.firstCall.args[4], 2, 'encryptionVersion should be forwarded'); + }); + + it('createBulkWalletShare passes encryptionVersion: undefined when not set', async function () { + const encryptPrvStub = sinon + .stub(wallet, 'encryptPrvForUserAsync') + .resolves({ encryptedPrv: 'enc', pub: 'pub', fromPubKey: 'fpk', toPubKey: 'tpk', path: 'm/0' }); + sinon + .stub(wallet as any, 'getDecryptedKeychainForSharing') + .resolves({ prv: 'prv', pub: 'pub', encryptedPrv: 'encPrv' }); + sinon.stub(wallet as any, 'createBulkKeyShares').resolves({ shares: [] }); + + await wallet.createBulkWalletShare({ + walletPassphrase: 'passphrase', + keyShareOptions: [{ userId: 'user-1', pubKey: 'pubKey', path: 'm/0', permissions: ['spend'] }], + }); + + assert.ok(encryptPrvStub.calledOnce); + assert.strictEqual(encryptPrvStub.firstCall.args[4], undefined); + }); + }); +});