From be5a7a12376d464c3b91bcfc71d13a9aacb72893 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 5 Aug 2026 16:45:54 -0700 Subject: [PATCH 1/4] PM-41073: Consume breaking changes from SDK making all children of CipherView decrypted --- .../v2-key-rotation-migration.spec.ts | 7 +-- .../vault/models/view/attachment.view.spec.ts | 9 +-- .../src/vault/models/view/attachment.view.ts | 29 ++------- .../src/vault/models/view/cipher.view.spec.ts | 31 +++------- .../src/vault/models/view/cipher.view.ts | 40 +++--------- .../vault/services/cipher-sdk.service.spec.ts | 31 ++-------- .../src/vault/services/cipher-sdk.service.ts | 13 ++-- .../default-cipher-encryption.service.spec.ts | 61 ++----------------- .../default-cipher-encryption.service.ts | 52 ---------------- 9 files changed, 43 insertions(+), 230 deletions(-) diff --git a/libs/common/src/key-management/encrypted-migrator/migrations/v2-key-rotation-migration.spec.ts b/libs/common/src/key-management/encrypted-migrator/migrations/v2-key-rotation-migration.spec.ts index 8b0dd1645522..09bdf43effbf 100644 --- a/libs/common/src/key-management/encrypted-migrator/migrations/v2-key-rotation-migration.spec.ts +++ b/libs/common/src/key-management/encrypted-migrator/migrations/v2-key-rotation-migration.spec.ts @@ -16,7 +16,6 @@ import { UserKey } from "../../../types/key"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { AttachmentView } from "../../../vault/models/view/attachment.view"; import { CipherView } from "../../../vault/models/view/cipher.view"; -import { EncString } from "../../crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction"; import { V2KeyRotationMigration } from "./v2-key-rotation-migration"; @@ -44,11 +43,9 @@ describe("V2KeyRotationMigration", () => { return cipher; }; - const makeAttachment = (hasEncryptedKey: boolean): AttachmentView => { + const makeAttachment = (hasKey: boolean): AttachmentView => { const a = new AttachmentView(); - a.encryptedKey = hasEncryptedKey - ? new EncString("2.abc|def|ghi") - : (undefined as unknown as EncString); + a.key = hasKey ? mock() : undefined; return a; }; diff --git a/libs/common/src/vault/models/view/attachment.view.spec.ts b/libs/common/src/vault/models/view/attachment.view.spec.ts index 31815ac9cac3..f34e6a64c66d 100644 --- a/libs/common/src/vault/models/view/attachment.view.spec.ts +++ b/libs/common/src/vault/models/view/attachment.view.spec.ts @@ -1,7 +1,6 @@ import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; import { mockFromJson } from "../../../../spec"; -import { EncString } from "../../../key-management/crypto/models/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { AttachmentView } from "./attachment.view"; @@ -34,8 +33,7 @@ describe("AttachmentView", () => { size: "size", sizeName: "sizeName", fileName: "fileName", - key: "encKeyB64_fromString", - decryptedKey: "decryptedKey_B64", + key: "decryptedKey_B64", } as SdkAttachmentView; const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView); @@ -47,7 +45,6 @@ describe("AttachmentView", () => { sizeName: "sizeName", fileName: "fileName", key: "mockKey", - encryptedKey: new EncString(sdkAttachmentView.key as string), }); expect(SymmetricCryptoKey.fromString).toHaveBeenCalledWith("decryptedKey_B64"); @@ -66,7 +63,6 @@ describe("AttachmentView", () => { attachmentView.size = "size"; attachmentView.sizeName = "sizeName"; attachmentView.fileName = "fileName"; - attachmentView.encryptedKey = new EncString("encKeyB64"); attachmentView.key = mockKey; const result = attachmentView.toSdkAttachmentView(); @@ -77,8 +73,7 @@ describe("AttachmentView", () => { size: "size", sizeName: "sizeName", fileName: "fileName", - key: "encKeyB64", - decryptedKey: "keyB64", + key: "keyB64", }); }); }); diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index 724e9304c6c7..423b117a658d 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 { DECRYPT_ERROR, EncString } from "../../../key-management/crypto/models/enc-string"; +import { DECRYPT_ERROR } 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"; @@ -14,10 +14,6 @@ export class AttachmentView implements View { sizeName?: string; fileName?: string; key?: SymmetricCryptoKey; - /** - * The SDK returns an encrypted key for the attachment. - */ - encryptedKey: EncString | undefined; private _hasDecryptionError?: boolean; constructor(a?: Attachment) { @@ -52,18 +48,7 @@ export class AttachmentView implements View { static fromJSON(obj: Partial>): AttachmentView { const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key); - - let encryptedKey: EncString | undefined; - if (obj.encryptedKey != null) { - if (typeof obj.encryptedKey === "string") { - // If the key is a string, we need to parse it as EncString - encryptedKey = EncString.fromJSON(obj.encryptedKey); - } else if ((obj.encryptedKey as any) instanceof EncString) { - // If the key is already an EncString instance, we can use it directly - encryptedKey = obj.encryptedKey; - } - } - return Object.assign(new AttachmentView(), obj, { key: key, encryptedKey: encryptedKey }); + return Object.assign(new AttachmentView(), obj, { key: key }); } /** @@ -76,9 +61,7 @@ export class AttachmentView implements View { size: this.size, sizeName: this.sizeName, fileName: this.fileName, - key: this.encryptedKey?.toSdk(), - // TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete - decryptedKey: this.key ? this.key.toBase64() : undefined, + key: this.key?.toBase64() ?? undefined, }; } @@ -99,9 +82,7 @@ export class AttachmentView implements View { view.size = obj.size; view.sizeName = obj.sizeName; view.fileName = obj.fileName; - // 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.key = obj.key ? SymmetricCryptoKey.fromString(obj.key) : undefined; view._hasDecryptionError = failure; return view; @@ -112,6 +93,6 @@ export class AttachmentView implements View { * In this case, the attachment is encrypted with the user's user-key */ isLegacyAttachment(): boolean { - return this.key == null && this.encryptedKey == null; + return this.key == null; } } diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index bcebdf1136be..2a7f2a02ebc9 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { CiphersClient, CipherView as SdkCipherView, @@ -13,7 +11,6 @@ import { } from "@bitwarden/sdk-internal"; import { mockFromJson, mockFromSdk } from "../../../../spec"; -import { EncString } from "../../../key-management/crypto/models/enc-string"; import { asUuid } from "../../../platform/abstractions/sdk/sdk.service"; import { CipherRepromptType } from "../../enums"; import { CipherType } from "../../enums/cipher-type"; @@ -94,23 +91,13 @@ describe("CipherView", () => { expect(actual).toMatchObject(expected); }); - it("handle both string and object inputs for the cipher key", () => { - const cipherKeyString = "cipherKeyString"; - const cipherKeyObject = new EncString("cipherKeyObject"); + it("passes the cipher key through as a plain string", () => { + const cipherKeyB64 = "c29tZS1iYXNlNjQta2V5"; - // Test with string input - let actual = CipherView.fromJSON({ - key: cipherKeyString, + const actual = CipherView.fromJSON({ + key: cipherKeyB64, }); - expect(actual.key).toBeInstanceOf(EncString); - expect(actual.key?.toJSON()).toBe(cipherKeyString); - - // Test with object input (which can happen when cipher view is stored in an InMemory state provider) - actual = CipherView.fromJSON({ - key: cipherKeyObject, - } as Jsonify); - expect(actual.key).toBeInstanceOf(EncString); - expect(actual.key?.toJSON()).toBe(cipherKeyObject.toJSON()); + expect(actual.key).toBe(cipherKeyB64); }); it("fromJSON should always restore top-level CipherView properties", () => { @@ -143,7 +130,7 @@ describe("CipherView", () => { original.deletedDate = new Date("2022-01-03"); original.archivedDate = new Date("2022-01-04"); original.reprompt = CipherRepromptType.Password; - original.key = new EncString("test-key"); + original.key = "dGVzdC1rZXktYjY0"; original.decryptionFailure = true; // Serialize and deserialize @@ -304,7 +291,7 @@ describe("CipherView", () => { cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c"; cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f"; cipherView.collectionIds = ["b0473506-3c3c-4260-a734-dfaaf833ab6f"]; - cipherView.key = new EncString("some-key"); + cipherView.key = "some-key-b64"; cipherView.name = "name"; cipherView.notes = "notes"; cipherView.type = CipherType.Login; @@ -334,7 +321,7 @@ describe("CipherView", () => { organizationId: asUuid("000f2a6e-da5e-4726-87ed-1c5c77322c3c"), folderId: asUuid("41b22db4-8e2a-4ed2-b568-f1186c72922f"), collectionIds: [asUuid("b0473506-3c3c-4260-a734-dfaaf833ab6f")], - key: "some-key" as any, + key: "some-key-b64" as any, name: "name", notes: "notes", type: SdkCipherType.Login, @@ -516,7 +503,7 @@ describe("CipherView", () => { cipherView.reprompt = CipherRepromptType.Password; cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z"); cipherView.archivedDate = new Date("2022-01-03T12:00:00.000Z"); - cipherView.key = new EncString("cipher-key"); + cipherView.key = "cipher-key-b64"; const mockField = new RealFieldView(); mockField.name = "testField"; diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index e914e5b0d3e2..51adc70d92d1 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -7,7 +7,6 @@ import { CipherView as SdkCipherView, } from "@bitwarden/sdk-internal"; -import { EncString } from "../../../key-management/crypto/models/enc-string"; import { View } from "../../../models/view/view"; import { asUuid, uuidAsString } from "../../../platform/abstractions/sdk/sdk.service"; import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface"; @@ -65,9 +64,7 @@ export class CipherView implements View, InitializerMetadata { deletedDate?: Date; archivedDate?: Date; reprompt: CipherRepromptType = CipherRepromptType.None; - // We need a copy of the encrypted key so we can pass it to - // the SdkCipherView during encryption - key?: EncString; + key?: string; /** * Flag to indicate if the cipher decryption failed. @@ -97,7 +94,6 @@ export class CipherView implements View, InitializerMetadata { this.archivedDate = c.archivedDate; // Old locally stored ciphers might have reprompt == null. If so set it to None. this.reprompt = c.reprompt ?? CipherRepromptType.None; - this.key = c.key; } private get item(): ItemView | undefined { @@ -252,17 +248,7 @@ export class CipherView implements View, InitializerMetadata { view.passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)) ?? []; - if (obj.key != null) { - let key: EncString | undefined; - if (typeof obj.key === "string") { - // If the key is a string, we need to parse it as EncString - key = EncString.fromJSON(obj.key); - } else if ((obj.key as any) instanceof EncString) { - // If the key is already an EncString instance, we can use it directly - key = obj.key; - } - view.key = key; - } + view.key = obj.key ?? undefined; switch (obj.type) { case CipherType.Card: @@ -299,7 +285,7 @@ export class CipherView implements View, InitializerMetadata { /** * Creates a CipherView from the SDK CipherView. */ - static fromSdkCipherView(obj: SdkCipherView, sdk?: CiphersClient): CipherView | undefined { + static fromSdkCipherView(obj: SdkCipherView): CipherView | undefined { if (obj == null) { return undefined; } @@ -340,7 +326,7 @@ export class CipherView implements View, InitializerMetadata { cipherView.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate); cipherView.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate); cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None; - cipherView.key = obj.key ? EncString.fromJSON(obj.key) : undefined; + cipherView.key = obj.key ?? undefined; switch (obj.type) { case CipherType.Card: @@ -353,17 +339,9 @@ export class CipherView implements View, InitializerMetadata { break; case CipherType.Login: cipherView.login = obj.login ? LoginView.fromSdkLoginView(obj.login) : new LoginView(); - if (sdk && obj.login?.fido2Credentials?.length) { - const fido2CredentialViews = sdk.decrypt_fido2_credentials(obj); - const decryptedKeyValue = sdk.decrypt_fido2_private_key(obj); - cipherView.login.fido2Credentials = fido2CredentialViews - .map((cred) => { - const view = Fido2CredentialView.fromSdkFido2CredentialView(cred); - if (view) { - view.keyValue = decryptedKeyValue; - } - return view; - }) + if (obj.login?.fido2Credentials?.length) { + cipherView.login.fido2Credentials = obj.login.fido2Credentials + .map((cred) => Fido2CredentialView.fromSdkFido2CredentialView(cred)) .filter((cred): cred is Fido2CredentialView => !!cred); } break; @@ -447,7 +425,7 @@ export class CipherView implements View, InitializerMetadata { revisionDate: this.revisionDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), - key: this.key?.toSdk(), + key: (this.key ?? undefined) as any, }; // If the cipher has FIDO2 credentials, we need to set them on the SDK edit request @@ -552,7 +530,7 @@ export class CipherView implements View, InitializerMetadata { deletedDate: this.deletedDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), reprompt: this.reprompt ?? CipherRepromptType.None, - key: this.key?.toSdk(), + key: (this.key ?? undefined) as any, // Cipher type specific properties are set in the switch statement below // CipherView initializes each with default constructors (undefined values) // The SDK does not expect those undefined values and will throw exceptions diff --git a/libs/common/src/vault/services/cipher-sdk.service.spec.ts b/libs/common/src/vault/services/cipher-sdk.service.spec.ts index d5ba0188ae58..1683fe5b408f 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.spec.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.spec.ts @@ -66,7 +66,6 @@ describe("DefaultCipherSdkService", () => { share_cipher: jest.fn(), share_ciphers_bulk: jest.fn(), decrypt_fido2_credentials: jest.fn(), - decrypt_fido2_private_key: jest.fn(), get_all: jest.fn().mockResolvedValue({ successes: [], failures: [] }), update_collection: jest.fn(), delete_attachment: jest.fn(), @@ -192,14 +191,13 @@ describe("DefaultCipherSdkService", () => { expect(result?.name).toBe(cipherView.name); }); - it("should decrypt FIDO2 credentials from create response", async () => { + it("should pass sdkCipherView with FIDO2 credentials to fromSdkCipherView", async () => { const cipherView = new CipherView(); cipherView.id = cipherId; cipherView.type = CipherType.Login; cipherView.name = "Test Cipher"; cipherView.organizationId = orgId; - // Build an SDK response that includes encrypted FIDO2 credentials const mockSdkResponse = { ...cipherView.toSdkCipherView(), login: { @@ -209,22 +207,14 @@ describe("DefaultCipherSdkService", () => { } as unknown as SdkCipherView; mockCiphersSdk.create.mockResolvedValue(mockSdkResponse); - // Mock FIDO2 decryption - const mockDecryptedFido2 = [{ credentialId: "decrypted-cred-id" }]; - mockCiphersSdk.decrypt_fido2_credentials.mockReturnValue(mockDecryptedFido2); - mockCiphersSdk.decrypt_fido2_private_key.mockReturnValue("decrypted-key-value"); - const mockFido2View = new Fido2CredentialView(); - mockFido2View.credentialId = "decrypted-cred-id"; + mockFido2View.credentialId = "encrypted-cred-id"; jest.spyOn(Fido2CredentialView, "fromSdkFido2CredentialView").mockReturnValue(mockFido2View); const result = await cipherSdkService.createWithServer(cipherView, userId, false); - expect(mockCiphersSdk.decrypt_fido2_credentials).toHaveBeenCalledWith(mockSdkResponse); - expect(mockCiphersSdk.decrypt_fido2_private_key).toHaveBeenCalledWith(mockSdkResponse); expect(result?.login?.fido2Credentials).toHaveLength(1); - expect(result?.login?.fido2Credentials?.[0].credentialId).toBe("decrypted-cred-id"); - expect(result?.login?.fido2Credentials?.[0].keyValue).toBe("decrypted-key-value"); + expect(result?.login?.fido2Credentials?.[0].credentialId).toBe("encrypted-cred-id"); }); it("should throw error and log when SDK throws an error", async () => { @@ -373,7 +363,7 @@ describe("DefaultCipherSdkService", () => { expect(result.name).toBe(cipherView.name); }); - it("should decrypt FIDO2 credentials from edit response", async () => { + it("should pass sdkCipherView with FIDO2 credentials to fromSdkCipherView", async () => { const cipherView = new CipherView(); cipherView.id = cipherId; cipherView.type = CipherType.Login; @@ -381,7 +371,6 @@ describe("DefaultCipherSdkService", () => { cipherView.organizationId = orgId; cipherView.edit = true; - // Build an SDK response that includes encrypted FIDO2 credentials const mockSdkResponse = { ...cipherView.toSdkCipherView(), login: { @@ -391,22 +380,14 @@ describe("DefaultCipherSdkService", () => { } as unknown as SdkCipherView; mockCiphersSdk.edit.mockResolvedValue(mockSdkResponse); - // Mock FIDO2 decryption - const mockDecryptedFido2 = [{ credentialId: "decrypted-cred-id" }]; - mockCiphersSdk.decrypt_fido2_credentials.mockReturnValue(mockDecryptedFido2); - mockCiphersSdk.decrypt_fido2_private_key.mockReturnValue("decrypted-key-value"); - const mockFido2View = new Fido2CredentialView(); - mockFido2View.credentialId = "decrypted-cred-id"; + mockFido2View.credentialId = "encrypted-cred-id"; jest.spyOn(Fido2CredentialView, "fromSdkFido2CredentialView").mockReturnValue(mockFido2View); const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, false); - expect(mockCiphersSdk.decrypt_fido2_credentials).toHaveBeenCalledWith(mockSdkResponse); - expect(mockCiphersSdk.decrypt_fido2_private_key).toHaveBeenCalledWith(mockSdkResponse); expect(result?.login?.fido2Credentials).toHaveLength(1); - expect(result?.login?.fido2Credentials?.[0].credentialId).toBe("decrypted-cred-id"); - expect(result?.login?.fido2Credentials?.[0].keyValue).toBe("decrypted-key-value"); + expect(result?.login?.fido2Credentials?.[0].credentialId).toBe("encrypted-cred-id"); }); it("should throw error and log when SDK throws an error", async () => { diff --git a/libs/common/src/vault/services/cipher-sdk.service.ts b/libs/common/src/vault/services/cipher-sdk.service.ts index 7705bfb484c6..dc6d4d32121c 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.ts @@ -47,7 +47,7 @@ export class DefaultCipherSdkService implements CipherSdkService { result = await sdkCiphersClient.create(sdkCreateRequest); } - return CipherView.fromSdkCipherView(result, sdkCiphersClient); + return CipherView.fromSdkCipherView(result); }), catchError((error: unknown) => { this.logService.error(`Failed to create cipher: ${error}`); @@ -87,7 +87,7 @@ export class DefaultCipherSdkService implements CipherSdkService { result = await sdkCiphersClient.edit_partial(sdkPartialUpdateRequest); } - return CipherView.fromSdkCipherView(result, sdkCiphersClient); + return CipherView.fromSdkCipherView(result); }), catchError((error: unknown) => { this.logService.error(`Failed to update cipher: ${error}`); @@ -282,7 +282,7 @@ export class DefaultCipherSdkService implements CipherSdkService { originalCipherView?.toSdkCipherView(sdkCiphersClient), ); - return CipherView.fromSdkCipherView(result, sdkCiphersClient); + return CipherView.fromSdkCipherView(result); }), catchError((error: unknown) => { this.logService.error(`Failed to share cipher: ${error}`); @@ -441,12 +441,11 @@ export class DefaultCipherSdkService implements CipherSdkService { this.sdkService.userClient$(userId).pipe( switchMap(async (sdk) => { using ref = sdk.take(); - const sdkCiphersClient = ref.value.vault().ciphers(); const result = await ref.value .vault() .attachments() .upgrade_attachment(asUuid(cipherId), attachmentId); - return CipherView.fromSdkCipherView(result, sdkCiphersClient); + return CipherView.fromSdkCipherView(result); }), catchError((error: unknown) => { this.logService.error(`Failed to upgrade attachment: ${error}`); @@ -611,7 +610,7 @@ export class DefaultCipherSdkService implements CipherSdkService { asUuid(cipherId), collectionIds.map((id) => asUuid(id)), ); - return CipherView.fromSdkCipherView(result, sdkCiphersClient); + return CipherView.fromSdkCipherView(result); }), catchError((error: unknown) => { this.logService.error(`Failed to update cipher collections as admin: ${error}`); @@ -636,7 +635,7 @@ export class DefaultCipherSdkService implements CipherSdkService { collectionIds.map((id) => asUuid(id)), false, ); - return CipherView.fromSdkCipherView(result, sdkCiphersClient); + return CipherView.fromSdkCipherView(result); }), catchError((error: unknown) => { this.logService.error(`Failed to update cipher collections: ${error}`); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts index 78e017b5c45b..0202b2247f70 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -11,7 +11,6 @@ import { Fido2CredentialFullView, } from "@bitwarden/sdk-internal"; -import { mockEnc } from "../../../spec"; import { UriMatchStrategy } from "../../models/domain/domain-service"; import { LogService } from "../../platform/abstractions/log.service"; import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; @@ -101,7 +100,6 @@ describe("DefaultCipherEncryptionService", () => { decrypt: jest.fn(), decrypt_list: jest.fn(), decrypt_list_with_failures: jest.fn(), - decrypt_fido2_credentials: jest.fn(), move_to_organization: jest.fn(), }), attachments: jest.fn().mockReturnValue({ @@ -466,79 +464,28 @@ describe("DefaultCipherEncryptionService", () => { expect(cipherObj.toSdkCipher).toHaveBeenCalledTimes(1); expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledWith({ id: cipherData.id }); expect(CipherView.fromSdkCipherView).toHaveBeenCalledWith(sdkCipherView); - expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).not.toHaveBeenCalled(); }); - it("should decrypt FIDO2 credentials if present", async () => { + it("should pass sdkCipherView with FIDO2 credentials to fromSdkCipherView", async () => { const fido2Credentials = [ - { - credentialId: mockEnc("credentialId"), - keyType: mockEnc("keyType"), - keyAlgorithm: mockEnc("keyAlgorithm"), - keyCurve: mockEnc("keyCurve"), - keyValue: mockEnc("keyValue"), - rpId: mockEnc("rpId"), - userHandle: mockEnc("userHandle"), - userName: mockEnc("userName"), - counter: mockEnc("2"), - rpName: mockEnc("rpName"), - userDisplayName: mockEnc("userDisplayName"), - discoverable: mockEnc("true"), - creationDate: new Date("2023-01-01T12:00:00.000Z"), - }, + { credentialId: "credentialId" }, ] as unknown as SdkFido2Credential[]; - sdkCipherView.login!.fido2Credentials = fido2Credentials; const expectedCipherView: CipherView = { id: cipherId, type: CipherType.Login, name: "test-name", - login: { - username: "test-username", - password: "test-password", - fido2Credentials: [], - }, + login: { username: "test-username", fido2Credentials: [] }, } as unknown as CipherView; - const fido2CredentialView: Fido2CredentialView = { - credentialId: "credentialId", - keyType: "keyType", - keyAlgorithm: "keyAlgorithm", - keyCurve: "keyCurve", - keyValue: "decrypted-key-value", - rpId: "rpId", - userHandle: "userHandle", - userName: "userName", - counter: 2, - rpName: "rpName", - userDisplayName: "userDisplayName", - discoverable: true, - creationDate: new Date("2023-01-01T12:00:00.000Z"), - } as unknown as Fido2CredentialView; - mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView); - mockSdkClient.vault().ciphers().decrypt_fido2_credentials.mockReturnValue(fido2Credentials); - mockSdkClient.vault().ciphers().decrypt_fido2_private_key = jest - .fn() - .mockReturnValue("decrypted-key-value"); - jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView); - jest - .spyOn(Fido2CredentialView, "fromSdkFido2CredentialView") - .mockReturnValueOnce(fido2CredentialView); const result = await cipherEncryptionService.decrypt(cipherObj, userId); expect(result).toBe(expectedCipherView); - expect(result.login?.fido2Credentials).toEqual([fido2CredentialView]); - expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).toHaveBeenCalledWith( - sdkCipherView, - ); - expect(mockSdkClient.vault().ciphers().decrypt_fido2_private_key).toHaveBeenCalledWith( - sdkCipherView, - ); - expect(Fido2CredentialView.fromSdkFido2CredentialView).toHaveBeenCalledTimes(1); + expect(CipherView.fromSdkCipherView).toHaveBeenCalledWith(sdkCipherView); }); }); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index 5458c65c0e1b..65b0bf39fd25 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -8,11 +8,9 @@ import { UserId, OrganizationId } from "../../types/guid"; import { UserKey } from "../../types/key"; import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { EncryptionContext } from "../abstractions/cipher.service"; -import { CipherType } from "../enums"; import { Cipher } from "../models/domain/cipher"; import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; -import { Fido2CredentialView } from "../models/view/fido2-credential.view"; export class DefaultCipherEncryptionService implements CipherEncryptionService { constructor( @@ -142,32 +140,6 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!; - // Decrypt Fido2 credentials if available - if ( - clientCipherView.type === CipherType.Login && - sdkCipherView.login?.fido2Credentials?.length - ) { - const fido2CredentialViews = ref.value - .vault() - .ciphers() - .decrypt_fido2_credentials(sdkCipherView); - - // TEMPORARY: Manually decrypt the keyValue for Fido2 credentials - // since we don't currently use the SDK for Fido2 Authentication. - const decryptedKeyValue = ref.value - .vault() - .ciphers() - .decrypt_fido2_private_key(sdkCipherView); - - clientCipherView.login.fido2Credentials = fido2CredentialViews - .map((f) => { - const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; - view.keyValue = decryptedKeyValue; - return view; - }) - .filter((view): view is Fido2CredentialView => view !== undefined); - } - return clientCipherView; }), catchError((error: unknown) => { @@ -192,30 +164,6 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { const sdkCipherView = await ref.value.vault().ciphers().decrypt(cipher.toSdkCipher()); const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!; - // Handle FIDO2 credentials if present - if ( - clientCipherView.type === CipherType.Login && - sdkCipherView.login?.fido2Credentials?.length - ) { - const fido2CredentialViews = ref.value - .vault() - .ciphers() - .decrypt_fido2_credentials(sdkCipherView); - - const decryptedKeyValue = ref.value - .vault() - .ciphers() - .decrypt_fido2_private_key(sdkCipherView); - - clientCipherView.login.fido2Credentials = fido2CredentialViews - .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}`); From 33b19b47ea8094fdf5e598bf5a20429de6d5c15e Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 7 Aug 2026 14:47:29 -0700 Subject: [PATCH 2/4] Consume SDK changes for decrypted cipher/attachment/fido2/totp keys --- .../common/src/key-management/state-bridge.ts | 24 +++++++++++++++++++ .../common/src/models/export/cipher.export.ts | 8 +++++-- .../src/vault/models/domain/attachment.ts | 1 - .../src/vault/models/view/attachment.view.ts | 4 ++-- .../src/vault/models/view/cipher.view.spec.ts | 19 ++++++++------- .../src/vault/models/view/cipher.view.ts | 11 +++++---- .../src/vault/services/cipher-sdk.service.ts | 6 ++--- 7 files changed, 50 insertions(+), 23 deletions(-) diff --git a/libs/common/src/key-management/state-bridge.ts b/libs/common/src/key-management/state-bridge.ts index e2e1a253ef53..c9288880f0c4 100644 --- a/libs/common/src/key-management/state-bridge.ts +++ b/libs/common/src/key-management/state-bridge.ts @@ -9,6 +9,7 @@ import { SymmetricKey, V2UpgradeToken, WasmStateBridge, + WebAuthnPrfUnlockData, WrappedAccountCryptographicState, Kdf, } from "@bitwarden/sdk-internal"; @@ -17,6 +18,7 @@ import { UserId } from "@bitwarden/user-core"; import { compareValues } from "../platform/misc/compare-values"; import { SymmetricCryptoKey } from "../platform/models/domain/symmetric-crypto-key"; import { USER_KEY } from "../platform/services/key-state/user-key.state"; +import { CRYPTO_DISK } from "../platform/state"; import { StateProvider, UserKeyDefinition } from "../state-migrations"; import { UserKey } from "../types/key"; @@ -30,6 +32,16 @@ import { } from "./pin/pin.state"; import { V2_UPGRADE_TOKEN } from "./upgrade-token/v2-upgrade-token.state"; +const WEBAUTHN_PRF_UNLOCK_DATA = new UserKeyDefinition( + CRYPTO_DISK, + "webAuthnPrfUnlockData", + { + deserializer: (jsonValue) => jsonValue, + clearOn: ["logout"], + cleanupDelayMs: 0, + }, +); + // Helper functions to work around unreliable state. KM state values correctness over speed // and eventual consistency is not acceptable. @@ -211,4 +223,16 @@ export class JsWasmStateBridge implements WasmStateBridge { async clear_kdf_config(): Promise { await deleteAtomic(this.stateProvider, this.userId, KDF_CONFIG); } + + async set_webauthn_prf_unlock_data(value: WebAuthnPrfUnlockData): Promise { + await writeAtomic(this.stateProvider, this.userId, WEBAUTHN_PRF_UNLOCK_DATA, value); + } + + async get_webauthn_prf_unlock_data(): Promise { + return await readAtomic(this.stateProvider, this.userId, WEBAUTHN_PRF_UNLOCK_DATA); + } + + async clear_webauthn_prf_unlock_data(): Promise { + await deleteAtomic(this.stateProvider, this.userId, WEBAUTHN_PRF_UNLOCK_DATA); + } } diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 7ab7173ea5d0..7885226b2871 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -1,4 +1,5 @@ import { EncString } from "../../key-management/crypto/models/enc-string"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CipherRepromptType } from "../../vault/enums/cipher-reprompt-type"; import { CipherType } from "../../vault/enums/cipher-type"; import { Cipher as CipherDomain } from "../../vault/models/domain/cipher"; @@ -43,7 +44,7 @@ export class CipherExport { view.notes = req.notes; view.favorite = req.favorite; view.reprompt = req.reprompt ?? CipherRepromptType.None; - view.key = req.key != null ? new EncString(req.key) : undefined; + view.key = req.key != null ? SymmetricCryptoKey.fromString(req.key) : undefined; if (req.fields != null) { view.fields = req.fields.map((f) => FieldExport.toView(f)); @@ -208,7 +209,10 @@ export class CipherExport { this.name = safeGetString(o.name) ?? ""; this.notes = safeGetString(o.notes); if ("key" in o) { - this.key = o.key?.encryptedString; + this.key = + o.key instanceof SymmetricCryptoKey + ? o.key.toBase64() + : (o.key as EncString | undefined)?.encryptedString; } this.favorite = o.favorite; diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index b9bcaad8cea9..23d23a8c5a5e 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -46,7 +46,6 @@ 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 diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index 423b117a658d..e26746604603 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; +import { AttachmentView as SdkAttachmentView, SymmetricKey } from "@bitwarden/sdk-internal"; import { DECRYPT_ERROR } from "../../../key-management/crypto/models/enc-string"; import { View } from "../../../models/view/view"; @@ -61,7 +61,7 @@ export class AttachmentView implements View { size: this.size, sizeName: this.sizeName, fileName: this.fileName, - key: this.key?.toBase64() ?? undefined, + key: (this.key?.toBase64() ?? undefined) as SymmetricKey | undefined, }; } diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index 2a7f2a02ebc9..f000007ddd63 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -12,6 +12,7 @@ import { import { mockFromJson, mockFromSdk } from "../../../../spec"; import { asUuid } from "../../../platform/abstractions/sdk/sdk.service"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { CipherRepromptType } from "../../enums"; import { CipherType } from "../../enums/cipher-type"; import { CipherPermissionsApi } from "../api/cipher-permissions.api"; @@ -91,13 +92,13 @@ describe("CipherView", () => { expect(actual).toMatchObject(expected); }); - it("passes the cipher key through as a plain string", () => { - const cipherKeyB64 = "c29tZS1iYXNlNjQta2V5"; + it("deserializes the cipher key to a SymmetricCryptoKey", () => { + const mockKey = { keyB64: "c29tZS1iYXNlNjQta2V5" }; + jest.spyOn(SymmetricCryptoKey, "fromJSON").mockReturnValue(mockKey as any); - const actual = CipherView.fromJSON({ - key: cipherKeyB64, - }); - expect(actual.key).toBe(cipherKeyB64); + const actual = CipherView.fromJSON({ key: mockKey as any }); + expect(actual.key).toBe(mockKey); + expect(SymmetricCryptoKey.fromJSON).toHaveBeenCalledWith(mockKey); }); it("fromJSON should always restore top-level CipherView properties", () => { @@ -130,7 +131,7 @@ describe("CipherView", () => { original.deletedDate = new Date("2022-01-03"); original.archivedDate = new Date("2022-01-04"); original.reprompt = CipherRepromptType.Password; - original.key = "dGVzdC1rZXktYjY0"; + original.key = new SymmetricCryptoKey(new Uint8Array(64)); original.decryptionFailure = true; // Serialize and deserialize @@ -291,7 +292,7 @@ describe("CipherView", () => { cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c"; cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f"; cipherView.collectionIds = ["b0473506-3c3c-4260-a734-dfaaf833ab6f"]; - cipherView.key = "some-key-b64"; + cipherView.key = { toBase64: () => "some-key-b64" } as any; cipherView.name = "name"; cipherView.notes = "notes"; cipherView.type = CipherType.Login; @@ -503,7 +504,7 @@ describe("CipherView", () => { cipherView.reprompt = CipherRepromptType.Password; cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z"); cipherView.archivedDate = new Date("2022-01-03T12:00:00.000Z"); - cipherView.key = "cipher-key-b64"; + cipherView.key = { toBase64: () => "cipher-key-b64" } as any; const mockField = new RealFieldView(); mockField.name = "testField"; diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 51adc70d92d1..10658a39432b 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -10,6 +10,7 @@ import { import { View } from "../../../models/view/view"; import { asUuid, uuidAsString } from "../../../platform/abstractions/sdk/sdk.service"; import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { DeepJsonify } from "../../../types/deep-jsonify"; import { CipherType, LinkedIdType } from "../../enums"; @@ -64,7 +65,7 @@ export class CipherView implements View, InitializerMetadata { deletedDate?: Date; archivedDate?: Date; reprompt: CipherRepromptType = CipherRepromptType.None; - key?: string; + key?: SymmetricCryptoKey; /** * Flag to indicate if the cipher decryption failed. @@ -248,7 +249,7 @@ export class CipherView implements View, InitializerMetadata { view.passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)) ?? []; - view.key = obj.key ?? undefined; + view.key = obj.key != null ? SymmetricCryptoKey.fromJSON(obj.key) : undefined; switch (obj.type) { case CipherType.Card: @@ -326,7 +327,7 @@ export class CipherView implements View, InitializerMetadata { cipherView.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate); cipherView.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate); cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None; - cipherView.key = obj.key ?? undefined; + cipherView.key = obj.key ? SymmetricCryptoKey.fromString(obj.key) : undefined; switch (obj.type) { case CipherType.Card: @@ -425,7 +426,7 @@ export class CipherView implements View, InitializerMetadata { revisionDate: this.revisionDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), - key: (this.key ?? undefined) as any, + key: this.key?.toBase64() ?? undefined, }; // If the cipher has FIDO2 credentials, we need to set them on the SDK edit request @@ -530,7 +531,7 @@ export class CipherView implements View, InitializerMetadata { deletedDate: this.deletedDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), reprompt: this.reprompt ?? CipherRepromptType.None, - key: (this.key ?? undefined) as any, + key: (this.key?.toBase64() ?? undefined) as any, // Cipher type specific properties are set in the switch statement below // CipherView initializes each with default constructors (undefined values) // The SDK does not expect those undefined values and will throw exceptions diff --git a/libs/common/src/vault/services/cipher-sdk.service.ts b/libs/common/src/vault/services/cipher-sdk.service.ts index dc6d4d32121c..2ae4aee8e288 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.ts @@ -313,7 +313,7 @@ export class DefaultCipherSdkService implements CipherSdkService { ); return results - .map((c) => CipherView.fromSdkCipherView(c, sdkCiphersClient)) + .map((c) => CipherView.fromSdkCipherView(c)) .filter((c): c is CipherView => c !== undefined); }), catchError((error: unknown) => { @@ -465,9 +465,7 @@ export class DefaultCipherSdkService implements CipherSdkService { const decryptResult = await sdkCiphersClient.get_all(); const successes = [...(decryptResult.successes ?? [])] - .map((sdkCipherView: any) => - CipherView.fromSdkCipherView(sdkCipherView, sdkCiphersClient), - ) + .map((sdkCipherView: any) => CipherView.fromSdkCipherView(sdkCipherView)) .filter((v): v is CipherView => v !== undefined); const failures: CipherView[] = [...(decryptResult.failures ?? [])].map((failure: any) => { From 5da5fc57717a73a812d41174033b833c910f86fe Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Mon, 10 Aug 2026 15:19:04 -0700 Subject: [PATCH 3/4] Consume new SymmetricKey type for CipherEditREquest --- libs/common/src/vault/models/view/cipher.view.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 10658a39432b..cbe14ed76062 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -5,6 +5,7 @@ import { CiphersClient, CipherViewType, CipherView as SdkCipherView, + SymmetricKey, } from "@bitwarden/sdk-internal"; import { View } from "../../../models/view/view"; @@ -426,7 +427,7 @@ export class CipherView implements View, InitializerMetadata { revisionDate: this.revisionDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), - key: this.key?.toBase64() ?? undefined, + key: (this.key?.toBase64() ?? undefined) as SymmetricKey | undefined, }; // If the cipher has FIDO2 credentials, we need to set them on the SDK edit request From f8d0e2abf958552319ca41175d21c54d9d79537a Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 14 Aug 2026 14:38:07 -0700 Subject: [PATCH 4/4] Address PR feedback --- apps/cli/src/vault/models/cipher.response.ts | 2 ++ libs/common/src/models/export/cipher.export.ts | 7 ++++++- libs/common/src/vault/models/domain/cipher.ts | 1 + .../vault/models/view/attachment.view.spec.ts | 2 +- .../src/vault/models/view/attachment.view.ts | 6 +++--- .../src/vault/models/view/cipher.view.spec.ts | 4 ++-- libs/common/src/vault/models/view/cipher.view.ts | 11 ++--------- libs/common/src/vault/models/view/login.view.ts | 16 ++++++++-------- 8 files changed, 25 insertions(+), 24 deletions(-) diff --git a/apps/cli/src/vault/models/cipher.response.ts b/apps/cli/src/vault/models/cipher.response.ts index 87e158eb23d3..373139ae7f70 100644 --- a/apps/cli/src/vault/models/cipher.response.ts +++ b/apps/cli/src/vault/models/cipher.response.ts @@ -22,6 +22,8 @@ export class CipherResponse extends CipherWithIdExport implements BaseResponse { super(); this.object = "item"; this.build(o); + // Redact the decrypted item key to prevent logging it to the CLI output. + this.key = undefined; if (o.attachments != null) { this.attachments = o.attachments.map((a) => new AttachmentResponse(a)); } diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 7885226b2871..364da49f6be0 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -44,7 +44,12 @@ export class CipherExport { view.notes = req.notes; view.favorite = req.favorite; view.reprompt = req.reprompt ?? CipherRepromptType.None; - view.key = req.key != null ? SymmetricCryptoKey.fromString(req.key) : undefined; + try { + view.key = req.key != null ? SymmetricCryptoKey.fromString(req.key) : undefined; + } catch { + // Old exports stored the wrapped EncString key which cannot be used on import + view.key = undefined; + } if (req.fields != null) { view.fields = req.fields.map((f) => FieldExport.toView(f)); diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 8602d2a7adc0..48ebb2c71a06 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -162,6 +162,7 @@ export class Cipher extends Domain implements Decryptable { try { const cipherKey = await encryptService.unwrapSymmetricKey(this.key, userKeyOrOrgKey); cipherDecryptionKey = cipherKey; + model.key = cipherKey; bypassValidation = false; } catch { model.name = "[error: cannot decrypt]"; diff --git a/libs/common/src/vault/models/view/attachment.view.spec.ts b/libs/common/src/vault/models/view/attachment.view.spec.ts index f34e6a64c66d..3db683a162fb 100644 --- a/libs/common/src/vault/models/view/attachment.view.spec.ts +++ b/libs/common/src/vault/models/view/attachment.view.spec.ts @@ -54,7 +54,7 @@ describe("AttachmentView", () => { describe("toSdkAttachmentView", () => { it("should convert AttachmentView to SdkAttachmentView", () => { const mockKey = { - toBase64: jest.fn().mockReturnValue("keyB64"), + toSdk: jest.fn().mockReturnValue("keyB64"), } as any; const attachmentView = new AttachmentView(); diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index e26746604603..199e9f651991 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { AttachmentView as SdkAttachmentView, SymmetricKey } from "@bitwarden/sdk-internal"; +import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; import { DECRYPT_ERROR } from "../../../key-management/crypto/models/enc-string"; import { View } from "../../../models/view/view"; @@ -61,7 +61,7 @@ export class AttachmentView implements View { size: this.size, sizeName: this.sizeName, fileName: this.fileName, - key: (this.key?.toBase64() ?? undefined) as SymmetricKey | undefined, + key: this.key?.toSdk(), }; } @@ -93,6 +93,6 @@ export class AttachmentView implements View { * In this case, the attachment is encrypted with the user's user-key */ isLegacyAttachment(): boolean { - return this.key == null; + return this.key == null && !this.hasDecryptionError; } } diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index f000007ddd63..a9973748ad8c 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -292,7 +292,7 @@ describe("CipherView", () => { cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c"; cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f"; cipherView.collectionIds = ["b0473506-3c3c-4260-a734-dfaaf833ab6f"]; - cipherView.key = { toBase64: () => "some-key-b64" } as any; + cipherView.key = { toSdk: () => "some-key-b64" } as any; cipherView.name = "name"; cipherView.notes = "notes"; cipherView.type = CipherType.Login; @@ -504,7 +504,7 @@ describe("CipherView", () => { cipherView.reprompt = CipherRepromptType.Password; cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z"); cipherView.archivedDate = new Date("2022-01-03T12:00:00.000Z"); - cipherView.key = { toBase64: () => "cipher-key-b64" } as any; + cipherView.key = { toSdk: () => "cipher-key-b64" } as any; const mockField = new RealFieldView(); mockField.name = "testField"; diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index cbe14ed76062..e7ce001791a5 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -5,7 +5,6 @@ import { CiphersClient, CipherViewType, CipherView as SdkCipherView, - SymmetricKey, } from "@bitwarden/sdk-internal"; import { View } from "../../../models/view/view"; @@ -24,7 +23,6 @@ import { AttachmentView } from "./attachment.view"; import { BankAccountView } from "./bank-account.view"; import { CardView } from "./card.view"; import { DriversLicenseView } from "./drivers-license.view"; -import { Fido2CredentialView } from "./fido2-credential.view"; import { FieldView } from "./field.view"; import { IdentityView } from "./identity.view"; import { ItemView } from "./item.view"; @@ -341,11 +339,6 @@ export class CipherView implements View, InitializerMetadata { break; case CipherType.Login: cipherView.login = obj.login ? LoginView.fromSdkLoginView(obj.login) : new LoginView(); - if (obj.login?.fido2Credentials?.length) { - cipherView.login.fido2Credentials = obj.login.fido2Credentials - .map((cred) => Fido2CredentialView.fromSdkFido2CredentialView(cred)) - .filter((cred): cred is Fido2CredentialView => !!cred); - } break; case CipherType.SecureNote: cipherView.secureNote = obj.secureNote @@ -427,7 +420,7 @@ export class CipherView implements View, InitializerMetadata { revisionDate: this.revisionDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), - key: (this.key?.toBase64() ?? undefined) as SymmetricKey | undefined, + key: this.key?.toSdk(), }; // If the cipher has FIDO2 credentials, we need to set them on the SDK edit request @@ -532,7 +525,7 @@ export class CipherView implements View, InitializerMetadata { deletedDate: this.deletedDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), reprompt: this.reprompt ?? CipherRepromptType.None, - key: (this.key?.toBase64() ?? undefined) as any, + key: this.key?.toSdk(), // Cipher type specific properties are set in the switch statement below // CipherView initializes each with default constructors (undefined values) // The SDK does not expect those undefined values and will throw exceptions diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 63f6332b3e6b..21440618af6a 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -111,10 +111,7 @@ export class LoginView extends ItemView { /** * Converts the SDK LoginView to a LoginView. * - * Note: FIDO2 credentials remain encrypted at this stage. - * Unlike other fields that are decrypted as part of the LoginView, the SDK maintains - * the FIDO2 credentials in encrypted form. We can decrypt them later using a separate - * call to client.vault().ciphers().decrypt_fido2_credentials(). + * FIDO2 credentials are eagerly decrypted by the SDK and mapped here directly. */ static fromSdkLoginView(obj: SdkLoginView): LoginView { const loginView = new LoginView(); @@ -129,8 +126,10 @@ export class LoginView extends ItemView { obj.uris ?.filter((uri) => uri.uri != null && uri.uri !== "") .map((uri) => LoginUriView.fromSdkLoginUriView(uri)!) || []; - // FIDO2 credentials are not decrypted here, they remain encrypted - loginView.fido2Credentials = []; + loginView.fido2Credentials = + obj.fido2Credentials + ?.map((cred) => Fido2CredentialView.fromSdkFido2CredentialView(cred)) + .filter((cred): cred is Fido2CredentialView => !!cred) ?? []; return loginView; } @@ -138,7 +137,8 @@ export class LoginView extends ItemView { /** * Converts the LoginView to an SDK LoginView. * - * Note: FIDO2 credentials remain encrypted in the SDK view so they are not included here. + * Note: FIDO2 credentials are intentionally excluded on the write path — they are + * handled separately via toSdkCipherView when the cipher has passkeys. */ toSdkLoginView(): SdkLoginView { return { @@ -148,7 +148,7 @@ export class LoginView extends ItemView { totp: this.hasTotp ? this.totp : undefined, autofillOnPageLoad: this.autofillOnPageLoad ?? undefined, uris: this.uris?.map((uri) => uri.toSdkLoginUriView()), - fido2Credentials: undefined, // FIDO2 credentials are handled separately and remain encrypted + fido2Credentials: undefined, }; } }