diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 84fdb3ddbb6..4c37c152ff6 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -8,6 +8,13 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { + MasterKeyWrappedUserKey, + MasterPasswordAuthenticationData, + MasterPasswordAuthenticationHash, + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -20,7 +27,13 @@ import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, UserPrivateKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { newGuid } from "@bitwarden/guid"; -import { Argon2KdfConfig, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; +import { + Argon2KdfConfig, + DEFAULT_KDF_CONFIG, + KdfType, + KeyService, + PBKDF2KdfConfig, +} from "@bitwarden/key-management"; import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; import { EmergencyAccessType } from "../enums/emergency-access-type"; @@ -468,6 +481,212 @@ describe("EmergencyAccessService", () => { }); }); + describe("takeover [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { + // Mock feature flag value + const PM27086_UpdateAuthenticationApisForInputPasswordEnabled = true; + + // Mock sut method params + const params = { + id: "emergencyAccessId", + masterPassword: "mockPassword", + email: "user@example.com", + activeUserId: newGuid() as UserId, + }; + + // Mock method data + const kdfConfig = DEFAULT_KDF_CONFIG; + + const takeoverResponse = { + keyEncrypted: "EncryptedKey", + kdf: kdfConfig.kdfType, + kdfIterations: kdfConfig.iterations, + } as EmergencyAccessTakeoverResponse; + + const activeUserPrivateKey = new Uint8Array(64) as UserPrivateKey; + let mockGrantorUserKey: UserKey; + let salt: MasterPasswordSalt; + let authenticationData: MasterPasswordAuthenticationData; + let unlockData: MasterPasswordUnlockData; + + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue( + PM27086_UpdateAuthenticationApisForInputPasswordEnabled, + ); + + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValue(takeoverResponse); + keyService.userPrivateKey$.mockReturnValue(of(activeUserPrivateKey)); + + const mockDecryptedGrantorUserKey = new SymmetricCryptoKey(new Uint8Array(64)); + encryptService.decapsulateKeyUnsigned.mockResolvedValue(mockDecryptedGrantorUserKey); + mockGrantorUserKey = mockDecryptedGrantorUserKey as UserKey; + + salt = params.email as MasterPasswordSalt; + masterPasswordService.emailToSalt.mockReturnValue(salt); + + authenticationData = { + salt, + kdf: kdfConfig, + masterPasswordAuthenticationHash: + "masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, + }; + + unlockData = { + salt, + kdf: kdfConfig, + masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, + } as MasterPasswordUnlockData; + + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + authenticationData, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(unlockData); + }); + + it("should throw if active user private key is not found", async () => { + // Arrange + keyService.userPrivateKey$.mockReturnValue(of(null)); + + // Act + const promise = emergencyAccessService.takeover( + params.id, + params.masterPassword, + params.email, + params.activeUserId, + ); + + // Assert + await expect(promise).rejects.toThrow( + "Active user does not have a private key, cannot complete a takeover.", + ); + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); + + it("should throw if the grantor user key cannot be decrypted via the active user private key", async () => { + // Arrange + encryptService.decapsulateKeyUnsigned.mockResolvedValue(null); + + // Act + const promise = emergencyAccessService.takeover( + params.id, + params.masterPassword, + params.email, + params.activeUserId, + ); + + // Assert + await expect(promise).rejects.toThrow("Failed to decrypt grantor key"); + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); + + it("should use PBKDF2 if takeover response contains KdfType.PBKDF2_SHA256", async () => { + // Act + await emergencyAccessService.takeover( + params.id, + params.masterPassword, + params.email, + params.activeUserId, + ); + + // Assert + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + params.masterPassword, + kdfConfig, // default config (PBKDF2) + salt, + ); + }); + + it("should use Argon2 if takeover response contains KdfType.Argon2id", async () => { + // Arrange + const argon2TakeoverResponse = { + keyEncrypted: "EncryptedKey", + kdf: KdfType.Argon2id, + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 4, + } as EmergencyAccessTakeoverResponse; + + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValue( + argon2TakeoverResponse, + ); + + const expectedKdfConfig = new Argon2KdfConfig( + argon2TakeoverResponse.kdfIterations, + argon2TakeoverResponse.kdfMemory, + argon2TakeoverResponse.kdfParallelism, + ); + + // Act + await emergencyAccessService.takeover( + params.id, + params.masterPassword, + params.email, + params.activeUserId, + ); + + // Assert + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + params.masterPassword, + expectedKdfConfig, + salt, + ); + expect(masterPasswordService.makeMasterPasswordAuthenticationData).not.toHaveBeenCalledWith( + params.masterPassword, + kdfConfig, // default config (PBKDF2) + salt, + ); + }); + + it("should call makeMasterPasswordAuthenticationData and makeMasterPasswordUnlockData with the correct parameters", async () => { + // Act + await emergencyAccessService.takeover( + params.id, + params.masterPassword, + params.email, + params.activeUserId, + ); + + // Assert + const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData); + + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + params.masterPassword, + kdfConfig, + salt, + ); + + expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith( + params.masterPassword, + kdfConfig, + salt, + mockGrantorUserKey, + ); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith( + params.id, + request, + ); + }); + + it("should call the API method to change the grantor's master password", async () => { + // Act + await emergencyAccessService.takeover( + params.id, + params.masterPassword, + params.email, + params.activeUserId, + ); + + // Assert + const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledTimes(1); + expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith( + params.id, + request, + ); + }); + }); + describe("getRotatedData", () => { const allowedStatuses = [ EmergencyAccessStatusType.Confirmed,