1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

Prevented double decryption (#17768)

This commit is contained in:
SmithThe4th
2025-12-02 16:13:34 -05:00
committed by GitHub
parent dc953b3945
commit 6f9b25e98e
5 changed files with 63 additions and 41 deletions

View File

@@ -67,7 +67,10 @@ export abstract class CipherEncryptionService {
* *
* @returns A promise that resolves to an array of decrypted cipher views * @returns A promise that resolves to an array of decrypted cipher views
*/ */
abstract decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise<CipherView[]>; abstract decryptManyLegacy(
ciphers: Cipher[],
userId: UserId,
): Promise<[CipherView[], CipherView[]]>;
/** /**
* Decrypts many ciphers using the SDK for the given userId, and returns a list of * Decrypts many ciphers using the SDK for the given userId, and returns a list of
* failures. * failures.

View File

@@ -807,7 +807,7 @@ describe("Cipher Service", () => {
// Set up expected results // Set up expected results
const expectedSuccessCipherViews = [ const expectedSuccessCipherViews = [
{ id: mockCiphers[0].id, name: "Success 1" } as unknown as CipherListView, { id: mockCiphers[0].id, name: "Success 1", decryptionFailure: false } as CipherView,
]; ];
const expectedFailedCipher = new CipherView(mockCiphers[1]); const expectedFailedCipher = new CipherView(mockCiphers[1]);
@@ -815,6 +815,11 @@ describe("Cipher Service", () => {
expectedFailedCipher.decryptionFailure = true; expectedFailedCipher.decryptionFailure = true;
const expectedFailedCipherViews = [expectedFailedCipher]; const expectedFailedCipherViews = [expectedFailedCipher];
cipherEncryptionService.decryptManyLegacy.mockResolvedValue([
expectedSuccessCipherViews,
expectedFailedCipherViews,
]);
// Execute // Execute
const [successes, failures] = await (cipherService as any).decryptCiphers( const [successes, failures] = await (cipherService as any).decryptCiphers(
mockCiphers, mockCiphers,
@@ -822,10 +827,7 @@ describe("Cipher Service", () => {
); );
// Verify the SDK was used for decryption // Verify the SDK was used for decryption
expect(cipherEncryptionService.decryptManyWithFailures).toHaveBeenCalledWith( expect(cipherEncryptionService.decryptManyLegacy).toHaveBeenCalledWith(mockCiphers, userId);
mockCiphers,
userId,
);
expect(successes).toEqual(expectedSuccessCipherViews); expect(successes).toEqual(expectedSuccessCipherViews);
expect(failures).toEqual(expectedFailedCipherViews); expect(failures).toEqual(expectedFailedCipherViews);

View File

@@ -2143,15 +2143,19 @@ export class CipherService implements CipherServiceAbstraction {
userId: UserId, userId: UserId,
fullDecryption: boolean = true, fullDecryption: boolean = true,
): Promise<[CipherViewLike[], CipherView[]]> { ): Promise<[CipherViewLike[], CipherView[]]> {
if (fullDecryption) {
const [decryptedViews, failedViews] = await this.cipherEncryptionService.decryptManyLegacy(
ciphers,
userId,
);
return [decryptedViews.sort(this.getLocaleSortingFunction()), failedViews];
}
const [decrypted, failures] = await this.cipherEncryptionService.decryptManyWithFailures( const [decrypted, failures] = await this.cipherEncryptionService.decryptManyWithFailures(
ciphers, ciphers,
userId, userId,
); );
const decryptedViews = fullDecryption
? await Promise.all(decrypted.map((c) => this.getFullCipherView(c)))
: decrypted;
const failedViews = failures.map((c) => { const failedViews = failures.map((c) => {
const cipher_view = new CipherView(c); const cipher_view = new CipherView(c);
cipher_view.name = "[error: cannot decrypt]"; cipher_view.name = "[error: cannot decrypt]";
@@ -2159,7 +2163,7 @@ export class CipherService implements CipherServiceAbstraction {
return cipher_view; return cipher_view;
}); });
return [decryptedViews.sort(this.getLocaleSortingFunction()), failedViews]; return [decrypted.sort(this.getLocaleSortingFunction()), failedViews];
} }
/** Fetches the full `CipherView` when a `CipherListView` is passed. */ /** Fetches the full `CipherView` when a `CipherListView` is passed. */

View File

@@ -496,9 +496,11 @@ describe("DefaultCipherEncryptionService", () => {
.mockReturnValueOnce(expectedViews[0]) .mockReturnValueOnce(expectedViews[0])
.mockReturnValueOnce(expectedViews[1]); .mockReturnValueOnce(expectedViews[1]);
const result = await cipherEncryptionService.decryptManyLegacy(ciphers, userId); const [successfulDecryptions, failedDecryptions] =
await cipherEncryptionService.decryptManyLegacy(ciphers, userId);
expect(result).toEqual(expectedViews); expect(successfulDecryptions).toEqual(expectedViews);
expect(failedDecryptions).toEqual([]);
expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledTimes(2); expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledTimes(2);
expect(CipherView.fromSdkCipherView).toHaveBeenCalledTimes(2); expect(CipherView.fromSdkCipherView).toHaveBeenCalledTimes(2);
}); });

View File

@@ -168,7 +168,7 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
); );
} }
decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise<CipherView[]> { decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise<[CipherView[], CipherView[]]> {
return firstValueFrom( return firstValueFrom(
this.sdkService.userClient$(userId).pipe( this.sdkService.userClient$(userId).pipe(
map((sdk) => { map((sdk) => {
@@ -178,38 +178,49 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
using ref = sdk.take(); using ref = sdk.take();
return ciphers.map((cipher) => { const successful: CipherView[] = [];
const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher()); const failed: CipherView[] = [];
const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!;
// Handle FIDO2 credentials if present ciphers.forEach((cipher) => {
if ( try {
clientCipherView.type === CipherType.Login && const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher());
sdkCipherView.login?.fido2Credentials?.length const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!;
) {
const fido2CredentialViews = ref.value
.vault()
.ciphers()
.decrypt_fido2_credentials(sdkCipherView);
// TODO (PM-21259): Remove manual keyValue decryption for FIDO2 credentials. // Handle FIDO2 credentials if present
// This is a temporary workaround until we can use the SDK for FIDO2 authentication. if (
const decryptedKeyValue = ref.value clientCipherView.type === CipherType.Login &&
.vault() sdkCipherView.login?.fido2Credentials?.length
.ciphers() ) {
.decrypt_fido2_private_key(sdkCipherView); const fido2CredentialViews = ref.value
.vault()
.ciphers()
.decrypt_fido2_credentials(sdkCipherView);
clientCipherView.login.fido2Credentials = fido2CredentialViews const decryptedKeyValue = ref.value
.map((f) => { .vault()
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; .ciphers()
view.keyValue = decryptedKeyValue; .decrypt_fido2_private_key(sdkCipherView);
return view;
}) clientCipherView.login.fido2Credentials = fido2CredentialViews
.filter((view): view is Fido2CredentialView => view !== undefined); .map((f) => {
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
view.keyValue = decryptedKeyValue;
return view;
})
.filter((view): view is Fido2CredentialView => view !== undefined);
}
successful.push(clientCipherView);
} catch (error) {
this.logService.error(`Failed to decrypt cipher ${cipher.id}: ${error}`);
const failedView = new CipherView(cipher);
failedView.name = "[error: cannot decrypt]";
failedView.decryptionFailure = true;
failed.push(failedView);
} }
return clientCipherView;
}); });
return [successful, failed] as [CipherView[], CipherView[]];
}), }),
catchError((error: unknown) => { catchError((error: unknown) => {
this.logService.error(`Failed to decrypt ciphers: ${error}`); this.logService.error(`Failed to decrypt ciphers: ${error}`);