import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { Send } from "@bitwarden/common/tools/send/models/domain/send"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FakeAccountService, mockAccountServiceWith, } from "../../../../../../libs/common/spec/fake-account-service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { StateService } from "../../core"; import { EmergencyAccessService } from "../emergency-access"; import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; import { UserKeyRotationService } from "./user-key-rotation.service"; describe("KeyRotationService", () => { let keyRotationService: UserKeyRotationService; let mockApiService: MockProxy; let mockCipherService: MockProxy; let mockFolderService: MockProxy; let mockSendService: MockProxy; let mockEmergencyAccessService: MockProxy; let mockResetPasswordService: MockProxy; let mockDeviceTrustCryptoService: MockProxy; let mockCryptoService: MockProxy; let mockEncryptService: MockProxy; let mockStateService: MockProxy; let mockConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); beforeAll(() => { mockMasterPasswordService = new FakeMasterPasswordService(); mockApiService = mock(); mockCipherService = mock(); mockFolderService = mock(); mockSendService = mock(); mockEmergencyAccessService = mock(); mockResetPasswordService = mock(); mockDeviceTrustCryptoService = mock(); mockCryptoService = mock(); mockEncryptService = mock(); mockStateService = mock(); mockConfigService = mock(); keyRotationService = new UserKeyRotationService( mockMasterPasswordService, mockApiService, mockCipherService, mockFolderService, mockSendService, mockEmergencyAccessService, mockResetPasswordService, mockDeviceTrustCryptoService, mockCryptoService, mockEncryptService, mockStateService, mockAccountService, mockConfigService, ); }); beforeEach(() => { jest.clearAllMocks(); }); it("instantiates", () => { expect(keyRotationService).not.toBeFalsy(); }); describe("rotateUserKeyAndEncryptedData", () => { let folderViews: BehaviorSubject; let sends: BehaviorSubject; beforeAll(() => { mockCryptoService.makeMasterKey.mockResolvedValue("mockMasterKey" as any); mockCryptoService.makeUserKey.mockResolvedValue([ new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, { encryptedString: "mockEncryptedUserKey", } as any, ]); mockCryptoService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash"); mockConfigService.getFeatureFlag.mockResolvedValue(true); // Mock private key mockCryptoService.getPrivateKey.mockResolvedValue("MockPrivateKey" as any); // Mock ciphers const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")]; mockCipherService.getAllDecrypted.mockResolvedValue(mockCiphers); // Mock folders const mockFolders = [createMockFolder("1", "Folder 1"), createMockFolder("2", "Folder 2")]; folderViews = new BehaviorSubject(mockFolders); mockFolderService.folderViews$ = folderViews; // Mock sends const mockSends = [createMockSend("1", "Send 1"), createMockSend("2", "Send 2")]; sends = new BehaviorSubject(mockSends); mockSendService.sends$ = sends; // Mock encryption methods mockEncryptService.encrypt.mockResolvedValue({ encryptedString: "mockEncryptedData", } as any); mockFolderService.encrypt.mockImplementation((folder, userKey) => { const encryptedFolder = new Folder(); encryptedFolder.id = folder.id; encryptedFolder.name = new EncString( EncryptionType.AesCbc256_HmacSha256_B64, "Encrypted: " + folder.name, ); return Promise.resolve(encryptedFolder); }); mockCipherService.encrypt.mockImplementation((cipher, userKey) => { const encryptedCipher = new Cipher(); encryptedCipher.id = cipher.id; encryptedCipher.name = new EncString( EncryptionType.AesCbc256_HmacSha256_B64, "Encrypted: " + cipher.name, ); return Promise.resolve(encryptedCipher); }); }); it("rotates the user key and encrypted data", async () => { await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"); expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled(); const arg = mockApiService.postUserKeyUpdate.mock.calls[0][0]; expect(arg.ciphers.length).toBe(2); expect(arg.folders.length).toBe(2); }); it("throws if master password provided is falsey", async () => { await expect(keyRotationService.rotateUserKeyAndEncryptedData("")).rejects.toThrow(); }); it("throws if master key creation fails", async () => { mockCryptoService.makeMasterKey.mockResolvedValueOnce(null); await expect( keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"), ).rejects.toThrow(); }); it("throws if user key creation fails", async () => { mockCryptoService.makeUserKey.mockResolvedValueOnce([null, null]); await expect( keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"), ).rejects.toThrow(); }); it("saves the master key in state after creation", async () => { await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"); expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( "mockMasterKey" as any, mockUserId, ); }); it("throws if server rotation fails", async () => { mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError")); await expect( keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"), ).rejects.toThrow(); }); }); }); function createMockFolder(id: string, name: string): FolderView { const folder = new FolderView(); folder.id = id; folder.name = name; return folder; } function createMockCipher(id: string, name: string): CipherView { const cipher = new CipherView(); cipher.id = id; cipher.name = name; cipher.type = CipherType.Login; return cipher; } function createMockSend(id: string, name: string): Send { const send = new Send(); send.id = id; send.name = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, name); return send; }