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:
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user