diff --git a/modules/passkey-crypto/src/attachPasskeyToWallet.ts b/modules/passkey-crypto/src/attachPasskeyToWallet.ts index 2771abccd1..f1c65ab4c4 100644 --- a/modules/passkey-crypto/src/attachPasskeyToWallet.ts +++ b/modules/passkey-crypto/src/attachPasskeyToWallet.ts @@ -67,7 +67,14 @@ export async function attachPasskeyToWallet(params: { } const prfPassword = derivePassword(authResult.prfResult); - const encryptedPrv = await bitgo.encrypt({ password: prfPassword, input: privateKey, encryptionVersion }); + const encryptedPrv = await bitgo.encrypt({ + password: prfPassword, + input: privateKey, + encryptionVersion, + // Bind to this enterprise via AES-GCM AAD — moving this blob to another enterprise + // or tampering with the stored adata field invalidates the GCM tag. + adata: enterpriseId, + }); 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 561746a142..a86ac7e784 100644 --- a/modules/passkey-crypto/test/unit/attachPasskeyToWallet.test.ts +++ b/modules/passkey-crypto/test/unit/attachPasskeyToWallet.test.ts @@ -153,9 +153,9 @@ describe('attachPasskeyToWallet', function () { assert.match(putBody.webauthnInfo.prfSalt, /^[A-Za-z0-9\-_]+$/); assert.strictEqual(typeof putBody.webauthnInfo.encryptedPrv, 'string'); - // encrypt must be called with encryptionVersion 2 + // encrypt must be called with encryptionVersion 2 and enterpriseId as AAD sinon.assert.calledOnce(mockBitGo.encrypt); - sinon.assert.calledWithMatch(mockBitGo.encrypt, { encryptionVersion: 2 }); + sinon.assert.calledWithMatch(mockBitGo.encrypt, { encryptionVersion: 2, adata: enterpriseId }); assert.strictEqual(result.id, keychainId); }); @@ -165,11 +165,12 @@ describe('attachPasskeyToWallet', function () { await callAttach(); - // The PRF-derived password and the decrypted xprv must be passed to encrypt + // The PRF-derived password, decrypted xprv, and enterpriseId AAD must be passed to encrypt sinon.assert.calledWithMatch(mockBitGo.encrypt, { password: expectedPrfPassword, input: decryptedPrv, encryptionVersion: 2, + adata: enterpriseId, }); // The v2 blob returned by encrypt is what gets stored on the server diff --git a/modules/sdk-api/test/unit/encrypt.ts b/modules/sdk-api/test/unit/encrypt.ts index 53d7dffb71..52eacaf608 100644 --- a/modules/sdk-api/test/unit/encrypt.ts +++ b/modules/sdk-api/test/unit/encrypt.ts @@ -191,6 +191,30 @@ describe('encryption methods tests', () => { await assert.rejects(() => decryptV2(password, JSON.stringify(envelope)), /operation-specific reason|incorrect/i); }); + it('adata: undefined is equivalent to omitting adata', async () => { + const fixedOpts = { + memorySize: 1024, + iterations: 1, + parallelism: 1, + salt: new Uint8Array(16).fill(0xaa), + iv: new Uint8Array(12).fill(0xbb), + }; + const withUndefined = await encryptV2(password, plaintext, { ...fixedOpts, adata: undefined }); + const withOmitted = await encryptV2(password, plaintext, fixedOpts); + assert.strictEqual(withUndefined, withOmitted); + assert.strictEqual(await decryptV2(password, withUndefined), plaintext); + }); + + it('ciphertext bound to enterprise-A cannot be re-attributed to enterprise-B', async () => { + const prv = 'xprv-private-key-bytes'; + const prfKey = 'prf-derived-key'; + const ct = await encryptV2(prfKey, prv, { adata: 'enterprise-A' }); + // Attacker moves blob to enterprise-B by altering the stored adata field + const envelope = JSON.parse(ct); + envelope.adata = 'enterprise-B'; + await assert.rejects(() => decryptV2(prfKey, JSON.stringify(envelope))); + }); + it('v1 and v2 are independent (v1 data does not decrypt with v2)', async () => { const v1ct = await encrypt(password, plaintext, { encryptionVersion: 1 }); await assert.rejects(() => decryptV2(password, v1ct), /invalid envelope/); diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 76d3962cf6..9a27d73e7b 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -617,6 +617,7 @@ export class Wallets implements IWallets { password: params.webauthnInfo.passphrase, input: userKeychain.prv, encryptionVersion: params.encryptionVersion, + adata: params.enterprise, }), }; } @@ -1352,6 +1353,7 @@ export class Wallets implements IWallets { password: webauthnInfo.passphrase, input: decryptedSharedWalletPrv, encryptionVersion: params.encryptionVersion, + adata: walletShare.enterprise, }), }; }