diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 940bb22c95c..5768d336115 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5966,6 +5966,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6138,4 +6141,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 550bbcad81e..85742db94ab 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8d168317471..b86d88b42ad 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12582,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index d9dfa128028..b9bcaad8cea 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -47,6 +47,12 @@ export class Attachment extends Domain { if (this.key != null) { view.key = await this.decryptAttachmentKey(decryptionKey); view.encryptedKey = this.key; // Keep the encrypted key for the view + + // When the attachment key couldn't be decrypted, mark a decryption error + // The file won't be able to be downloaded in these cases + if (!view.key) { + view.hasDecryptionError = true; + } } return view; diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index ef4a9ed8b27..6eaa943fba0 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -2,7 +2,7 @@ import { Jsonify } from "type-fest"; import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; -import { EncString } from "../../../key-management/crypto/models/enc-string"; +import { DECRYPT_ERROR, EncString } from "../../../key-management/crypto/models/enc-string"; import { View } from "../../../models/view/view"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { Attachment } from "../domain/attachment"; @@ -18,6 +18,7 @@ export class AttachmentView implements View { * The SDK returns an encrypted key for the attachment. */ encryptedKey: EncString | undefined; + private _hasDecryptionError?: boolean; constructor(a?: Attachment) { if (!a) { @@ -41,6 +42,14 @@ export class AttachmentView implements View { return 0; } + get hasDecryptionError(): boolean { + return this._hasDecryptionError || this.fileName === DECRYPT_ERROR; + } + + set hasDecryptionError(value: boolean) { + this._hasDecryptionError = value; + } + static fromJSON(obj: Partial>): AttachmentView { const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key); @@ -76,7 +85,10 @@ export class AttachmentView implements View { /** * Converts the SDK AttachmentView to a AttachmentView. */ - static fromSdkAttachmentView(obj: SdkAttachmentView): AttachmentView | undefined { + static fromSdkAttachmentView( + obj: SdkAttachmentView, + failure = false, + ): AttachmentView | undefined { if (!obj) { return undefined; } @@ -90,6 +102,7 @@ export class AttachmentView implements View { // TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete view.key = obj.decryptedKey ? SymmetricCryptoKey.fromString(obj.decryptedKey) : undefined; view.encryptedKey = obj.key ? new EncString(obj.key) : undefined; + view._hasDecryptionError = failure; return view; } diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 0909d0bda80..1e0cce8d72e 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -280,6 +280,17 @@ export class CipherView implements View, InitializerMetadata { return undefined; } + const attachments = obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)!) ?? []; + + if (obj.attachmentDecryptionFailures?.length) { + obj.attachmentDecryptionFailures.forEach((attachment) => { + const attachmentView = AttachmentView.fromSdkAttachmentView(attachment, true); + if (attachmentView) { + attachments.push(attachmentView); + } + }); + } + const cipherView = new CipherView(); cipherView.id = uuidAsString(obj.id); cipherView.organizationId = uuidAsString(obj.organizationId); @@ -295,8 +306,7 @@ export class CipherView implements View, InitializerMetadata { cipherView.edit = obj.edit; cipherView.viewPassword = obj.viewPassword; cipherView.localData = fromSdkLocalData(obj.localData); - cipherView.attachments = - obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)!) ?? []; + cipherView.attachments = attachments; cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)!) ?? []; cipherView.passwordHistory = obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)!) ?? []; diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html index 6aaaf033e0d..0e214abd72d 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html @@ -4,52 +4,76 @@ diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index 002ad019653..88ee1f9b599 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -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", () => { diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html index 0a46b83b086..c8110b9e863 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html @@ -5,8 +5,12 @@ - {{ attachment.fileName }} - {{ attachment.sizeName }} + + {{ getAttachmentFileName(attachment) }} + + @if (!attachment.hasDecryptionError) { + {{ attachment.sizeName }} + } diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts index 4e324d8002e..3826d3a3ad0 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts @@ -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 ?? ""; + } } diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts index a46ce28fca8..05d5e9bc276 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts @@ -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(); diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index 31ed609637c..bdca510c5aa 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -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 () => {