diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index 7f830bd1a9..8edcfeb513 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -2554,7 +2554,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { const walletPassphrase = buildParams.walletPassphrase; const userKeychain = await this.keychains().get({ id: wallet.keyIds()[0] }); - const userPrv = wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); + const userPrv = await wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); const userPrvBuffer = bip32.fromBase58(userPrv).privateKey; if (!userPrvBuffer) { throw new Error('invalid userPrv'); diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index b5d04a47a7..653f7bf59c 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -677,7 +677,7 @@ export abstract class AbstractUtxoCoin /** * @deprecated - use function verifyUserPublicKey instead */ - protected verifyUserPublicKey(params: VerifyUserPublicKeyOptions): boolean { + protected async verifyUserPublicKey(params: VerifyUserPublicKeyOptions): Promise { return verifyUserPublicKey(this.bitgo, params); } diff --git a/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts b/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts index cfb4332773..f9c0e46877 100644 --- a/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts +++ b/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts @@ -302,7 +302,7 @@ export class InscriptionBuilder implements IInscriptionBuilder { txPrebuild: PrebuildTransactionResult ): Promise { const userKeychain = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] }); - const prv = this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); + const prv = await this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); const halfSigned = (await this.wallet.signTransaction({ prv, txPrebuild })) as HalfSignedUtxoTransaction; return this.wallet.submitTransaction({ halfSigned }); diff --git a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts index d9dd99a2f1..46f8f01246 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts @@ -79,7 +79,11 @@ export async function verifyTransaction( let userPublicKeyVerified = false; try { // verify the user public key matches the private key - this will throw if there is no match - userPublicKeyVerified = verifyUserPublicKey(bitgo, { userKeychain: keychains.user, disableNetworking, txParams }); + userPublicKeyVerified = await verifyUserPublicKey(bitgo, { + userKeychain: keychains.user, + disableNetworking, + txParams, + }); } catch (e) { debug('failed to verify user public key!', e); } diff --git a/modules/abstract-utxo/src/verifyKey.ts b/modules/abstract-utxo/src/verifyKey.ts index 15dbbc1391..a33ca6f2aa 100644 --- a/modules/abstract-utxo/src/verifyKey.ts +++ b/modules/abstract-utxo/src/verifyKey.ts @@ -84,7 +84,7 @@ export function verifyCustomChangeKeySignatures /** * Decrypt the wallet's user private key and verify that the claimed public key matches */ -export function verifyUserPublicKey(bitgo: BitGoBase, params: VerifyUserPublicKeyOptions): boolean { +export async function verifyUserPublicKey(bitgo: BitGoBase, params: VerifyUserPublicKeyOptions): Promise { const { userKeychain, txParams, disableNetworking } = params; if (!userKeychain) { throw new Error('user keychain is required'); @@ -94,7 +94,7 @@ export function verifyUserPublicKey(bitgo: BitGoBase, params: VerifyUserPublicKe let userPrv = userKeychain.prv; if (!userPrv && txParams.walletPassphrase) { - userPrv = decryptKeychainPrivateKey(bitgo, userKeychain, txParams.walletPassphrase); + userPrv = await decryptKeychainPrivateKey(bitgo, userKeychain, txParams.walletPassphrase); } if (!userPrv) { diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts index 21495eba42..dee40f849f 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts @@ -176,6 +176,55 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () { assert.equal(bitgoKeychain.source, 'bitgo'); }); + it('should generate TSS MPCv2 keys with v2 encryption envelopes', async function () { + const bitgoSession = new DklsDkg.Dkg(3, 2, 2); + + const round1Nock = await nockKeyGenRound1(bitgoSession, 1); + const round2Nock = await nockKeyGenRound2(bitgoSession, 1); + const round3Nock = await nockKeyGenRound3(bitgoSession, 1); + const addKeyNock = await nockAddKeyChain(coinName, 3); + const params = { + passphrase: 'test', + enterprise: enterpriseId, + originalPasscodeEncryptionCode: '123456', + encryptionVersion: 2 as const, + }; + const { userKeychain, backupKeychain, bitgoKeychain } = await tssUtils.createKeychains(params); + assert.ok(round1Nock.isDone()); + assert.ok(round2Nock.isDone()); + assert.ok(round3Nock.isDone()); + assert.ok(addKeyNock.isDone()); + + assert.ok(userKeychain); + assert.equal(userKeychain.source, 'user'); + assert.ok(userKeychain.commonKeychain); + assert.ok(ECDSAUtils.EcdsaMPCv2Utils.validateCommonKeychainPublicKey(userKeychain.commonKeychain)); + + // Verify v2 envelopes for encryptedPrv + assert.ok(userKeychain.encryptedPrv); + const encryptedPrvParsed: { v: number } = JSON.parse(userKeychain.encryptedPrv); + assert.equal(encryptedPrvParsed.v, 2, 'encryptedPrv should be a v2 envelope'); + + // Verify v2 envelopes for reducedEncryptedPrv + assert.ok(userKeychain.reducedEncryptedPrv); + const reducedEncryptedPrvParsed: { v: number } = JSON.parse(userKeychain.reducedEncryptedPrv); + assert.equal(reducedEncryptedPrvParsed.v, 2, 'reducedEncryptedPrv should be a v2 envelope'); + + // Verify v2 envelope is decryptable via decryptAsync + const decrypted = await bitgo.decryptAsync({ input: userKeychain.encryptedPrv, password: params.passphrase }); + assert.ok(decrypted, 'decryptAsync should successfully decrypt v2 envelope'); + + // Verify backup keychain also uses v2 envelopes + assert.ok(backupKeychain); + assert.equal(backupKeychain.source, 'backup'); + assert.ok(backupKeychain.encryptedPrv); + const backupEncryptedPrvParsed: { v: number } = JSON.parse(backupKeychain.encryptedPrv); + assert.equal(backupEncryptedPrvParsed.v, 2, 'backup encryptedPrv should be a v2 envelope'); + + assert.ok(bitgoKeychain); + assert.equal(bitgoKeychain.source, 'bitgo'); + }); + it('should generate TSS MPCv2 keys for retrofit', async function () { const xiList = [ Array.from(bigIntToBufferBE(BigInt(1), 32)), diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index 08540a71c9..5afbf1d542 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -546,6 +546,49 @@ describe('TSS Utils:', async function () { }) .should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.'); }); + + it('should generate TSS key chains with v2 encryption envelopes', async function () { + const passphrase = 'passphrase'; + const userKeyShare = MPC.keyShare(1, 2, 3); + const backupKeyShare = MPC.keyShare(2, 2, 3); + + await nockBitgoKeychain({ + coin: coinName, + userKeyShare, + backupKeyShare, + bitgoKeyShare, + userGpgKey, + backupGpgKey, + bitgoGpgKey, + }); + await nockUserKeychain({ coin: coinName }); + await nockBackupKeychain({ coin: coinName }); + + const bitgoKeychain = await tssUtils.createBitgoKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + }); + const userKeychain = await tssUtils.createUserKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + bitgoKeychain, + passphrase, + encryptionVersion: 2, + }); + + should.exist(userKeychain.encryptedPrv); + const envelope = JSON.parse(userKeychain.encryptedPrv!); + envelope.v.should.equal(2); + + const decrypted = await bitgo.decryptAsync({ input: userKeychain.encryptedPrv!, password: passphrase }); + should.exist(decrypted); + const parsed: Record = JSON.parse(decrypted); + should.exist(parsed.uShare); + }); }); describe('signTxRequest:', function () { diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index 2e9e9b8e82..ad1bc60e62 100644 --- a/modules/bitgo/test/v2/unit/wallet.ts +++ b/modules/bitgo/test/v2/unit/wallet.ts @@ -352,7 +352,7 @@ describe('V2 Wallet:', function () { prv, coldDerivationSeed: '123', }; - wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); + (await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv); }); it('should use the user keychain derivedFromParentWithSeed as the cold derivation seed if none is provided', async () => { @@ -365,7 +365,7 @@ describe('V2 Wallet:', function () { type: 'independent', }, }; - wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); + (await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv); }); it('should prefer the explicit cold derivation seed to the user keychain derivedFromParentWithSeed', async () => { @@ -379,7 +379,7 @@ describe('V2 Wallet:', function () { type: 'independent', }, }; - wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); + (await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv); }); it('should return the prv provided for TSS SMC', async () => { @@ -407,7 +407,7 @@ describe('V2 Wallet:', function () { prv, keychain, }; - wallet.getUserPrv(userPrvOptions).should.eql(prv); + (await wallet.getUserPrv(userPrvOptions)).should.eql(prv); }); }); diff --git a/modules/bitgo/test/v2/unit/wallets.ts b/modules/bitgo/test/v2/unit/wallets.ts index 206f937962..acc839e280 100644 --- a/modules/bitgo/test/v2/unit/wallets.ts +++ b/modules/bitgo/test/v2/unit/wallets.ts @@ -2440,7 +2440,7 @@ describe('V2 Wallets:', function () { const keychainTest: OptionalKeychainEncryptedKey = { encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }), }; - const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); + const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); if (!userPrv) { throw new Error('Unable to decrypt user keychain'); } @@ -2512,7 +2512,7 @@ describe('V2 Wallets:', function () { const keychainTest: OptionalKeychainEncryptedKey = { encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }), }; - const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); + const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); if (!userPrv) { throw new Error('Unable to decrypt user keychain'); } @@ -2591,7 +2591,7 @@ describe('V2 Wallets:', function () { const keychainTest: OptionalKeychainEncryptedKey = { encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }), }; - const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); + const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); if (!userPrv) { throw new Error('Unable to decrypt user keychain'); } @@ -2659,7 +2659,7 @@ describe('V2 Wallets:', function () { const keychainTest: OptionalKeychainEncryptedKey = { encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }), }; - const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); + const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); if (!userPrv) { throw new Error('Unable to decrypt user keychain'); } @@ -2760,7 +2760,7 @@ describe('V2 Wallets:', function () { const keychainTest: OptionalKeychainEncryptedKey = { encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }), }; - const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); + const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); if (!userPrv) { throw new Error('Unable to decrypt user keychain'); } @@ -2862,7 +2862,7 @@ describe('V2 Wallets:', function () { const keychainTest: OptionalKeychainEncryptedKey = { encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }), }; - const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); + const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase); if (!userPrv) { throw new Error('Unable to decrypt user keychain'); } diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index 19d481893b..47dd74f1be 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -41,6 +41,8 @@ import { verifyResponseAsync, } from './api'; import { decrypt, decryptAsync, encrypt } from './encrypt'; +import { createEncryptionSession } from './encryptionSession'; +import { encryptV2 } from './encryptV2'; import { verifyAddress } from './v1/verifyAddress'; import { AccessTokenOptions, @@ -715,6 +717,29 @@ export class BitGoAPI implements BitGoBase { return encrypt(params.password, params.input, { adata: params.adata }); } + /** + * Async encrypt that dispatches to v1 (SJCL) or v2 (Argon2id + AES-256-GCM) + * based on `encryptionVersion`. + */ + async encryptAsync(params: EncryptOptions): Promise { + common.validateParams(params, ['input', 'password'], []); + if (!params.password) { + throw new Error('cannot encrypt without password'); + } + if (params.encryptionVersion === 2) { + return encryptV2(params.password, params.input); + } + return encrypt(params.password, params.input, { adata: params.adata }); + } + + /** + * Create an encryption session for multi-call operations. + * Runs Argon2id once; all subsequent calls derive keys via HKDF. + */ + async createEncryptionSession(password: string) { + return createEncryptionSession(password); + } + /** * Decrypt an encrypted string locally. */ diff --git a/modules/sdk-api/src/encryptV2.ts b/modules/sdk-api/src/encryptV2.ts index 6ae7ef3189..03455c9951 100644 --- a/modules/sdk-api/src/encryptV2.ts +++ b/modules/sdk-api/src/encryptV2.ts @@ -101,12 +101,16 @@ export function hkdfDeriveAesKey(hkdfKey: CryptoKey, hkdfSalt: Uint8Array, usage } export async function aesGcmEncrypt(key: CryptoKey, iv: Uint8Array, plaintext: string): Promise { - const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext)); + const ct = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv, tagLength: 128 }, + key, + new TextEncoder().encode(plaintext) + ); return new Uint8Array(ct); } export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array, ct: Uint8Array): Promise { - const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct); + const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv, tagLength: 128 }, key, ct); return new TextDecoder().decode(plaintext); } diff --git a/modules/sdk-api/src/encryptionSession.ts b/modules/sdk-api/src/encryptionSession.ts index 8f479a7233..3fad4518cc 100644 --- a/modules/sdk-api/src/encryptionSession.ts +++ b/modules/sdk-api/src/encryptionSession.ts @@ -21,7 +21,7 @@ import { */ export class EncryptionSession { private hkdfKey: CryptoKey | null; - private readonly argon2SaltB64: string; + private argon2SaltB64: string | null; private readonly memorySize: number; private readonly iterations: number; private readonly parallelism: number; @@ -53,7 +53,7 @@ export class EncryptionSession { if (!envelope.hkdfSalt) { throw new Error('envelope was not encrypted with a session; use decryptV2 instead'); } - if (envelope.salt !== this.argon2SaltB64) { + if (envelope.salt !== this.getSaltOrThrow()) { throw new Error('envelope was not encrypted with this session'); } const iv = new Uint8Array(Buffer.from(envelope.iv, 'base64')); @@ -65,22 +65,30 @@ export class EncryptionSession { destroy(): void { this.hkdfKey = null; + this.argon2SaltB64 = null; } private getKeyOrThrow(): CryptoKey { - if (this.hkdfKey === null) { + if (this.hkdfKey === null || this.argon2SaltB64 === null) { throw new Error('EncryptionSession has been destroyed'); } return this.hkdfKey; } + private getSaltOrThrow(): string { + if (this.argon2SaltB64 === null) { + throw new Error('EncryptionSession has been destroyed'); + } + return this.argon2SaltB64; + } + private buildEnvelope(hkdfSalt: Uint8Array, iv: Uint8Array, ct: Uint8Array): V2Envelope { return { v: 2, m: this.memorySize, t: this.iterations, p: this.parallelism, - salt: this.argon2SaltB64, + salt: this.getSaltOrThrow(), hkdfSalt: Buffer.from(hkdfSalt).toString('base64'), iv: Buffer.from(iv).toString('base64'), ct: Buffer.from(ct).toString('base64'), diff --git a/modules/sdk-api/test/unit/encrypt.ts b/modules/sdk-api/test/unit/encrypt.ts index 205ad18f81..eb86a9c62e 100644 --- a/modules/sdk-api/test/unit/encrypt.ts +++ b/modules/sdk-api/test/unit/encrypt.ts @@ -2,6 +2,7 @@ import assert from 'assert'; import { randomBytes } from 'crypto'; import { decrypt, decryptAsync, decryptV2, encrypt, encryptV2, V2Envelope, createEncryptionSession } from '../../src'; +import { BitGoAPI } from '../../src/bitgoAPI'; describe('encryption methods tests', () => { describe('encrypt', () => { @@ -320,4 +321,48 @@ describe('encryption methods tests', () => { assert.strictEqual(envelope.p, 2); }); }); + + describe('BitGoAPI.encryptAsync', () => { + let bitgo: BitGoAPI; + const password = 'test-password'; + const plaintext = 'hello encryptAsync'; + + before(() => { + bitgo = new BitGoAPI({ env: 'test' }); + }); + + it('dispatches to v1 by default and output is decryptable via decrypt', async () => { + const ct = await bitgo.encryptAsync({ input: plaintext, password }); + const envelope = JSON.parse(ct); + assert.notStrictEqual(envelope.v, 2, 'default should not produce v2 envelope'); + assert.strictEqual(decrypt(password, ct), plaintext); + }); + + it('dispatches to v2 when encryptionVersion: 2 and output is decryptable via decryptAsync', async () => { + const ct = await bitgo.encryptAsync({ input: plaintext, password, encryptionVersion: 2 }); + const envelope: V2Envelope = JSON.parse(ct); + assert.strictEqual(envelope.v, 2); + const result = await decryptAsync(password, ct); + assert.strictEqual(result, plaintext); + }); + }); + + describe('BitGoAPI.createEncryptionSession', () => { + let bitgo: BitGoAPI; + const password = 'test-password'; + const plaintext = 'hello session'; + + before(() => { + bitgo = new BitGoAPI({ env: 'test' }); + }); + + it('returns working session (encrypt/decrypt/destroy)', async () => { + const session = await bitgo.createEncryptionSession(password); + const ct = await session.encrypt(plaintext); + const result = await session.decrypt(ct); + assert.strictEqual(result, plaintext); + session.destroy(); + await assert.rejects(() => session.encrypt(plaintext), /destroyed/); + }); + }); }); diff --git a/modules/sdk-core/src/api/types.ts b/modules/sdk-core/src/api/types.ts index 10e73f3fe3..1c8e1e5214 100644 --- a/modules/sdk-core/src/api/types.ts +++ b/modules/sdk-core/src/api/types.ts @@ -21,6 +21,7 @@ export interface EncryptOptions { input: string; password?: string; adata?: string; + encryptionVersion?: 2; } export interface GetSharingKeyOptions { diff --git a/modules/sdk-core/src/bitgo/bitgoBase.ts b/modules/sdk-core/src/bitgo/bitgoBase.ts index ac03322b92..817032ff0d 100644 --- a/modules/sdk-core/src/bitgo/bitgoBase.ts +++ b/modules/sdk-core/src/bitgo/bitgoBase.ts @@ -19,6 +19,12 @@ export interface BitGoBase { decryptKeys(params: DecryptKeysOptions): string[]; del(url: string): BitGoRequest; encrypt(params: EncryptOptions): string; + encryptAsync(params: EncryptOptions): Promise; + createEncryptionSession(password: string): Promise<{ + encrypt(plaintext: string): Promise; + decrypt(ciphertext: string): Promise; + destroy(): void; + }>; readonly env: EnvironmentName; fetchConstants(): Promise; get(url: string): BitGoRequest; diff --git a/modules/sdk-core/src/bitgo/keychain/decryptKeychain.ts b/modules/sdk-core/src/bitgo/keychain/decryptKeychain.ts index 095c62452c..8327af59cb 100644 --- a/modules/sdk-core/src/bitgo/keychain/decryptKeychain.ts +++ b/modules/sdk-core/src/bitgo/keychain/decryptKeychain.ts @@ -2,9 +2,9 @@ import { BitGoBase } from '../bitgoBase'; import { OptionalKeychainEncryptedKey } from './iKeychains'; import { notEmpty } from '../utils'; -function maybeDecrypt(bitgo: BitGoBase, input: string, password: string): string | undefined { +async function maybeDecrypt(bitgo: BitGoBase, input: string, password: string): Promise { try { - return bitgo.decrypt({ + return await bitgo.decryptAsync({ input, password, }); @@ -17,19 +17,20 @@ function maybeDecrypt(bitgo: BitGoBase, input: string, password: string): string * Decrypts the private key of a keychain. * This method will try the password against the traditional encryptedPrv, * and any webauthn device encryptedPrvs. + * Auto-detects v1 (SJCL) and v2 (Argon2id) envelopes. * * @param bitgo * @param keychain * @param password */ -export function decryptKeychainPrivateKey( +export async function decryptKeychainPrivateKey( bitgo: BitGoBase, keychain: OptionalKeychainEncryptedKey, password: string -): string | undefined { +): Promise { const prvs = [keychain.encryptedPrv, ...(keychain.webauthnDevices ?? []).map((d) => d.encryptedPrv)].filter(notEmpty); for (const prv of prvs) { - const decrypted = maybeDecrypt(bitgo, prv, password); + const decrypted = await maybeDecrypt(bitgo, prv, password); if (decrypted) { return decrypted; } diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index 4876d031d3..17670f1843 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -166,6 +166,7 @@ export interface CreateBackupOptions { prv?: string; encryptedPrv?: string; passphrase?: string; + encryptionVersion?: 2; } export interface CreateBitGoOptions { @@ -188,6 +189,7 @@ export interface CreateMpcOptions { originalPasscodeEncryptionCode?: string; enterprise?: string; retrofit?: DecryptedRetrofitPayload; + encryptionVersion?: 2; } export interface RecreateMpcOptions extends Omit { diff --git a/modules/sdk-core/src/bitgo/keychain/keychains.ts b/modules/sdk-core/src/bitgo/keychain/keychains.ts index 93da7a7dc6..4ed7e0e815 100644 --- a/modules/sdk-core/src/bitgo/keychain/keychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/keychains.ts @@ -275,7 +275,13 @@ export class Keychains implements IKeychains { const key = this.create(); _.extend(params, key); if (params.passphrase !== undefined) { - _.extend(params, { encryptedPrv: this.bitgo.encrypt({ input: key.prv, password: params.passphrase }) }); + _.extend(params, { + encryptedPrv: await this.bitgo.encryptAsync({ + input: key.prv, + password: params.passphrase, + encryptionVersion: params.encryptionVersion, + }), + }); } } @@ -333,6 +339,7 @@ export class Keychains implements IKeychains { enterprise: params.enterprise, originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, retrofit: params.retrofit, + encryptionVersion: params.encryptionVersion, }); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index ca9d96a791..74ffdf8818 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -190,6 +190,7 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil enterprise?: string | undefined; originalPasscodeEncryptionCode?: string | undefined; isThirdPartyBackup?: boolean; + encryptionVersion?: 2; }): Promise { throw new Error('Method not implemented.'); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts index d1dfb1b3d4..ae191153b4 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts @@ -482,6 +482,7 @@ export type CreateKeychainParamsBase = { passphrase?: string; enterprise?: string; originalPasscodeEncryptionCode?: string; + encryptionVersion?: 2; }; export type CreateBitGoKeychainParamsBase = Omit; @@ -712,6 +713,7 @@ export interface ITssUtils { enterprise?: string; originalPasscodeEncryptionCode?: string; isThirdPartyBackup?: boolean; + encryptionVersion?: 2; }): Promise; signTxRequest(params: { txRequest: string | TxRequest; prv: string; reqId: IRequestTracer }): Promise; signTxRequestForMessage(params: TSSParams): Promise; 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 1e45de5ebc..6250fd526d 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts @@ -107,6 +107,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase: string; enterprise?: string | undefined; originalPasscodeEncryptionCode?: string | undefined; + encryptionVersion?: 2; }): Promise { const MPC = new Ecdsa(); const m = 2; 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 4a623111ef..bd47a86aa0 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -51,6 +51,14 @@ import { BaseEcdsaUtils } from './base'; import { EcdsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './ecdsaMPCv2KeyGenSender'; import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys'; +function isV2Envelope(ciphertext: string): boolean { + try { + return JSON.parse(ciphertext).v === 2; + } catch { + return false; + } +} + export class EcdsaMPCv2Utils extends BaseEcdsaUtils { /** @inheritdoc */ async createKeychains(params: { @@ -58,6 +66,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { enterprise: string; originalPasscodeEncryptionCode?: string; retrofit?: DecryptedRetrofitPayload; + encryptionVersion?: 2; }): Promise { const { userSession, backupSession } = this.getUserAndBackupSession(2, 3, params.retrofit); const userGpgKey = await generateGPGKeyPair('secp256k1'); @@ -313,34 +322,42 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { assert.equal(bitgoCommonKeychain, userCommonKeychain, 'User and Bitgo Common keychains do not match'); assert.equal(bitgoCommonKeychain, backupCommonKeychain, 'Backup and Bitgo Common keychains do not match'); - const userKeychainPromise = this.addUserKeychain( - bitgoCommonKeychain, - userPrivateMaterial, - userReducedPrivateMaterial, - params.passphrase, - params.originalPasscodeEncryptionCode - ); - const backupKeychainPromise = this.addBackupKeychain( - bitgoCommonKeychain, - backupPrivateMaterial, - backupReducedPrivateMaterial, - params.passphrase, - params.originalPasscodeEncryptionCode - ); - const bitgoKeychainPromise = this.addBitgoKeychain(bitgoCommonKeychain); - - const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([ - userKeychainPromise, - backupKeychainPromise, - bitgoKeychainPromise, - ]); - // #endregion - - return { - userKeychain, - backupKeychain, - bitgoKeychain, - }; + const encryptionSession = + params.encryptionVersion === 2 ? await this.bitgo.createEncryptionSession(params.passphrase) : undefined; + try { + const userKeychainPromise = this.addUserKeychain( + bitgoCommonKeychain, + userPrivateMaterial, + userReducedPrivateMaterial, + params.passphrase, + params.originalPasscodeEncryptionCode, + encryptionSession + ); + const backupKeychainPromise = this.addBackupKeychain( + bitgoCommonKeychain, + backupPrivateMaterial, + backupReducedPrivateMaterial, + params.passphrase, + params.originalPasscodeEncryptionCode, + encryptionSession + ); + const bitgoKeychainPromise = this.addBitgoKeychain(bitgoCommonKeychain); + + const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([ + userKeychainPromise, + backupKeychainPromise, + bitgoKeychainPromise, + ]); + // #endregion + + return { + userKeychain, + backupKeychain, + bitgoKeychain, + }; + } finally { + encryptionSession?.destroy(); + } } // #region keychain utils @@ -350,7 +367,12 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { privateMaterial?: Buffer, reducedPrivateMaterial?: Buffer, passphrase?: string, - originalPasscodeEncryptionCode?: string + originalPasscodeEncryptionCode?: string, + encryptionSession?: { + encrypt(plaintext: string): Promise; + decrypt(ciphertext: string): Promise; + destroy(): void; + } ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -362,20 +384,27 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { assert(privateMaterial, `Private material is required for ${source} keychain`); assert(reducedPrivateMaterial, `Reduced private material is required for ${source} keychain`); assert(passphrase, `Passphrase is required for ${source} keychain`); - encryptedPrv = this.bitgo.encrypt({ - input: privateMaterial.toString('base64'), - password: passphrase, - }); - // Encrypts the CBOR-encoded ReducedKeyShare (which contains the party's private - // scalar s_i) with the wallet passphrase. The result is stored as reducedEncryptedPrv - // on the key card QR code and represents a second copy of private key material - // beyond the server-stored encryptedPrv. - reducedEncryptedPrv = this.bitgo.encrypt({ - // Buffer.toString('base64') can not be used here as it does not work on the browser. - // 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, - }); + if (encryptionSession) { + encryptedPrv = await encryptionSession.encrypt(privateMaterial.toString('base64')); + reducedEncryptedPrv = await encryptionSession.encrypt( + btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(reducedPrivateMaterial)))) + ); + } else { + encryptedPrv = this.bitgo.encrypt({ + input: privateMaterial.toString('base64'), + password: passphrase, + }); + // Encrypts the CBOR-encoded ReducedKeyShare (which contains the party's private + // scalar s_i) with the wallet passphrase. The result is stored as reducedEncryptedPrv + // on the key card QR code and represents a second copy of private key material + // beyond the server-stored encryptedPrv. + reducedEncryptedPrv = this.bitgo.encrypt({ + // Buffer.toString('base64') can not be used here as it does not work on the browser. + // 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, + }); + } break; case MPCv2PartiesEnum.BITGO: source = 'bitgo'; @@ -512,7 +541,12 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { privateMaterial: Buffer, reducedPrivateMaterial: Buffer, passphrase: string, - originalPasscodeEncryptionCode?: string + originalPasscodeEncryptionCode?: string, + encryptionSession?: { + encrypt(plaintext: string): Promise; + decrypt(ciphertext: string): Promise; + destroy(): void; + } ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -520,7 +554,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { privateMaterial, reducedPrivateMaterial, passphrase, - originalPasscodeEncryptionCode + originalPasscodeEncryptionCode, + encryptionSession ); } @@ -529,7 +564,12 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { privateMaterial: Buffer, reducedPrivateMaterial: Buffer, passphrase: string, - originalPasscodeEncryptionCode?: string + originalPasscodeEncryptionCode?: string, + encryptionSession?: { + encrypt(plaintext: string): Promise; + decrypt(ciphertext: string): Promise; + destroy(): void; + } ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.BACKUP, @@ -537,7 +577,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { privateMaterial, reducedPrivateMaterial, passphrase, - originalPasscodeEncryptionCode + originalPasscodeEncryptionCode, + encryptionSession ); } @@ -982,9 +1023,13 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { userGpgKey: pgp.SerializedKeyPair; }> { const bitgoGpgKey = await pgp.readKey({ armoredKey: bitgoPublicGpgKey }); - const userDecryptedKey = await pgp.readKey({ - armoredKey: this.bitgo.decrypt({ input: encryptedUserGpgPrvKey, password: walletPassphrase }), - }); + let decryptedGpgPrvKey: string; + if (isV2Envelope(encryptedUserGpgPrvKey)) { + decryptedGpgPrvKey = await this.bitgo.decryptAsync({ input: encryptedUserGpgPrvKey, password: walletPassphrase }); + } else { + decryptedGpgPrvKey = this.bitgo.decrypt({ input: encryptedUserGpgPrvKey, password: walletPassphrase }); + } + const userDecryptedKey = await pgp.readKey({ armoredKey: decryptedGpgPrvKey }); const userGpgKey: pgp.SerializedKeyPair = { privateKey: userDecryptedKey.armor(), publicKey: userDecryptedKey.toPublic().armor(), @@ -1114,13 +1159,18 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { return sendTxRequest(this.bitgo, txRequestResolved.walletId, txRequestResolved.txRequestId, requestType, reqId); } - async createOfflineRound1Share(params: { txRequest: TxRequest; prv: string; walletPassphrase: string }): Promise<{ + async createOfflineRound1Share(params: { + txRequest: TxRequest; + prv: string; + walletPassphrase: string; + encryptedPrv?: string; + }): Promise<{ signatureShareRound1: SignatureShareRecord; userGpgPubKey: string; encryptedRound1Session: string; encryptedUserGpgPrvKey: string; }> { - const { prv, walletPassphrase, txRequest } = params; + const { prv, walletPassphrase, txRequest, encryptedPrv } = params; const { hashBuffer, derivationPath } = this.getHashStringAndDerivationPath(txRequest); const adata = `${hashBuffer.toString('hex')}:${derivationPath}`; @@ -1130,10 +1180,22 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer); const userSignerBroadcastMsg1 = await userSigner.init(); const signatureShareRound1 = await getSignatureShareRoundOne(userSignerBroadcastMsg1, userGpgKey); - const session = userSigner.getSession(); - const encryptedRound1Session = this.bitgo.encrypt({ input: session, password: walletPassphrase, adata }); - + const sessionData = userSigner.getSession(); const userGpgPubKey = userGpgKey.publicKey; + + const useV2 = encryptedPrv !== undefined && isV2Envelope(encryptedPrv); + if (useV2) { + const session = await this.bitgo.createEncryptionSession(walletPassphrase); + try { + const encryptedRound1Session = await session.encrypt(sessionData); + const encryptedUserGpgPrvKey = await session.encrypt(userGpgKey.privateKey); + return { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey }; + } finally { + session.destroy(); + } + } + + const encryptedRound1Session = this.bitgo.encrypt({ input: sessionData, password: walletPassphrase, adata }); const encryptedUserGpgPrvKey = this.bitgo.encrypt({ input: userGpgKey.privateKey, password: walletPassphrase, @@ -1159,6 +1221,9 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { const { hashBuffer, derivationPath } = this.getHashStringAndDerivationPath(txRequest); const adata = `${hashBuffer.toString('hex')}:${derivationPath}`; + + const useV2 = isV2Envelope(encryptedRound1Session); + const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys( bitgoPublicGpgKey, encryptedUserGpgPrvKey, @@ -1179,9 +1244,14 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { bitgoGpgKey ); - const round1Session = this.bitgo.decrypt({ input: encryptedRound1Session, password: walletPassphrase }); + let round1Session: string; + if (useV2) { + round1Session = await this.bitgo.decryptAsync({ input: encryptedRound1Session, password: walletPassphrase }); + } else { + round1Session = this.bitgo.decrypt({ input: encryptedRound1Session, password: walletPassphrase }); + this.validateAdata(adata, encryptedRound1Session); + } - this.validateAdata(adata, encryptedRound1Session); const userKeyShare = Buffer.from(prv, 'base64'); const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer); await userSigner.setSession(round1Session); @@ -1201,8 +1271,19 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { userGpgKey, bitgoGpgKey ); - const session = userSigner.getSession(); - const encryptedRound2Session = this.bitgo.encrypt({ input: session, password: walletPassphrase, adata }); + const sessionData = userSigner.getSession(); + + if (useV2) { + const encSession = await this.bitgo.createEncryptionSession(walletPassphrase); + try { + const encryptedRound2Session = await encSession.encrypt(sessionData); + return { signatureShareRound2, encryptedRound2Session }; + } finally { + encSession.destroy(); + } + } + + const encryptedRound2Session = this.bitgo.encrypt({ input: sessionData, password: walletPassphrase, adata }); return { signatureShareRound2, @@ -1227,6 +1308,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { const { hashBuffer, derivationPath } = this.getHashStringAndDerivationPath(txRequest); const adata = `${hashBuffer.toString('hex')}:${derivationPath}`; + const useV2 = isV2Envelope(encryptedRound2Session); + const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys( bitgoPublicGpgKey, encryptedUserGpgPrvKey, @@ -1252,8 +1335,14 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { broadcastMessages: [], }); - const round2Session = this.bitgo.decrypt({ input: encryptedRound2Session, password: walletPassphrase }); - this.validateAdata(adata, encryptedRound2Session); + let round2Session: string; + if (useV2) { + round2Session = await this.bitgo.decryptAsync({ input: encryptedRound2Session, password: walletPassphrase }); + } else { + round2Session = this.bitgo.decrypt({ input: encryptedRound2Session, password: walletPassphrase }); + this.validateAdata(adata, encryptedRound2Session); + } + const userKeyShare = Buffer.from(prv, 'base64'); const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer); await userSigner.setSession(round2Session); 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 41168c28dd..fd3b96bd14 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -44,6 +44,17 @@ import { getBitgoMpcGpgPubKey } from '../../../tss/bitgoPubKeys'; import { EnvironmentName } from '../../../environments'; import { readKey } from 'openpgp'; +/** + * Checks whether a ciphertext string is a v2 encryption envelope. + */ +function isV2Envelope(ciphertext: string): boolean { + try { + return JSON.parse(ciphertext).v === 2; + } catch { + return false; + } +} + /** * Utility functions for TSS work flows. */ @@ -128,6 +139,7 @@ export class EddsaUtils extends baseTSSUtils { bitgoKeychain, passphrase, originalPasscodeEncryptionCode, + encryptionVersion, }: CreateEddsaKeychainParams): Promise { const MPC = await Eddsa.initialize(); const bitgoKeyShares = bitgoKeychain.keyShares; @@ -184,10 +196,19 @@ export class EddsaUtils extends baseTSSUtils { originalPasscodeEncryptionCode, }; if (passphrase !== undefined) { - userKeychainParams.encryptedPrv = this.bitgo.encrypt({ - input: JSON.stringify(userSigningMaterial), - password: passphrase, - }); + if (encryptionVersion === 2) { + const session = await this.bitgo.createEncryptionSession(passphrase); + try { + userKeychainParams.encryptedPrv = await session.encrypt(JSON.stringify(userSigningMaterial)); + } finally { + session.destroy(); + } + } else { + userKeychainParams.encryptedPrv = this.bitgo.encrypt({ + input: JSON.stringify(userSigningMaterial), + password: passphrase, + }); + } } return await this.baseCoin.keychains().add(userKeychainParams); @@ -211,6 +232,7 @@ export class EddsaUtils extends baseTSSUtils { backupKeyShare, bitgoKeychain, passphrase, + encryptionVersion, }: CreateEddsaKeychainParams): Promise { const MPC = await Eddsa.initialize(); const bitgoKeyShares = bitgoKeychain.keyShares; @@ -269,7 +291,16 @@ export class EddsaUtils extends baseTSSUtils { }; if (passphrase !== undefined) { - params.encryptedPrv = this.bitgo.encrypt({ input: prv, password: passphrase }); + if (encryptionVersion === 2) { + const session = await this.bitgo.createEncryptionSession(passphrase); + try { + params.encryptedPrv = await session.encrypt(prv); + } finally { + session.destroy(); + } + } else { + params.encryptedPrv = this.bitgo.encrypt({ input: prv, password: passphrase }); + } } return await this.baseCoin.keychains().createBackup(params); @@ -344,6 +375,7 @@ export class EddsaUtils extends baseTSSUtils { passphrase?: string; enterprise?: string; originalPasscodeEncryptionCode?: string; + encryptionVersion?: 2; }): Promise { const MPC = await Eddsa.initialize(); const m = 2; @@ -370,6 +402,7 @@ export class EddsaUtils extends baseTSSUtils { bitgoKeychain, passphrase: params.passphrase, originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, + encryptionVersion: params.encryptionVersion, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, @@ -378,6 +411,7 @@ export class EddsaUtils extends baseTSSUtils { backupKeyShare, bitgoKeychain, passphrase: params.passphrase, + encryptionVersion: params.encryptionVersion, }); const [userKeychain, backupKeychain] = await Promise.all([userKeychainPromise, backupKeychainPromise]); @@ -396,6 +430,7 @@ export class EddsaUtils extends baseTSSUtils { prv: string; walletPassphrase: string; bitgoGpgPubKey: string; + encryptedPrv?: string; }): Promise<{ userToBitgoCommitment: CommitmentShareRecord; encryptedSignerShare: EncryptedSignerShareRecord; @@ -441,7 +476,17 @@ export class EddsaUtils extends baseTSSUtils { const encryptedSignerShare = this.createUserToBitgoEncryptedSignerShare(userToBitgoEncryptedSignerShare); const stringifiedRShare = JSON.stringify(userSignShare); - const encryptedRShare = this.bitgo.encrypt({ input: stringifiedRShare, password: params.walletPassphrase }); + let encryptedRShare: string; + if (params.encryptedPrv && isV2Envelope(params.encryptedPrv)) { + const session = await this.bitgo.createEncryptionSession(params.walletPassphrase); + try { + encryptedRShare = await session.encrypt(stringifiedRShare); + } finally { + session.destroy(); + } + } else { + encryptedRShare = this.bitgo.encrypt({ input: stringifiedRShare, password: params.walletPassphrase }); + } const encryptedUserToBitgoRShare = this.createUserToBitgoEncryptedRShare(encryptedRShare); return { userToBitgoCommitment, encryptedSignerShare, encryptedUserToBitgoRShare }; @@ -454,10 +499,18 @@ export class EddsaUtils extends baseTSSUtils { }): Promise<{ rShare: SignShare }> { const { walletPassphrase, encryptedUserToBitgoRShare } = params; - const decryptedRShare = this.bitgo.decrypt({ - input: encryptedUserToBitgoRShare.share, - password: walletPassphrase, - }); + let decryptedRShare: string; + if (isV2Envelope(encryptedUserToBitgoRShare.share)) { + decryptedRShare = await this.bitgo.decryptAsync({ + input: encryptedUserToBitgoRShare.share, + password: walletPassphrase, + }); + } else { + decryptedRShare = this.bitgo.decrypt({ + input: encryptedUserToBitgoRShare.share, + password: walletPassphrase, + }); + } const rShare = JSON.parse(decryptedRShare); assert(rShare.xShare, 'Unable to find xShare in decryptedRShare'); assert(rShare.rShares, 'Unable to find rShares in decryptedRShare'); diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 1f65b5a977..fc6145ba0d 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -1070,7 +1070,7 @@ export interface IWallet { removeUser(params?: RemoveUserOptions): Promise; prebuildTransaction(params?: PrebuildTransactionOptions): Promise; signTransaction(params?: WalletSignTransactionOptions): Promise; - getUserPrv(params?: GetUserPrvOptions): string; + getUserPrv(params?: GetUserPrvOptions): Promise; prebuildAndSignTransaction(params?: PrebuildAndSignTransactionOptions): Promise; signAndSendTxRequest(params?: SignAndSendTxRequestOptions): Promise; accelerateTransaction(params?: AccelerateTransactionOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index 416f6682f8..55fe8f29be 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -45,6 +45,7 @@ export interface GenerateBaseMpcWalletOptions { export interface GenerateMpcWalletOptions extends GenerateBaseMpcWalletOptions { passphrase: string; originalPasscodeEncryptionCode?: string; + encryptionVersion?: 2; } export interface GenerateSMCMpcWalletOptions extends GenerateBaseMpcWalletOptions { bitgoKeyId: string; @@ -92,6 +93,7 @@ export interface GenerateWalletOptions { evmKeyRingReferenceWalletId?: string; /** Optional WebAuthn PRF-based encryption info. When provided, the user private key is additionally encrypted with the PRF-derived passphrase so the server can store a WebAuthn-protected copy. */ webauthnInfo?: GenerateWalletWebauthnInfo; + encryptionVersion?: 2; } export const GenerateLightningWalletOptionsCodec = t.intersection( @@ -105,20 +107,26 @@ export const GenerateLightningWalletOptionsCodec = t.intersection( }), t.partial({ lightningProvider: t.union([t.literal('amboss'), t.literal('voltage')]), + encryptionVersion: t.literal(2), }), ], 'GenerateLightningWalletOptions' ); export type GenerateLightningWalletOptions = t.TypeOf; -export const GenerateGoAccountWalletOptionsCodec = t.strict( - { - label: t.string, - passphrase: t.string, - enterprise: t.string, - passcodeEncryptionCode: t.string, - type: t.literal('trading'), - }, +export const GenerateGoAccountWalletOptionsCodec = t.intersection( + [ + t.strict({ + label: t.string, + passphrase: t.string, + enterprise: t.string, + passcodeEncryptionCode: t.string, + type: t.literal('trading'), + }), + t.partial({ + encryptionVersion: t.literal(2), + }), + ], 'GenerateGoAccountWalletOptions' ); diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 69a95271c2..e25a03267e 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -1669,7 +1669,7 @@ export class Wallet implements IWallet { if (!params.walletPassphrase) { throw new Error('wallet passphrase was not provided'); } - const userPrv = decryptKeychainPrivateKey(this.bitgo, userKeychain, params.walletPassphrase); + const userPrv = await decryptKeychainPrivateKey(this.bitgo, userKeychain, params.walletPassphrase); if (!userPrv) { throw new Error('error decrypting wallet private key'); } @@ -1849,7 +1849,7 @@ export class Wallet implements IWallet { throw new Error('Missing walletPassphrase argument'); } - const prv = decryptKeychainPrivateKey(this.bitgo, keychain, walletPassphrase); + const prv = await decryptKeychainPrivateKey(this.bitgo, keychain, walletPassphrase); if (!prv) { throw new IncorrectPasswordError('Password shared is incorrect for this wallet'); } @@ -2214,7 +2214,7 @@ export class Wallet implements IWallet { if (this.multisigType() === 'tss') { return this.signTransactionTss({ ...presign, - prv: this.getUserPrv(presign as GetUserPrvOptions), + prv: await this.getUserPrv(presign as GetUserPrvOptions), apiVersion, }); } @@ -2252,7 +2252,7 @@ export class Wallet implements IWallet { } return this.baseCoin.signTransaction({ ...signTransactionParams, - prv: this.getUserPrv(presign as GetUserPrvOptions), + prv: await this.getUserPrv(presign as GetUserPrvOptions), wallet: this, }); } @@ -2287,7 +2287,7 @@ export class Wallet implements IWallet { ...params, walletData: this._wallet, tssUtils: this.tssUtils, - prv: this.getUserPrv(userPrvOptions), + prv: await this.getUserPrv(userPrvOptions), keychain: keychains[0], backupKeychain: keychains.length > 1 ? keychains[1] : null, bitgoKeychain: keychains.length > 2 ? keychains[2] : null, @@ -2326,7 +2326,7 @@ export class Wallet implements IWallet { ...params, walletData: this._wallet, tssUtils: this.tssUtils, - prv: this.getUserPrv(userPrvOptions), + prv: await this.getUserPrv(userPrvOptions), keychain: keychains[0], backupKeychain: keychains.length > 1 ? keychains[1] : null, bitgoKeychain: keychains.length > 2 ? keychains[2] : null, @@ -2383,7 +2383,7 @@ export class Wallet implements IWallet { * @param [params.keychain / params.key] (object) or params.prv (string) * @param params.walletPassphrase (string) */ - getUserPrv(params: GetUserPrvOptions = {}): string { + async getUserPrv(params: GetUserPrvOptions = {}): Promise { const userKeychain = params.keychain || params.key; let userPrv = params.prv; if (userPrv && typeof userPrv !== 'string') { @@ -2420,7 +2420,7 @@ export class Wallet implements IWallet { if (!params.walletPassphrase) { throw new Error('walletPassphrase property missing'); } - userPrv = decryptKeychainPrivateKey(this.bitgo, userKeychain, params.walletPassphrase); + userPrv = await decryptKeychainPrivateKey(this.bitgo, userKeychain, params.walletPassphrase); if (!userPrv) { throw new Error('failed to decrypt user keychain'); } @@ -4393,7 +4393,7 @@ export class Wallet implements IWallet { // we ignore this check with if customSigningFunction is provided // which means that the user is handling the signing in external signing mode if (!customSigningFunction && keychains?.[0]?.encryptedPrv && walletPassphrase) { - if (!decryptKeychainPrivateKey(this.bitgo, keychains[0], walletPassphrase)) { + if (!(await decryptKeychainPrivateKey(this.bitgo, keychains[0], walletPassphrase))) { const error: Error & { code?: string } = new Error( `unable to decrypt keychain with the given wallet passphrase` ); diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 0c191b1670..3e0bf40520 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -169,7 +169,8 @@ export class Wallets implements IWallets { const reqId = new RequestTracer(); this.bitgo.setRequestTracer(reqId); - const { label, passphrase, enterprise, passcodeEncryptionCode, subType, lightningProvider } = params; + const { label, passphrase, enterprise, passcodeEncryptionCode, subType, lightningProvider, encryptionVersion } = + params; // TODO BTC-1899: only userAuth key is required for custodial lightning wallet. all 3 keys are required for self custodial lightning. // to avoid changing the platform for custodial flow, let us all 3 keys both wallet types. @@ -178,7 +179,11 @@ export class Wallets implements IWallets { const keychain = this.baseCoin.keychains().create(); const keychainParams: AddKeychainOptions = { pub: keychain.pub, - encryptedPrv: this.bitgo.encrypt({ password: passphrase, input: keychain.prv }), + encryptedPrv: await this.bitgo.encryptAsync({ + password: passphrase, + input: keychain.prv, + encryptionVersion, + }), originalPasscodeEncryptionCode: purpose === undefined ? passcodeEncryptionCode : undefined, coinSpecific: purpose === undefined ? undefined : { [this.baseCoin.getChain()]: { purpose } }, keyType: 'independent', @@ -228,13 +233,17 @@ export class Wallets implements IWallets { const reqId = new RequestTracer(); this.bitgo.setRequestTracer(reqId); - const { label, passphrase, enterprise, passcodeEncryptionCode } = params; + const { label, passphrase, enterprise, passcodeEncryptionCode, encryptionVersion } = params; const keychain = this.baseCoin.keychains().create(); const keychainParams: AddKeychainOptions = { pub: keychain.pub, - encryptedPrv: this.bitgo.encrypt({ password: passphrase, input: keychain.prv }), + encryptedPrv: await this.bitgo.encryptAsync({ + password: passphrase, + input: keychain.prv, + encryptionVersion, + }), originalPasscodeEncryptionCode: passcodeEncryptionCode, keyType: 'independent', source: 'user', @@ -318,9 +327,10 @@ export class Wallets implements IWallets { ); const walletData = await this.generateLightningWallet(options); - walletData.encryptedWalletPassphrase = this.bitgo.encrypt({ + walletData.encryptedWalletPassphrase = await this.bitgo.encryptAsync({ input: options.passphrase, password: options.passcodeEncryptionCode, + encryptionVersion: options.encryptionVersion, }); return walletData; } @@ -337,9 +347,10 @@ export class Wallets implements IWallets { ); const walletData = await this.generateGoAccountWallet(options); - walletData.encryptedWalletPassphrase = this.bitgo.encrypt({ + walletData.encryptedWalletPassphrase = await this.bitgo.encryptAsync({ input: options.passphrase, password: options.passcodeEncryptionCode, + encryptionVersion: options.encryptionVersion, }); return walletData; } @@ -437,11 +448,13 @@ export class Wallets implements IWallets { originalPasscodeEncryptionCode: params.passcodeEncryptionCode, enterprise, walletVersion: params.walletVersion, + encryptionVersion: params.encryptionVersion, }); if (params.passcodeEncryptionCode) { - walletData.encryptedWalletPassphrase = this.bitgo.encrypt({ + walletData.encryptedWalletPassphrase = await this.bitgo.encryptAsync({ input: passphrase, password: params.passcodeEncryptionCode, + encryptionVersion: params.encryptionVersion, }); } return walletData; @@ -574,7 +587,11 @@ export class Wallets implements IWallets { } // Create the user key. userKeychain = this.baseCoin.keychains().create(); - userKeychain.encryptedPrv = this.bitgo.encrypt({ password: passphrase, input: userKeychain.prv }); + userKeychain.encryptedPrv = await this.bitgo.encryptAsync({ + password: passphrase, + input: userKeychain.prv, + encryptionVersion: params.encryptionVersion, + }); userKeychainParams = { pub: userKeychain.pub, encryptedPrv: userKeychain.encryptedPrv, @@ -588,9 +605,10 @@ export class Wallets implements IWallets { { otpDeviceId: params.webauthnInfo.otpDeviceId, prfSalt: params.webauthnInfo.prfSalt, - encryptedPrv: this.bitgo.encrypt({ + encryptedPrv: await this.bitgo.encryptAsync({ password: params.webauthnInfo.passphrase, input: userKeychain.prv, + encryptionVersion: params.encryptionVersion, }), }, ]; @@ -611,6 +629,7 @@ export class Wallets implements IWallets { krsSpecific: params.krsSpecific, type: this.baseCoin.getChain(), passphrase: params.passphrase, + encryptionVersion: params.encryptionVersion, reqId, }); } @@ -628,7 +647,11 @@ export class Wallets implements IWallets { throw new Error('cannot generate backup keypair without passphrase'); } // No provided backup xpub or address, so default to creating one here - return this.baseCoin.keychains().createBackup({ reqId, passphrase: params.passphrase }); + return this.baseCoin.keychains().createBackup({ + reqId, + passphrase: params.passphrase, + encryptionVersion: params.encryptionVersion, + }); } }; const { userKeychain, backupKeychain, bitgoKeychain }: KeychainsTriplet = await promiseProps({ @@ -683,9 +706,10 @@ export class Wallets implements IWallets { } if (canEncrypt && params.passcodeEncryptionCode) { - result.encryptedWalletPassphrase = this.bitgo.encrypt({ + result.encryptedWalletPassphrase = await this.bitgo.encryptAsync({ input: passphrase, password: params.passcodeEncryptionCode, + encryptionVersion: params.encryptionVersion, }); } @@ -1500,6 +1524,7 @@ export class Wallets implements IWallets { enterprise, walletVersion, originalPasscodeEncryptionCode, + encryptionVersion, }: GenerateMpcWalletOptions): Promise { if (multisigType === 'tss' && this.baseCoin.getMPCAlgorithm() === 'ecdsa') { const tssSettings: TssSettings = await this.bitgo @@ -1519,6 +1544,7 @@ export class Wallets implements IWallets { passphrase, enterprise, originalPasscodeEncryptionCode, + encryptionVersion, }); // Create Wallet diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts new file mode 100644 index 0000000000..af77a23b28 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts @@ -0,0 +1,49 @@ +import assert from 'assert'; +import { isRight, isLeft } from 'fp-ts/Either'; + +import { + GenerateLightningWalletOptionsCodec, + GenerateGoAccountWalletOptionsCodec, +} from '../../../../src/bitgo/wallet/iWallets'; + +describe('wallet options codecs with encryptionVersion', () => { + const lightningBase = { + label: 'test', + passphrase: 'pass', + enterprise: 'ent', + passcodeEncryptionCode: 'code', + subType: 'lightningCustody' as const, + }; + + const goAccountBase = { + label: 'test', + passphrase: 'pass', + enterprise: 'ent', + passcodeEncryptionCode: 'code', + type: 'trading' as const, + }; + + it('GenerateLightningWalletOptionsCodec accepts encryptionVersion: 2', () => { + assert.ok(isRight(GenerateLightningWalletOptionsCodec.decode({ ...lightningBase, encryptionVersion: 2 }))); + }); + + it('GenerateLightningWalletOptionsCodec rejects encryptionVersion: 3', () => { + assert.ok(isLeft(GenerateLightningWalletOptionsCodec.decode({ ...lightningBase, encryptionVersion: 3 }))); + }); + + it('GenerateLightningWalletOptionsCodec works without encryptionVersion', () => { + assert.ok(isRight(GenerateLightningWalletOptionsCodec.decode(lightningBase))); + }); + + it('GenerateGoAccountWalletOptionsCodec accepts encryptionVersion: 2', () => { + assert.ok(isRight(GenerateGoAccountWalletOptionsCodec.decode({ ...goAccountBase, encryptionVersion: 2 }))); + }); + + it('GenerateGoAccountWalletOptionsCodec rejects encryptionVersion: 3', () => { + assert.ok(isLeft(GenerateGoAccountWalletOptionsCodec.decode({ ...goAccountBase, encryptionVersion: 3 }))); + }); + + it('GenerateGoAccountWalletOptionsCodec works without encryptionVersion', () => { + assert.ok(isRight(GenerateGoAccountWalletOptionsCodec.decode(goAccountBase))); + }); +}); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts index 5e74b7ff80..3167ee9e73 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts @@ -36,6 +36,11 @@ describe('Wallets - WebAuthn wallet creation', function () { encrypt: sinon .stub() .callsFake(({ password, input }: { password: string; input: string }) => `encrypted:${password}:${input}`), + encryptAsync: sinon + .stub() + .callsFake( + async ({ password, input }: { password: string; input: string }) => `encrypted:${password}:${input}` + ), setRequestTracer: sinon.stub(), }; @@ -132,7 +137,7 @@ describe('Wallets - WebAuthn wallet creation', function () { }, }); - const encryptCalls = mockBitGo.encrypt.getCalls(); + const encryptCalls = mockBitGo.encryptAsync.getCalls(); const passwordsUsed = encryptCalls.map((call: sinon.SinonSpyCall) => call.args[0].password); passwordsUsed.should.containEql(walletPassphrase); passwordsUsed.should.containEql(webauthnPassphrase);