mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[PM-16098] Improved cipher decryption error handling (#12468)
* [PM-16098] Add decryptionFailure flag to CipherView * [PM-16098] Add failedToDecryptCiphers$ observable to CipherService * [PM-16098] Introduce decryption-failure-dialog.component * [PM-16098] Disable cipher rows for the Web Vault * [PM-16098] Show decryption error dialog on vault load or when attempting to view/edit a corrupted cipher * [PM-16098] Browser - Show decryption error dialog on vault load or when attempting to view/edit a corrupted cipher * [PM-16098] Desktop - Show decryption error dialog on vault load or when attempting to view a corrupted cipher. Remove edit/clone context menu options and footer actions. * [PM-16098] Add CS link to decryption failure dialog * [PM-16098] Return cipherViews and move filtering of isDeleted to consumers * [PM-16098] Throw an error when retrieving cipher data for key rotation when a decryption failure is present * [PM-16098] Properly filter out deleted, corrupted ciphers when showing dialog within the Vault * [PM-16098] Show the decryption error dialog when attempting to view a cipher in trash and disable the restore option * [PM-16098] Exclude failed to decrypt ciphers from getAllDecrypted method and cipherViews$ observable * [PM-16098] Avoid re-sorting remainingCiphers$ as it was redundant * [PM-16098] Update tests * [PM-16098] Prevent opening view dialog in AC for corrupted ciphers * [PM-16098] Remove withLatestFrom operator that was causing race conditions when navigating away from the individual vault * [PM-16098] Ensure decryption error dialog is only shown once on Desktop when switching accounts
This commit is contained in:
@@ -7,7 +7,8 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { DialogModule, CalloutModule } from "@bitwarden/components";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
import { DecryptionFailureDialogComponent } from "@bitwarden/vault";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
@@ -61,6 +62,7 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
CalloutModule,
|
||||
DeleteAccountComponent,
|
||||
UserVerificationComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
declarations: [
|
||||
AccessibilityCookieComponent,
|
||||
|
||||
@@ -249,6 +249,20 @@
|
||||
"error": {
|
||||
"message": "Error"
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Decryption error"
|
||||
},
|
||||
"couldNotDecryptVaultItemsBelow": {
|
||||
"message": "Bitwarden could not decrypt the vault item(s) listed below."
|
||||
},
|
||||
"contactCSToAvoidDataLossPart1": {
|
||||
"message": "Contact customer success",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"contactCSToAvoidDataLossPart2": {
|
||||
"message": "to avoid additional data loss.",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"january": {
|
||||
"message": "January"
|
||||
},
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil, switchMap } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
import { combineLatest, firstValueFrom, Subject, takeUntil, switchMap } from "rxjs";
|
||||
import { filter, first, map, take } from "rxjs/operators";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -28,13 +28,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
import { GeneratorComponent } from "../../../app/tools/generator.component";
|
||||
@@ -113,6 +115,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -238,6 +241,25 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
notificationId: authRequest.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Store a reference to the current active account during page init
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
// Combine with the activeAccount$ to ensure we only show the dialog for the current account from ngOnInit.
|
||||
// The account switching process updates the cipherService before Vault is destroyed and would cause duplicate emissions
|
||||
combineLatest([this.accountService.activeAccount$, this.cipherService.failedToDecryptCiphers$])
|
||||
.pipe(
|
||||
filter(([account]) => account.id === activeAccount.id),
|
||||
map(([_, ciphers]) => ciphers.filter((c) => !c.isDeleted)),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe((ciphers) => {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: ciphers.map((c) => c.id as CipherId),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -302,6 +324,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (cipher.decryptionFailure) {
|
||||
invokeMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cipher.isDeleted) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("edit"),
|
||||
|
||||
@@ -638,33 +638,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer" *ngIf="cipher">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="!cipher?.organizationId && !cipher.isDeleted"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ng-container *ngIf="!cipher.decryptionFailure">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="!cipher?.organizationId && !cipher.isDeleted"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div class="right" *ngIf="canDeleteCipher$ | async">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
@@ -32,7 +33,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "ViewComponent";
|
||||
|
||||
@@ -98,6 +99,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
}
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
@@ -117,6 +119,13 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
|
||||
async ngOnChanges() {
|
||||
await super.load();
|
||||
|
||||
if (this.cipher.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [this.cipherId as CipherId],
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
viewHistory() {
|
||||
|
||||
Reference in New Issue
Block a user