1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-27 10:03:23 +00:00

[PM-24978] Corrupt Attachment Keys (#17790)

* display translated content for attachments that cannot be downloaded

* consume decryption failure from the sdk for attachments

* add decryption errors from sdk

* only show fix attachment issues for when key is null and it does not have a decryption failure

* separate decryption failure state in view
This commit is contained in:
Nick Krantz
2026-02-11 10:31:38 -06:00
committed by GitHub
parent 3f3fc6f984
commit f20686cdf4
12 changed files with 140 additions and 62 deletions

View File

@@ -4,52 +4,76 @@
<ul aria-labelledby="attachments" class="tw-list-none tw-pl-0">
@for (attachment of attachments; track attachment.id) {
<li>
<bit-item>
<bit-item-content>
<span data-testid="file-name" [title]="attachment.fileName">{{
attachment.fileName
}}</span>
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
<i
*ngIf="attachment.key == null"
slot="default-trailing"
class="bwi bwi-exclamation-triangle bwi-sm tw-text-muted"
[appA11yTitle]="'fixEncryptionTooltip' | i18n"
></i>
</bit-item-content>
<ng-container slot="end">
<bit-item-action>
@if (attachment.key != null) {
<app-download-attachment
[admin]="admin() && organization()?.canEditAllCiphers"
[cipher]="cipher()"
[attachment]="attachment"
></app-download-attachment>
} @else {
<button
[bitAction]="fixOldAttachment(attachment)"
bitButton
buttonType="primary"
size="small"
type="button"
>
{{ "fixEncryption" | i18n }}
</button>
@if (!attachment.hasDecryptionError) {
<bit-item>
<bit-item-content>
<span data-testid="file-name" [title]="attachment.fileName">
{{ attachment.fileName }}
</span>
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
@if (attachment.key == null) {
<i
slot="default-trailing"
class="bwi bwi-exclamation-triangle bwi-sm tw-text-muted"
[appA11yTitle]="'fixEncryptionTooltip' | i18n"
></i>
}
</bit-item-action>
@if (cipher().edit) {
</bit-item-content>
<ng-container slot="end">
<bit-item-action>
<app-delete-attachment
[admin]="admin() && organization()?.canEditAllCiphers"
[cipherId]="cipher().id"
[attachment]="attachment"
(onDeletionSuccess)="removeAttachment(attachment)"
></app-delete-attachment>
@if (attachment.key != null) {
<app-download-attachment
[admin]="admin() && organization()?.canEditAllCiphers"
[cipher]="cipher()"
[attachment]="attachment"
></app-download-attachment>
} @else {
<button
[bitAction]="fixOldAttachment(attachment)"
bitButton
buttonType="primary"
size="small"
type="button"
>
{{ "fixEncryption" | i18n }}
</button>
}
</bit-item-action>
}
</ng-container>
</bit-item>
@if (cipher().edit) {
<bit-item-action>
<app-delete-attachment
[admin]="admin() && organization()?.canEditAllCiphers"
[cipherId]="cipher().id"
[attachment]="attachment"
(onDeletionSuccess)="removeAttachment(attachment)"
></app-delete-attachment>
</bit-item-action>
}
</ng-container>
</bit-item>
} @else {
<bit-item>
<bit-item-content>
<span data-testid="file-name" [title]="'errorCannotDecrypt' | i18n">
{{ "errorCannotDecrypt" | i18n }}
</span>
</bit-item-content>
<ng-container slot="end">
@if (cipher().edit) {
<bit-item-action>
<app-delete-attachment
[admin]="admin() && organization()?.canEditAllCiphers"
[cipherId]="cipher().id"
[attachment]="attachment"
(onDeletionSuccess)="removeAttachment(attachment)"
></app-delete-attachment>
</bit-item-action>
}
</ng-container>
</bit-item>
}
</li>
}
</ul>

View File

@@ -173,7 +173,7 @@ describe("CipherAttachmentsComponent", () => {
const fileSize = fixture.debugElement.query(By.css('[data-testid="file-size"]'));
expect(fileName.nativeElement.textContent.trim()).toEqual(attachment.fileName);
expect(fileSize.nativeElement.textContent).toEqual(attachment.sizeName);
expect(fileSize.nativeElement.textContent.trim()).toEqual(attachment.sizeName);
});
describe("bitSubmit", () => {

View File

@@ -5,8 +5,12 @@
<bit-item-group>
<bit-item *ngFor="let attachment of cipher.attachments">
<bit-item-content>
<span data-testid="file-name" [title]="attachment.fileName">{{ attachment.fileName }}</span>
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
<span data-testid="file-name" [title]="getAttachmentFileName(attachment)">
{{ getAttachmentFileName(attachment) }}
</span>
@if (!attachment.hasDecryptionError) {
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
}
</bit-item-content>
<ng-container slot="end">
<bit-item-action class="tw-pr-4 [@media(min-width:650px)]:tw-pr-6">

View File

@@ -8,9 +8,11 @@ import { NEVER, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
ItemModule,
@@ -59,6 +61,7 @@ export class AttachmentsV2ViewComponent {
private billingAccountProfileStateService: BillingAccountProfileStateService,
private stateProvider: StateProvider,
private accountService: AccountService,
private i18nService: I18nService,
) {
this.subscribeToHasPremiumCheck();
this.subscribeToOrgKey();
@@ -89,4 +92,12 @@ export class AttachmentsV2ViewComponent {
}
});
}
getAttachmentFileName(attachment: AttachmentView): string {
if (attachment.hasDecryptionError) {
return this.i18nService.t("errorCannotDecrypt");
}
return attachment.fileName ?? "";
}
}

View File

@@ -36,12 +36,11 @@ describe("DownloadAttachmentComponent", () => {
.mockResolvedValue({ url: "https://www.downloadattachement.com" });
const download = jest.fn();
const attachment = {
id: "222-3333-4444",
url: "https://www.attachment.com",
fileName: "attachment-filename",
size: "1234",
} as AttachmentView;
const attachment = new AttachmentView();
attachment.id = "222-3333-4444";
attachment.url = "https://www.attachment.com";
attachment.fileName = "attachment-filename";
attachment.size = "1234";
const cipherView = {
id: "5555-444-3333",
@@ -123,7 +122,12 @@ describe("DownloadAttachmentComponent", () => {
});
it("hides download button when the attachment has decryption failure", () => {
const decryptFailureAttachment = { ...attachment, fileName: DECRYPT_ERROR };
const decryptFailureAttachment = new AttachmentView();
decryptFailureAttachment.id = attachment.id;
decryptFailureAttachment.url = attachment.url;
decryptFailureAttachment.size = attachment.size;
decryptFailureAttachment.fileName = DECRYPT_ERROR;
fixture.componentRef.setInput("attachment", decryptFailureAttachment);
fixture.detectChanges();

View File

@@ -4,7 +4,6 @@ import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DECRYPT_ERROR } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -46,9 +45,7 @@ export class DownloadAttachmentComponent {
private cipherService: CipherService,
) {}
protected readonly isDecryptionFailure = computed(
() => this.attachment().fileName === DECRYPT_ERROR,
);
protected readonly isDecryptionFailure = computed(() => this.attachment().hasDecryptionError);
/** Download the attachment */
download = async () => {