mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
[PM-22136] Implement SDK cipher encryption (#15337)
* [PM-22136] Update sdk cipher view map to support uknown uuid type * [PM-22136] Add key to CipherView for copying to SdkCipherView for encryption * [PM-22136] Add fromSdk* helpers to Cipher domain objects * [PM-22136] Add toSdk* helpers to Cipher View objects * [PM-22136] Add encrypt() to cipher encryption service * [PM-22136] Add feature flag * [PM-22136] Use new SDK encrypt method when feature flag is enabled * [PM-22136] Filter out null/empty URIs * [PM-22136] Change default value for cipher view arrays to []. See ADR-0014. * [PM-22136] Keep encrypted key value on attachment so that it is passed to the SDK * [PM-22136] Keep encrypted key value on CipherView so that it is passed to the SDK during encryption * [PM-22136] Update failing attachment test * [PM-22136] Update failing importer tests due to new default value for arrays * [PM-22136] Update CipherView.fromJson to handle the prototype of EncString for the cipher key * [PM-22136] Add tickets for followup work * [PM-22136] Use new set_fido2_credentials SDK method instead * [PM-22136] Fix missing prototype when decrypting Fido2Credentials * [PM-22136] Fix test after sdk change * [PM-22136] Update @bitwarden/sdk-internal version * [PM-22136] Fix some strict typing errors * [PM-23348] Migrate move cipher to org to SDK (#15567) * [PM-23348] Add moveToOrganization method to cipher-encryption.service.ts * [PM-23348] Use cipherEncryptionService.moveToOrganization in cipherService shareWithServer and shareManyWithServer methods * [PM-23348] Update cipherFormService to use the shareWithServer() method instead of encrypt() * [PM-23348] Fix typo * [PM-23348] Add missing docs * [PM-22136] Fix EncString import after merge with main
This commit is contained in:
@@ -1,20 +1,22 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { Fido2Credential } from "@bitwarden/common/vault/models/domain/fido2-credential";
|
||||
import {
|
||||
Fido2Credential,
|
||||
Fido2Credential as SdkFido2Credential,
|
||||
Cipher as SdkCipher,
|
||||
CipherType as SdkCipherType,
|
||||
CipherView as SdkCipherView,
|
||||
CipherListView,
|
||||
AttachmentView as SdkAttachmentView,
|
||||
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";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { UserId, CipherId, OrganizationId } from "../../types/guid";
|
||||
import { CipherRepromptType, CipherType } from "../enums";
|
||||
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
@@ -25,10 +27,15 @@ import { Fido2CredentialView } from "../models/view/fido2-credential.view";
|
||||
|
||||
import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service";
|
||||
|
||||
const cipherId = "bdc4ef23-1116-477e-ae73-247854af58cb" as CipherId;
|
||||
const orgId = "c5e9654f-6cc5-44c4-8e09-3d323522668c" as OrganizationId;
|
||||
const folderId = "a3e9654f-6cc5-44c4-8e09-3d323522668c";
|
||||
const userId = "59fbbb44-8cc8-4279-ab40-afc5f68704f4" as UserId;
|
||||
|
||||
const cipherData: CipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
id: cipherId,
|
||||
organizationId: orgId,
|
||||
folderId: folderId,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
@@ -78,13 +85,17 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
const sdkService = mock<SdkService>();
|
||||
const logService = mock<LogService>();
|
||||
let sdkCipherView: SdkCipherView;
|
||||
let sdkCipher: SdkCipher;
|
||||
|
||||
const mockSdkClient = {
|
||||
vault: jest.fn().mockReturnValue({
|
||||
ciphers: jest.fn().mockReturnValue({
|
||||
encrypt: jest.fn(),
|
||||
set_fido2_credentials: jest.fn(),
|
||||
decrypt: jest.fn(),
|
||||
decrypt_list: jest.fn(),
|
||||
decrypt_fido2_credentials: jest.fn(),
|
||||
move_to_organization: jest.fn(),
|
||||
}),
|
||||
attachments: jest.fn().mockReturnValue({
|
||||
decrypt_buffer: jest.fn(),
|
||||
@@ -99,21 +110,25 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
take: jest.fn().mockReturnValue(mockRef),
|
||||
};
|
||||
|
||||
const userId = "user-id" as UserId;
|
||||
|
||||
let cipherObj: Cipher;
|
||||
let cipherViewObj: CipherView;
|
||||
|
||||
beforeEach(() => {
|
||||
sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
|
||||
cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService);
|
||||
cipherObj = new Cipher(cipherData);
|
||||
cipherViewObj = new CipherView(cipherObj);
|
||||
|
||||
jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => {
|
||||
return { id: cipherData.id } as SdkCipher;
|
||||
});
|
||||
|
||||
jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(() => {
|
||||
return { id: cipherData.id } as SdkCipherView;
|
||||
});
|
||||
|
||||
sdkCipherView = {
|
||||
id: "test-id",
|
||||
id: cipherId as string,
|
||||
type: SdkCipherType.Login,
|
||||
name: "test-name",
|
||||
login: {
|
||||
@@ -121,16 +136,211 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
password: "test-password",
|
||||
},
|
||||
} as SdkCipherView;
|
||||
|
||||
sdkCipher = {
|
||||
id: cipherId,
|
||||
type: SdkCipherType.Login,
|
||||
name: "encrypted-name",
|
||||
login: {
|
||||
username: "encrypted-username",
|
||||
password: "encrypted-password",
|
||||
},
|
||||
} as unknown as SdkCipher;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("encrypt", () => {
|
||||
it("should encrypt a cipher successfully", async () => {
|
||||
const expectedCipher: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name",
|
||||
login: {
|
||||
username: "encrypted-username",
|
||||
password: "encrypted-password",
|
||||
},
|
||||
} as unknown as Cipher;
|
||||
|
||||
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||
cipher: sdkCipher,
|
||||
encryptedFor: userId,
|
||||
});
|
||||
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
|
||||
|
||||
const result = await cipherEncryptionService.encrypt(cipherViewObj, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.cipher).toEqual(expectedCipher);
|
||||
expect(result!.encryptedFor).toBe(userId);
|
||||
expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
|
||||
expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith({ id: cipherData.id });
|
||||
});
|
||||
|
||||
it("should encrypt FIDO2 credentials if present", async () => {
|
||||
const fidoCredentialView = new Fido2CredentialView();
|
||||
fidoCredentialView.credentialId = "credentialId";
|
||||
|
||||
cipherViewObj.login.fido2Credentials = [fidoCredentialView];
|
||||
|
||||
jest.spyOn(fidoCredentialView, "toSdkFido2CredentialFullView").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
credentialId: "credentialId",
|
||||
}) as Fido2CredentialFullView,
|
||||
);
|
||||
jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
id: cipherId as string,
|
||||
login: {
|
||||
fido2Credentials: undefined,
|
||||
},
|
||||
}) as unknown as SdkCipherView,
|
||||
);
|
||||
|
||||
mockSdkClient
|
||||
.vault()
|
||||
.ciphers()
|
||||
.set_fido2_credentials.mockReturnValue({
|
||||
id: cipherId as string,
|
||||
login: {
|
||||
fido2Credentials: [
|
||||
{
|
||||
credentialId: "encrypted-credentialId",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||
cipher: sdkCipher,
|
||||
encryptedFor: userId,
|
||||
});
|
||||
|
||||
cipherObj.login!.fido2Credentials = [
|
||||
{ credentialId: "encrypted-credentialId" } as unknown as Fido2Credential,
|
||||
];
|
||||
|
||||
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(cipherObj);
|
||||
|
||||
const result = await cipherEncryptionService.encrypt(cipherViewObj, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.cipher.login!.fido2Credentials).toHaveLength(1);
|
||||
|
||||
// Ensure set_fido2_credentials was called with correct parameters
|
||||
expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: cipherId }),
|
||||
[{ credentialId: "credentialId" }],
|
||||
);
|
||||
|
||||
// Encrypted fido2 credential should be in the cipher passed to encrypt
|
||||
expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: cipherId,
|
||||
login: { fido2Credentials: [{ credentialId: "encrypted-credentialId" }] },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveToOrganization", () => {
|
||||
it("should call the sdk method to move a cipher to an organization", async () => {
|
||||
const expectedCipher: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name",
|
||||
organizationId: orgId,
|
||||
login: {
|
||||
username: "encrypted-username",
|
||||
password: "encrypted-password",
|
||||
},
|
||||
} as unknown as Cipher;
|
||||
|
||||
mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({
|
||||
id: cipherId,
|
||||
organizationId: orgId,
|
||||
});
|
||||
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||
cipher: sdkCipher,
|
||||
encryptedFor: userId,
|
||||
});
|
||||
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
|
||||
|
||||
const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.cipher).toEqual(expectedCipher);
|
||||
expect(result!.encryptedFor).toBe(userId);
|
||||
expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
|
||||
expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith(
|
||||
{ id: cipherData.id },
|
||||
orgId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should re-encrypt any fido2 credentials when moving to an organization", async () => {
|
||||
const mockSdkCredentialView = {
|
||||
username: "username",
|
||||
} as unknown as Fido2CredentialFullView;
|
||||
const mockCredentialView = mock<Fido2CredentialView>();
|
||||
mockCredentialView.toSdkFido2CredentialFullView.mockReturnValue(mockSdkCredentialView);
|
||||
cipherViewObj.login.fido2Credentials = [mockCredentialView];
|
||||
const expectedCipher: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name",
|
||||
organizationId: orgId,
|
||||
login: {
|
||||
username: "encrypted-username",
|
||||
password: "encrypted-password",
|
||||
fido2Credentials: [{ username: "encrypted-username" }],
|
||||
},
|
||||
} as unknown as Cipher;
|
||||
|
||||
mockSdkClient
|
||||
.vault()
|
||||
.ciphers()
|
||||
.set_fido2_credentials.mockReturnValue({
|
||||
id: cipherId as string,
|
||||
login: {
|
||||
fido2Credentials: [mockSdkCredentialView],
|
||||
},
|
||||
} as SdkCipherView);
|
||||
mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({
|
||||
id: cipherId,
|
||||
organizationId: orgId,
|
||||
});
|
||||
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||
cipher: sdkCipher,
|
||||
encryptedFor: userId,
|
||||
});
|
||||
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
|
||||
|
||||
const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.cipher).toEqual(expectedCipher);
|
||||
expect(result!.encryptedFor).toBe(userId);
|
||||
expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
|
||||
expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: cipherId }),
|
||||
expect.arrayContaining([mockSdkCredentialView]),
|
||||
);
|
||||
expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith(
|
||||
{ id: cipherData.id, login: { fido2Credentials: [mockSdkCredentialView] } },
|
||||
orgId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
it("should decrypt a cipher successfully", async () => {
|
||||
const expectedCipherView: CipherView = {
|
||||
id: "test-id",
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "test-name",
|
||||
login: {
|
||||
@@ -168,12 +378,12 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
discoverable: mockEnc("true"),
|
||||
creationDate: new Date("2023-01-01T12:00:00.000Z"),
|
||||
},
|
||||
] as unknown as Fido2Credential[];
|
||||
] as unknown as SdkFido2Credential[];
|
||||
|
||||
sdkCipherView.login!.fido2Credentials = fido2Credentials;
|
||||
|
||||
const expectedCipherView: CipherView = {
|
||||
id: "test-id",
|
||||
id: cipherId,
|
||||
type: CipherType.Login,
|
||||
name: "test-name",
|
||||
login: {
|
||||
@@ -228,13 +438,15 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
it("should decrypt multiple ciphers successfully", async () => {
|
||||
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
|
||||
|
||||
const cipherId2 = "bdc4ef23-2222-477e-ae73-247854af58cb" as CipherId;
|
||||
|
||||
const expectedViews = [
|
||||
{
|
||||
id: "test-id-1",
|
||||
id: cipherId as string,
|
||||
name: "test-name-1",
|
||||
} as CipherView,
|
||||
{
|
||||
id: "test-id-2",
|
||||
id: cipherId2 as string,
|
||||
name: "test-name-2",
|
||||
} as CipherView,
|
||||
];
|
||||
@@ -242,8 +454,11 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
mockSdkClient
|
||||
.vault()
|
||||
.ciphers()
|
||||
.decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView)
|
||||
.mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView);
|
||||
.decrypt.mockReturnValueOnce({
|
||||
id: cipherId,
|
||||
name: "test-name-1",
|
||||
} as unknown as SdkCipherView)
|
||||
.mockReturnValueOnce({ id: cipherId2, name: "test-name-2" } as unknown as SdkCipherView);
|
||||
|
||||
jest
|
||||
.spyOn(CipherView, "fromSdkCipherView")
|
||||
|
||||
Reference in New Issue
Block a user