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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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
*/
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
* failures.

View File

@ -807,7 +807,7 @@ describe("Cipher Service", () => {
// Set up expected results
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]);
@ -815,6 +815,11 @@ describe("Cipher Service", () => {
expectedFailedCipher.decryptionFailure = true;
const expectedFailedCipherViews = [expectedFailedCipher];
cipherEncryptionService.decryptManyLegacy.mockResolvedValue([
expectedSuccessCipherViews,
expectedFailedCipherViews,
]);
// Execute
const [successes, failures] = await (cipherService as any).decryptCiphers(
mockCiphers,
@ -822,10 +827,7 @@ describe("Cipher Service", () => {
);
// Verify the SDK was used for decryption
expect(cipherEncryptionService.decryptManyWithFailures).toHaveBeenCalledWith(
mockCiphers,
userId,
);
expect(cipherEncryptionService.decryptManyLegacy).toHaveBeenCalledWith(mockCiphers, userId);
expect(successes).toEqual(expectedSuccessCipherViews);
expect(failures).toEqual(expectedFailedCipherViews);

View File

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

View File

@ -496,9 +496,11 @@ describe("DefaultCipherEncryptionService", () => {
.mockReturnValueOnce(expectedViews[0])
.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(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(
this.sdkService.userClient$(userId).pipe(
map((sdk) => {
@ -178,7 +178,11 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
using ref = sdk.take();
return ciphers.map((cipher) => {
const successful: CipherView[] = [];
const failed: CipherView[] = [];
ciphers.forEach((cipher) => {
try {
const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher());
const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!;
@ -192,8 +196,6 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
.ciphers()
.decrypt_fido2_credentials(sdkCipherView);
// TODO (PM-21259): Remove manual keyValue decryption for FIDO2 credentials.
// This is a temporary workaround until we can use the SDK for FIDO2 authentication.
const decryptedKeyValue = ref.value
.vault()
.ciphers()
@ -208,8 +210,17 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
.filter((view): view is Fido2CredentialView => view !== undefined);
}
return clientCipherView;
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 [successful, failed] as [CipherView[], CipherView[]];
}),
catchError((error: unknown) => {
this.logService.error(`Failed to decrypt ciphers: ${error}`);