diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts index bd193be48bf3..d5e77526c5f0 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts @@ -90,8 +90,7 @@ describe("DesktopSetInitialPasswordService", () => { }); /** - * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties - * in the test setup above that are now un-used and can also be removed. + * @deprecated use `initializePasswordJitPasswordUserV2Encryption()` instead */ describe("setInitialPassword(...)", () => { // Mock function parameters diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts index b03d87870f97..c3a09a261ac5 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts @@ -56,7 +56,7 @@ export class DesktopSetInitialPasswordService } /** - * @deprecated To be removed in PM-28143 + * @deprecated use `initializePasswordJitPasswordUserV2Encryption()` instead */ override async setInitialPassword( credentials: SetInitialPasswordCredentials, diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts index e442294d632e..54dc8af17e5f 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts @@ -21,7 +21,6 @@ import { MasterPasswordSalt, MasterPasswordUnlockData, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -29,8 +28,8 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { UserKey, OrgKey, MasterKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KdfConfig, KdfType, KeyService } from "@bitwarden/key-management"; +import { UserKey, OrgKey } from "@bitwarden/common/types/key"; +import { DEFAULT_KDF_CONFIG, KdfConfig, KeyService } from "@bitwarden/key-management"; import { OrganizationUserResetPasswordService } from "./organization-user-reset-password.service"; @@ -49,7 +48,6 @@ describe("OrganizationUserResetPasswordService", () => { const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; - let configService: MockProxy; beforeAll(() => { keyService = mock(); @@ -60,7 +58,6 @@ describe("OrganizationUserResetPasswordService", () => { i18nService = mock(); accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); - configService = mock(); sut = new OrganizationUserResetPasswordService( keyService, @@ -71,7 +68,6 @@ describe("OrganizationUserResetPasswordService", () => { i18nService, accountService, masterPasswordService, - configService, ); }); @@ -144,82 +140,13 @@ describe("OrganizationUserResetPasswordService", () => { }); }); - /** - * @deprecated This 'describe' to be removed in PM-28143. When you remove this, check also if there are - * any imports/properties in the test setup above that are now un-used and can also be removed. - */ - describe("resetMasterPassword [PM27086_UpdateAuthenticationApisForInputPassword flag DISABLED]", () => { - const PM27086_UpdateAuthenticationApisForInputPasswordFlagEnabled = false; - - const mockNewMP = "new-password"; - const mockEmail = "test@example.com"; - const mockOrgUserId = "test-org-user-id"; - const mockOrgId = "test-org-id"; - - beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue( - PM27086_UpdateAuthenticationApisForInputPasswordFlagEnabled, - ); - - organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue( - new OrganizationUserResetPasswordDetailsResponse({ - kdf: KdfType.PBKDF2_SHA256, - kdfIterations: 5000, - resetPasswordKey: "test-reset-password-key", - encryptedPrivateKey: "test-encrypted-private-key", - }), - ); - - const mockRandomBytes = new Uint8Array(64) as CsprngArray; - const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey; - keyService.orgKeys$.mockReturnValue( - of({ [mockOrgId]: mockOrgKey } as Record), - ); - - encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes); - - encryptService.rsaDecrypt.mockResolvedValue(mockRandomBytes); - const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey; - keyService.makeMasterKey.mockResolvedValue(mockMasterKey); - keyService.hashMasterKey.mockResolvedValue("test-master-key-hash"); - - const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; - keyService.encryptUserKeyWithMasterKey.mockResolvedValue([ - mockUserKey, - new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "test-encrypted-user-key"), - ]); - }); - - it("should reset the user's master password", async () => { - await sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId); - expect(organizationUserApiService.putOrganizationUserRecoverAccount).toHaveBeenCalled(); - }); - - it("should throw an error if the user details are null", async () => { - organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null); - await expect( - sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId), - ).rejects.toThrow(); - }); - - it("should throw an error if the org key is null", async () => { - keyService.orgKeys$.mockReturnValue(of(null)); - await expect( - sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId), - ).rejects.toThrow(); - }); - }); - - describe("resetMasterPassword [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { + describe("resetMasterPassword", () => { // Mock sut method parameters const newMasterPassword = "new-master-password"; const email = "user@example.com"; const orgUserId = "org-user-id"; const orgId = "org-id" as OrganizationId; - // Mock feature flag value - const PM27086_UpdateAuthenticationApisForInputPasswordFlagEnabled = true; - // Mock method data let organizationUserResetPasswordDetailsResponse: OrganizationUserResetPasswordDetailsResponse; let salt: MasterPasswordSalt; @@ -229,11 +156,6 @@ describe("OrganizationUserResetPasswordService", () => { let userKey: UserKey; beforeEach(() => { - // Mock feature flag value - configService.getFeatureFlag.mockResolvedValue( - PM27086_UpdateAuthenticationApisForInputPasswordFlagEnabled, - ); - // Mock method data kdfConfig = DEFAULT_KDF_CONFIG; @@ -381,9 +303,7 @@ describe("OrganizationUserResetPasswordService", () => { let unlockData: MasterPasswordUnlockData; /** Sets up mocks needed when resetMasterPassword is true */ - function setupPasswordResetMocks(flagEnabled: boolean) { - configService.getFeatureFlag.mockResolvedValue(flagEnabled); - + function setupPasswordResetMocks() { kdfConfig = DEFAULT_KDF_CONFIG; organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue( @@ -406,46 +326,29 @@ describe("OrganizationUserResetPasswordService", () => { const mockDecryptedUserKey = new SymmetricCryptoKey(new Uint8Array(64).fill(3)); encryptService.decapsulateKeyUnsigned.mockResolvedValue(mockDecryptedUserKey); - if (flagEnabled) { - salt = email as MasterPasswordSalt; - masterPasswordService.mock.emailToSalt.mockReturnValue(salt); - - authenticationData = { - salt, - kdf: kdfConfig, - masterPasswordAuthenticationHash: - "masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, - }; - - unlockData = { - salt, - kdf: kdfConfig, - masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, - } as MasterPasswordUnlockData; - - masterPasswordService.mock.makeMasterPasswordAuthenticationData.mockResolvedValue( - authenticationData, - ); - masterPasswordService.mock.makeMasterPasswordUnlockData.mockResolvedValue(unlockData); - } else { - const mockRandomBytes = new Uint8Array(64) as CsprngArray; - const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey; - keyService.makeMasterKey.mockResolvedValue(mockMasterKey); - keyService.hashMasterKey.mockResolvedValue("test-master-key-hash"); - const mockEncryptedUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; - keyService.encryptUserKeyWithMasterKey.mockResolvedValue([ - mockEncryptedUserKey, - new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "test-encrypted-user-key"), - ]); - } + salt = email as MasterPasswordSalt; + masterPasswordService.mock.emailToSalt.mockReturnValue(salt); + + authenticationData = { + salt, + kdf: kdfConfig, + masterPasswordAuthenticationHash: + "masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, + }; + + unlockData = { + salt, + kdf: kdfConfig, + masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, + } as MasterPasswordUnlockData; + + masterPasswordService.mock.makeMasterPasswordAuthenticationData.mockResolvedValue( + authenticationData, + ); + masterPasswordService.mock.makeMasterPasswordUnlockData.mockResolvedValue(unlockData); } describe("reset 2FA only", () => { - beforeEach(() => { - // No feature flag or password details needed for 2FA-only reset - configService.getFeatureFlag.mockResolvedValue(false); - }); - it("should call putOrganizationUserRecoverAccount with resetTwoFactor: true and resetMasterPassword: false", async () => { await sut.recoverAccount({ organizationUserId: orgUserId, @@ -487,62 +390,9 @@ describe("OrganizationUserResetPasswordService", () => { }); }); - describe("reset master password only [PM27086_UpdateAuthenticationApisForInputPassword flag DISABLED]", () => { - beforeEach(() => { - setupPasswordResetMocks(false); - }); - - it("should call putOrganizationUserRecoverAccount with resetMasterPassword: true and resetTwoFactor: false", async () => { - await sut.recoverAccount({ - organizationUserId: orgUserId, - organizationId: orgId, - resetMasterPassword: true, - resetTwoFactor: false, - newMasterPassword, - email, - }); - - expect(organizationUserApiService.putOrganizationUserRecoverAccount).toHaveBeenCalledWith( - orgId, - orgUserId, - expect.objectContaining({ resetMasterPassword: true, resetTwoFactor: false }), - ); - }); - - it("should throw if reset password details are null", async () => { - organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null); - - await expect( - sut.recoverAccount({ - organizationUserId: orgUserId, - organizationId: orgId, - resetMasterPassword: true, - resetTwoFactor: false, - newMasterPassword, - email, - }), - ).rejects.toThrow(); - }); - - it("should throw if org key is null", async () => { - keyService.orgKeys$.mockReturnValue(of(null)); - - await expect( - sut.recoverAccount({ - organizationUserId: orgUserId, - organizationId: orgId, - resetMasterPassword: true, - resetTwoFactor: false, - newMasterPassword, - email, - }), - ).rejects.toThrow(); - }); - }); - - describe("reset master password only [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { + describe("reset master password only", () => { beforeEach(() => { - setupPasswordResetMocks(true); + setupPasswordResetMocks(); }); it("should call putOrganizationUserRecoverAccount with password data and resetTwoFactor: false", async () => { @@ -598,9 +448,9 @@ describe("OrganizationUserResetPasswordService", () => { }); }); - describe("reset both master password and 2FA [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { + describe("reset both master password and 2FA", () => { beforeEach(() => { - setupPasswordResetMocks(true); + setupPasswordResetMocks(); }); it("should call putOrganizationUserRecoverAccount with both flags true and password data", async () => { @@ -692,7 +542,7 @@ describe("OrganizationUserResetPasswordService", () => { function createOrganization(id: string, name: string, resetPasswordEnrolled = true): Organization { const org = new Organization(); - org.id = id; + org.id = id as OrganizationId; org.name = name; org.identifier = name; org.isMember = true; diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index 571d98475d19..801190fdced6 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -12,7 +12,6 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, @@ -20,7 +19,6 @@ import { } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; @@ -63,7 +61,6 @@ export class OrganizationUserResetPasswordService implements UserKeyRotationKeyR private i18nService: I18nService, private accountService: AccountService, private masterPasswordService: MasterPasswordServiceAbstraction, - private configService: ConfigService, ) {} /** @@ -129,23 +126,12 @@ export class OrganizationUserResetPasswordService implements UserKeyRotationKeyR request.organizationId, ); - const newApisEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword, - ); - - ({ newMasterPasswordHash, key } = newApisEnabled - ? await this.buildResetPasswordRequestV2( - request.newMasterPassword, - request.email, - kdfConfig, - existingUserKey, - ) - : await this.buildMasterPasswordRequest( - request.newMasterPassword, - request.email, - kdfConfig, - existingUserKey, - )); + ({ newMasterPasswordHash, key } = await this.buildResetPasswordRequest( + request.newMasterPassword, + request.email, + kdfConfig, + existingUserKey, + )); } await this.organizationUserApiService.putOrganizationUserRecoverAccount( @@ -280,11 +266,7 @@ export class OrganizationUserResetPasswordService implements UserKeyRotationKeyR )) as UserKey; } - /** - * Builds a reset password request using the new authentication APIs - * (feature flag PM27086_UpdateAuthenticationApisForInputPassword). - */ - private async buildResetPasswordRequestV2( + private async buildResetPasswordRequest( newMasterPassword: string, email: string, kdfConfig: KdfConfig, @@ -316,28 +298,4 @@ export class OrganizationUserResetPasswordService implements UserKeyRotationKeyR key: unlockData.masterKeyWrappedUserKey, }; } - - /** Builds a reset password request using the legacy crypto path. */ - private async buildMasterPasswordRequest( - newMasterPassword: string, - email: string, - kdfConfig: KdfConfig, - existingUserKey: UserKey, - ): Promise> { - const newMasterKey = await this.keyService.makeMasterKey( - newMasterPassword, - email.trim().toLowerCase(), - kdfConfig, - ); - const newMasterKeyHash = await this.keyService.hashMasterKey(newMasterPassword, newMasterKey); - const newUserKey = await this.keyService.encryptUserKeyWithMasterKey( - newMasterKey, - existingUserKey, - ); - - return { - newMasterPasswordHash: newMasterKeyHash, - key: newUserKey[1].encryptedString, - }; - } } diff --git a/apps/web/src/app/auth/core/services/change-password/web-change-password.service.spec.ts b/apps/web/src/app/auth/core/services/change-password/web-change-password.service.spec.ts index 151e837bd33e..8fae477bf7dd 100644 --- a/apps/web/src/app/auth/core/services/change-password/web-change-password.service.spec.ts +++ b/apps/web/src/app/auth/core/services/change-password/web-change-password.service.spec.ts @@ -40,10 +40,6 @@ describe("WebChangePasswordService", () => { }), }; - const currentPassword = "currentPassword"; - const newPassword = "newPassword"; - const newPasswordHint = "newPasswordHint"; - beforeEach(() => { keyService = mock(); masterPasswordApiService = mock(); @@ -64,23 +60,6 @@ describe("WebChangePasswordService", () => { ); }); - describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => { - it("should call the method with the same name on the UserKeyRotationService with the correct arguments", async () => { - // Act - await sut.rotateUserKeyMasterPasswordAndEncryptedData( - currentPassword, - newPassword, - user, - newPasswordHint, - ); - - // Assert - expect( - userKeyRotationService.rotateUserKeyMasterPasswordAndEncryptedData, - ).toHaveBeenCalledWith(currentPassword, newPassword, user, newPasswordHint); - }); - }); - describe("changePasswordAndRotateUserKey()", () => { // Mock method params let passwordInputResult: PasswordInputResult; @@ -93,7 +72,6 @@ describe("WebChangePasswordService", () => { newPasswordHint: "new-password-hint", kdfConfig: DEFAULT_KDF_CONFIG, salt: "salt" as MasterPasswordSalt, - newApisWithInputPasswordFlagEnabled: true, }; // Mock returned/resolved values @@ -119,11 +97,11 @@ describe("WebChangePasswordService", () => { }); }); - it("should throw if newPasswordHint is null on the PasswordInputResult object", async () => { + it("should throw if newPasswordHint is undefined on the PasswordInputResult object", async () => { // Arrange const invalidPasswordInputResult: PasswordInputResult = { ...passwordInputResult, - newPasswordHint: null, + newPasswordHint: undefined, }; // Act diff --git a/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts b/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts index ff0cf0db9dfd..8d7c93a67470 100644 --- a/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts +++ b/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts @@ -57,23 +57,6 @@ export class WebChangePasswordService ); } - /** - * @deprecated To be removed in PM-28143 - */ - override async rotateUserKeyMasterPasswordAndEncryptedData( - currentPassword: string, - newPassword: string, - user: Account, - newPasswordHint: string, - ): Promise { - await this.userKeyRotationService.rotateUserKeyMasterPasswordAndEncryptedData( - currentPassword, - newPassword, - user, - newPasswordHint, - ); - } - async clearDeeplinkState() { await this.routerService.getAndClearLoginRedirectUrl(); } diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts index 7e262a7ab750..a04dc5cc20ce 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts @@ -90,7 +90,9 @@ describe("WebSetInitialPasswordService", () => { }); /** - * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties + * @deprecated use `initializePasswordJitPasswordUserV2Encryption()` instead + * + * When you remove this, check also if there are any imports/properties * in the test setup above that are now un-used and can also be removed. */ describe("setInitialPassword(...)", () => { diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts index a6a902ab8477..9921449bbdc2 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts @@ -57,7 +57,7 @@ export class WebSetInitialPasswordService } /** - * @deprecated To be removed in PM-28143 + * @deprecated use `initializePasswordJitPasswordUserV2Encryption()` instead */ override async setInitialPassword( credentials: SetInitialPasswordCredentials, diff --git a/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap b/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap index fafd44efe68c..d2c418a3d9de 100644 --- a/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap +++ b/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap @@ -1,132 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) it registers the user with org invite when given an org invite 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": "orgInviteToken", - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": "organizationUserId", - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user when given a provider invite token 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": "providerInviteToken", - "providerUserId": "providerUserId", - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user when given an emergency access invite token 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": "emergencyAccessId", - "acceptEmergencyAccessInviteToken": "acceptEmergencyAccessInviteToken", - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user when given an org sponsored free family plan token 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": "orgSponsoredFreeFamilyPlanToken", - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user with KDF fields when given valid email verification input 1`] = ` +exports[`WebRegistrationFinishService finishRegistration() derives the master key and registers the user 1`] = ` RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": "emailVerificationToken", - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) derives the master key and registers the user with new data types 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", @@ -160,8 +35,8 @@ RegisterFinishRequestWithAuthUnlockDataTypes { } `; -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) it registers the user with org invite when given an org invite 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +exports[`WebRegistrationFinishService finishRegistration() it registers the user with org invite when given an org invite 1`] = ` +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", @@ -195,8 +70,8 @@ RegisterFinishRequestWithAuthUnlockDataTypes { } `; -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) registers the user when given a provider invite token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +exports[`WebRegistrationFinishService finishRegistration() registers the user when given a provider invite token 1`] = ` +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", @@ -230,8 +105,8 @@ RegisterFinishRequestWithAuthUnlockDataTypes { } `; -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) registers the user when given an emergency access invite token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +exports[`WebRegistrationFinishService finishRegistration() registers the user when given an emergency access invite token 1`] = ` +RegisterFinishRequest { "acceptEmergencyAccessId": "emergencyAccessId", "acceptEmergencyAccessInviteToken": "acceptEmergencyAccessInviteToken", "email": "test@email.com", @@ -265,8 +140,8 @@ RegisterFinishRequestWithAuthUnlockDataTypes { } `; -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) registers the user when given an org sponsored free family plan token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +exports[`WebRegistrationFinishService finishRegistration() registers the user when given an org sponsored free family plan token 1`] = ` +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 08061a9e4fff..7fef7fcfdea8 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -9,7 +9,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; @@ -22,7 +21,6 @@ import { MasterPasswordAuthenticationHash, MasterKeyWrappedUserKey, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -40,7 +38,6 @@ describe("WebRegistrationFinishService", () => { let logService: MockProxy; let policyService: MockProxy; let masterPasswordService: MockProxy; - let configService: MockProxy; beforeEach(() => { keyService = mock(); @@ -50,13 +47,11 @@ describe("WebRegistrationFinishService", () => { logService = mock(); policyService = mock(); masterPasswordService = mock(); - configService = mock(); service = new WebRegistrationFinishService( keyService, accountApiService, masterPasswordService, - configService, organizationInviteService, policyApiService, logService, @@ -196,17 +191,17 @@ describe("WebRegistrationFinishService", () => { email = "test@email.com"; emailVerificationToken = "emailVerificationToken"; masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey; + salt = "salt" as MasterPasswordSalt; + passwordInputResult = { - newMasterKey: masterKey, - newServerMasterKeyHash: "newServerMasterKeyHash", + newPassword: "newPassword", kdfConfig: DEFAULT_KDF_CONFIG, newPasswordHint: "newPasswordHint", - newPassword: "newPassword", + salt: salt, }; userKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; userKeyEncString = new EncString("userKeyEncrypted"); - userKeyPair = ["publicKey", new EncString("privateKey")]; orgInvite = new OrganizationInvite(); @@ -219,12 +214,12 @@ describe("WebRegistrationFinishService", () => { providerInviteToken = "providerInviteToken"; providerUserId = "providerUserId"; + keyService.makeMasterKey.mockResolvedValue(masterKey); keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - salt = "salt" as MasterPasswordSalt; masterPasswordAuthentication = { salt, kdf: DEFAULT_KDF_CONFIG, @@ -235,6 +230,10 @@ describe("WebRegistrationFinishService", () => { DEFAULT_KDF_CONFIG, "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, ); + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + masterPasswordAuthentication, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); }); it("throws an error if the user key cannot be created", async () => { @@ -245,272 +244,129 @@ describe("WebRegistrationFinishService", () => { ); }); - describe("when feature flag is OFF (old API)", () => { - it("registers the user with KDF fields when given valid email verification input", async () => { - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Old API sends flat KDF and master password hash fields - expect(registerCall.kdf).toBeDefined(); - expect(registerCall.kdfIterations).toBeDefined(); - expect(registerCall.masterPasswordHash).toBeDefined(); - expect(registerCall.userSymmetricKey).toBeDefined(); - - // Unique to this flow: emailVerificationToken is populated - expect(registerCall.emailVerificationToken).toEqual(emailVerificationToken); - - expect(registerCall).toMatchSnapshot(); - }); - - it("it registers the user with org invite when given an org invite", async () => { - organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); - - await service.finishRegistration(email, passwordInputResult); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: org invite fields are populated - expect(registerCall.orgInviteToken).toEqual(orgInvite.token); - expect(registerCall.organizationUserId).toEqual(orgInvite.organizationUserId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an org sponsored free family plan token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - orgSponsoredFreeFamilyPlanToken, - ); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: org sponsored free family plan token is populated - expect(registerCall.orgSponsoredFreeFamilyPlanToken).toEqual( - orgSponsoredFreeFamilyPlanToken, - ); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an emergency access invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - acceptEmergencyAccessInviteToken, - emergencyAccessId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: emergency access fields are populated - expect(registerCall.acceptEmergencyAccessInviteToken).toEqual( - acceptEmergencyAccessInviteToken, - ); - expect(registerCall.acceptEmergencyAccessId).toEqual(emergencyAccessId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given a provider invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - undefined, - undefined, - providerInviteToken, - providerUserId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: provider invite fields are populated - expect(registerCall.providerInviteToken).toEqual(providerInviteToken); - expect(registerCall.providerUserId).toEqual(providerUserId); - - expect(registerCall).toMatchSnapshot(); - }); + it("derives the master key and registers the user", async () => { + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + + // Verify master key is derived internally + expect(keyService.makeMasterKey).toHaveBeenCalledWith( + passwordInputResult.newPassword, + passwordInputResult.salt, + passwordInputResult.kdfConfig, + ); + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: emailVerificationToken is populated + expect(registerCall.emailVerificationToken).toEqual(emailVerificationToken); + + expect(registerCall).toMatchSnapshot(); + }); + + it("it registers the user with org invite when given an org invite", async () => { + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + await service.finishRegistration(email, passwordInputResult); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: org invite fields are populated + expect(registerCall.orgInviteToken).toEqual(orgInvite.token); + expect(registerCall.organizationUserId).toEqual(orgInvite.organizationUserId); + + expect(registerCall).toMatchSnapshot(); }); - describe("when feature flag is ON (new API)", () => { - beforeEach(() => { - // When the Auth flag is ON, InputPasswordComponent emits newApisWithInputPasswordFlagEnabled: true - // and does NOT emit newMasterKey, newServerMasterKeyHash. - passwordInputResult = { - newPassword: "newPassword", - kdfConfig: DEFAULT_KDF_CONFIG, - newPasswordHint: "newPasswordHint", - newApisWithInputPasswordFlagEnabled: true, - salt: salt, - }; - - // The service derives the master key internally when the Auth flag is ON - keyService.makeMasterKey.mockResolvedValue(masterKey); - - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); - }); - - it("derives the master key and registers the user with new data types", async () => { - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - // Verify master key is derived internally - expect(keyService.makeMasterKey).toHaveBeenCalledWith( - passwordInputResult.newPassword, - passwordInputResult.salt, - passwordInputResult.kdfConfig, - ); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - - // New API sends structured authentication and unlock data - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Old API flat fields must NOT be present - expect((registerCall as any).masterPasswordHash).toBeUndefined(); - expect((registerCall as any).userSymmetricKey).toBeUndefined(); - expect((registerCall as any).kdf).toBeUndefined(); - expect((registerCall as any).kdfIterations).toBeUndefined(); - - // Unique to this flow: emailVerificationToken is populated - expect(registerCall.emailVerificationToken).toEqual(emailVerificationToken); - - expect(registerCall).toMatchSnapshot(); - }); - - it("it registers the user with org invite when given an org invite", async () => { - organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); - - await service.finishRegistration(email, passwordInputResult); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: org invite fields are populated - expect(registerCall.orgInviteToken).toEqual(orgInvite.token); - expect(registerCall.organizationUserId).toEqual(orgInvite.organizationUserId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an org sponsored free family plan token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - orgSponsoredFreeFamilyPlanToken, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: org sponsored free family plan token is populated - expect(registerCall.orgSponsoredFreeFamilyPlanToken).toEqual( - orgSponsoredFreeFamilyPlanToken, - ); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an emergency access invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - acceptEmergencyAccessInviteToken, - emergencyAccessId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: emergency access fields are populated - expect(registerCall.acceptEmergencyAccessInviteToken).toEqual( - acceptEmergencyAccessInviteToken, - ); - expect(registerCall.acceptEmergencyAccessId).toEqual(emergencyAccessId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given a provider invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - undefined, - undefined, - providerInviteToken, - providerUserId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: provider invite fields are populated - expect(registerCall.providerInviteToken).toEqual(providerInviteToken); - expect(registerCall.providerUserId).toEqual(providerUserId); - - expect(registerCall).toMatchSnapshot(); - }); + it("registers the user when given an org sponsored free family plan token", async () => { + await service.finishRegistration( + email, + passwordInputResult, + undefined, + orgSponsoredFreeFamilyPlanToken, + ); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: org sponsored free family plan token is populated + expect(registerCall.orgSponsoredFreeFamilyPlanToken).toEqual(orgSponsoredFreeFamilyPlanToken); + + expect(registerCall).toMatchSnapshot(); + }); + + it("registers the user when given an emergency access invite token", async () => { + await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + acceptEmergencyAccessInviteToken, + emergencyAccessId, + ); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: emergency access fields are populated + expect(registerCall.acceptEmergencyAccessInviteToken).toEqual( + acceptEmergencyAccessInviteToken, + ); + expect(registerCall.acceptEmergencyAccessId).toEqual(emergencyAccessId); + + expect(registerCall).toMatchSnapshot(); + }); + + it("registers the user when given a provider invite token", async () => { + await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + undefined, + undefined, + providerInviteToken, + providerUserId, + ); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: provider invite fields are populated + expect(registerCall.providerInviteToken).toEqual(providerInviteToken); + expect(registerCall.providerUserId).toEqual(providerUserId); + + expect(registerCall).toMatchSnapshot(); }); }); }); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index 10fdde602064..354bf6145487 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -12,15 +12,10 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; -import { - EncryptedString, - EncString, -} from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; @@ -33,13 +28,12 @@ export class WebRegistrationFinishService protected keyService: KeyService, protected accountApiService: AccountApiService, protected masterPasswordService: MasterPasswordServiceAbstraction, - protected configService: ConfigService, private organizationInviteService: OrganizationInviteService, private policyApiService: PolicyApiServiceAbstraction, private logService: LogService, private policyService: PolicyService, ) { - super(keyService, accountApiService, masterPasswordService, configService); + super(keyService, accountApiService, masterPasswordService); } override async getOrgNameFromOrgInvite(): Promise { @@ -87,7 +81,6 @@ export class WebRegistrationFinishService newUserKey: UserKey, email: string, passwordInputResult: PasswordInputResult, - encryptedUserKey: EncryptedString, userAsymmetricKeys: [string, EncString], emailVerificationToken?: string, orgSponsoredFreeFamilyPlanToken?: string, @@ -95,12 +88,11 @@ export class WebRegistrationFinishService emergencyAccessId?: string, providerInviteToken?: string, providerUserId?: string, - ): Promise { + ): Promise { const registerRequest = await super.buildRegisterRequest( newUserKey, email, passwordInputResult, - encryptedUserKey, userAsymmetricKeys, emailVerificationToken, ); 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 6ec2faf87863..f43dc766152a 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 @@ -17,14 +17,12 @@ import { } 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"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey, MasterKey, UserPrivateKey } from "@bitwarden/common/types/key"; +import { UserKey, UserPrivateKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { newGuid } from "@bitwarden/guid"; import { @@ -32,7 +30,6 @@ import { DEFAULT_KDF_CONFIG, KdfType, KeyService, - PBKDF2KdfConfig, } from "@bitwarden/key-management"; import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; @@ -58,7 +55,6 @@ describe("EmergencyAccessService", () => { let logService: MockProxy; let emergencyAccessService: EmergencyAccessService; let masterPasswordService: MockProxy; - let configService: MockProxy; const mockNewUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("trustedPublicKey")]; @@ -72,7 +68,6 @@ describe("EmergencyAccessService", () => { cipherService = mock(); logService = mock(); masterPasswordService = mock(); - configService = mock(); emergencyAccessService = new EmergencyAccessService( emergencyAccessApiService, @@ -82,7 +77,6 @@ describe("EmergencyAccessService", () => { cipherService, logService, masterPasswordService, - configService, ); }); @@ -236,255 +230,7 @@ describe("EmergencyAccessService", () => { }); }); - /** - * @deprecated This 'describe' to be removed in PM-28143. When you remove this, check also if there are any imports/properties - * in the test setup above that are now un-used and can also be removed. - */ - describe("takeover [PM27086_UpdateAuthenticationApisForInputPassword flag DISABLED]", () => { - const PM27086_UpdateAuthenticationApisForInputPasswordEnabled = false; - - const params = { - id: "emergencyAccessId", - masterPassword: "mockPassword", - email: "emergencyAccessEmail", - activeUserId: Utils.newGuid() as UserId, - }; - - const takeoverResponse = { - keyEncrypted: "EncryptedKey", - kdf: KdfType.PBKDF2_SHA256, - kdfIterations: 500, - } as EmergencyAccessTakeoverResponse; - - const userPrivateKey = new Uint8Array(64) as UserPrivateKey; - const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; - const mockMasterKeyHash = "mockMasterKeyHash"; - let mockGrantorUserKey: UserKey; - - // must mock [UserKey, EncString] return from keyService.encryptUserKeyWithMasterKey - // where UserKey is the decrypted grantor user key - const mockMasterKeyEncryptedUserKey = new EncString( - EncryptionType.AesCbc256_HmacSha256_B64, - "mockMasterKeyEncryptedUserKey", - ); - - beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue( - PM27086_UpdateAuthenticationApisForInputPasswordEnabled, - ); - - emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce(takeoverResponse); - keyService.userPrivateKey$.mockReturnValue(of(userPrivateKey)); - - const mockDecryptedGrantorUserKey = new SymmetricCryptoKey(new Uint8Array(64)); - encryptService.decapsulateKeyUnsigned.mockResolvedValueOnce(mockDecryptedGrantorUserKey); - mockGrantorUserKey = mockDecryptedGrantorUserKey as UserKey; - - keyService.makeMasterKey.mockResolvedValueOnce(mockMasterKey); - keyService.hashMasterKey.mockResolvedValueOnce(mockMasterKeyHash); - keyService.encryptUserKeyWithMasterKey.mockResolvedValueOnce([ - mockGrantorUserKey, - mockMasterKeyEncryptedUserKey, - ]); - }); - - it("posts a new password when decryption succeeds", async () => { - // Arrange - const expectedKdfConfig = new PBKDF2KdfConfig(takeoverResponse.kdfIterations); - - const expectedEmergencyAccessPasswordRequest = new EmergencyAccessPasswordRequest(); - expectedEmergencyAccessPasswordRequest.newMasterPasswordHash = mockMasterKeyHash; - expectedEmergencyAccessPasswordRequest.key = mockMasterKeyEncryptedUserKey.encryptedString; - - // Act - await emergencyAccessService.takeover( - params.id, - params.masterPassword, - params.email, - params.activeUserId, - ); - - // Assert - expect(keyService.userPrivateKey$).toHaveBeenCalledWith(params.activeUserId); - expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledWith( - new EncString(takeoverResponse.keyEncrypted), - userPrivateKey, - ); - expect(keyService.makeMasterKey).toHaveBeenCalledWith( - params.masterPassword, - params.email, - expectedKdfConfig, - ); - expect(keyService.hashMasterKey).toHaveBeenCalledWith(params.masterPassword, mockMasterKey); - expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - mockGrantorUserKey, - ); - expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith( - params.id, - expectedEmergencyAccessPasswordRequest, - ); - }); - - it("uses argon2 KDF if takeover response is argon2", async () => { - const argon2TakeoverResponse = { - keyEncrypted: "EncryptedKey", - kdf: KdfType.Argon2id, - kdfIterations: 3, - kdfMemory: 64, - kdfParallelism: 4, - } as EmergencyAccessTakeoverResponse; - emergencyAccessApiService.postEmergencyAccessTakeover.mockReset(); - emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce( - argon2TakeoverResponse, - ); - - const expectedKdfConfig = new Argon2KdfConfig( - argon2TakeoverResponse.kdfIterations, - argon2TakeoverResponse.kdfMemory, - argon2TakeoverResponse.kdfParallelism, - ); - - const expectedEmergencyAccessPasswordRequest = new EmergencyAccessPasswordRequest(); - expectedEmergencyAccessPasswordRequest.newMasterPasswordHash = mockMasterKeyHash; - expectedEmergencyAccessPasswordRequest.key = mockMasterKeyEncryptedUserKey.encryptedString; - - await emergencyAccessService.takeover( - params.id, - params.masterPassword, - params.email, - params.activeUserId, - ); - - expect(keyService.userPrivateKey$).toHaveBeenCalledWith(params.activeUserId); - expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledWith( - new EncString(argon2TakeoverResponse.keyEncrypted), - userPrivateKey, - ); - expect(keyService.makeMasterKey).toHaveBeenCalledWith( - params.masterPassword, - params.email, - expectedKdfConfig, - ); - expect(keyService.hashMasterKey).toHaveBeenCalledWith(params.masterPassword, mockMasterKey); - expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - mockGrantorUserKey, - ); - expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith( - params.id, - expectedEmergencyAccessPasswordRequest, - ); - }); - - it("throws an error if masterKeyEncryptedUserKey is not found", async () => { - keyService.encryptUserKeyWithMasterKey.mockReset(); - keyService.encryptUserKeyWithMasterKey.mockResolvedValueOnce(null); - const expectedKdfConfig = new PBKDF2KdfConfig(takeoverResponse.kdfIterations); - - await expect( - emergencyAccessService.takeover( - params.id, - params.masterPassword, - params.email, - params.activeUserId, - ), - ).rejects.toThrow("masterKeyEncryptedUserKey not found"); - - expect(keyService.userPrivateKey$).toHaveBeenCalledWith(params.activeUserId); - expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledWith( - new EncString(takeoverResponse.keyEncrypted), - userPrivateKey, - ); - expect(keyService.makeMasterKey).toHaveBeenCalledWith( - params.masterPassword, - params.email, - expectedKdfConfig, - ); - expect(keyService.hashMasterKey).toHaveBeenCalledWith(params.masterPassword, mockMasterKey); - expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - mockGrantorUserKey, - ); - expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); - }); - - it("should not post a new password if decryption fails", async () => { - emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce(takeoverResponse); - encryptService.decapsulateKeyUnsigned.mockReset(); - encryptService.decapsulateKeyUnsigned.mockResolvedValueOnce(null); - - await expect( - emergencyAccessService.takeover( - params.id, - params.masterPassword, - params.email, - params.activeUserId, - ), - ).rejects.toThrow("Failed to decrypt grantor key"); - - expect(keyService.userPrivateKey$).toHaveBeenCalledWith(params.activeUserId); - expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledWith( - new EncString(takeoverResponse.keyEncrypted), - userPrivateKey, - ); - expect(keyService.makeMasterKey).not.toHaveBeenCalled(); - expect(keyService.hashMasterKey).not.toHaveBeenCalled(); - expect(keyService.encryptUserKeyWithMasterKey).not.toHaveBeenCalled(); - expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); - }); - - it("should not post a new password if decryption throws", async () => { - encryptService.decapsulateKeyUnsigned.mockReset(); - encryptService.decapsulateKeyUnsigned.mockImplementationOnce(() => { - throw new Error("Failed to unwrap grantor key"); - }); - - await expect( - emergencyAccessService.takeover( - params.id, - params.masterPassword, - params.email, - params.activeUserId, - ), - ).rejects.toThrow("Failed to unwrap grantor key"); - - expect(keyService.userPrivateKey$).toHaveBeenCalledWith(params.activeUserId); - expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledWith( - new EncString(takeoverResponse.keyEncrypted), - userPrivateKey, - ); - expect(keyService.makeMasterKey).not.toHaveBeenCalled(); - expect(keyService.hashMasterKey).not.toHaveBeenCalled(); - expect(keyService.encryptUserKeyWithMasterKey).not.toHaveBeenCalled(); - expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); - }); - - it("should throw an error if the users private key cannot be retrieved", async () => { - keyService.userPrivateKey$.mockReturnValue(of(null)); - - await expect( - emergencyAccessService.takeover( - params.id, - params.masterPassword, - params.email, - params.activeUserId, - ), - ).rejects.toThrow("user does not have a private key"); - - expect(keyService.userPrivateKey$).toHaveBeenCalledWith(params.activeUserId); - expect(encryptService.decapsulateKeyUnsigned).not.toHaveBeenCalled(); - expect(keyService.makeMasterKey).not.toHaveBeenCalled(); - expect(keyService.hashMasterKey).not.toHaveBeenCalled(); - expect(keyService.encryptUserKeyWithMasterKey).not.toHaveBeenCalled(); - expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); - }); - }); - - describe("takeover [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { - // Mock feature flag value - const PM27086_UpdateAuthenticationApisForInputPasswordEnabled = true; - + describe("takeover", () => { // Mock sut method params const id = "emergency-access-id"; const masterPassword = "mockPassword"; @@ -507,10 +253,6 @@ describe("EmergencyAccessService", () => { let unlockData: MasterPasswordUnlockData; beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue( - PM27086_UpdateAuthenticationApisForInputPasswordEnabled, - ); - emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValue(takeoverResponse); keyService.userPrivateKey$.mockReturnValue(of(activeUserPrivateKey)); diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 9c66974ab000..2993b906bdf3 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -4,7 +4,6 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, @@ -16,7 +15,6 @@ import { MasterPasswordSalt, MasterPasswordUnlockData, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; @@ -65,7 +63,6 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide private cipherService: CipherService, private logService: LogService, private masterPasswordService: MasterPasswordServiceAbstraction, - private configService: ConfigService, ) {} /** @@ -319,57 +316,33 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide break; } - // When you unwind the flag in PM-28143, also remove the ConfigService if it is un-used. - const newApisWithInputPasswordFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword, - ); - - if (newApisWithInputPasswordFlagEnabled) { - // Prefer server-provided salt from the takeover response. - // Falls back to email-derived salt for backward compatibility with servers - // that don't yet include Salt in the response (PM-31636). - // - // TODO: PM-32059 — When salt is fully disconnected from email (Stage 3), - // the email fallback will be removed and server salt becomes mandatory. - const salt: MasterPasswordSalt = - typeof takeoverResponse.salt === "string" - ? (takeoverResponse.salt as MasterPasswordSalt) - : this.masterPasswordService.emailToSalt(email); - - const authenticationData: MasterPasswordAuthenticationData = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - masterPassword, - config, - salt, - ); - - const unlockData: MasterPasswordUnlockData = - await this.masterPasswordService.makeMasterPasswordUnlockData( - masterPassword, - config, - salt, - grantorUserKey, - ); - - const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData); - - await this.emergencyAccessApiService.postEmergencyAccessPassword(id, request); - - return; // EARLY RETURN for flagged logic - } - - const masterKey = await this.keyService.makeMasterKey(masterPassword, email, config); - const masterKeyHash = await this.keyService.hashMasterKey(masterPassword, masterKey); - - const encKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey); + // Prefer server-provided salt from the takeover response. + // Falls back to email-derived salt for backward compatibility with servers + // that don't yet include Salt in the response (PM-31636). + // + // TODO: PM-32059 — When salt is fully disconnected from email (Stage 3), + // the email fallback will be removed and server salt becomes mandatory. + const salt: MasterPasswordSalt = + typeof takeoverResponse.salt === "string" + ? (takeoverResponse.salt as MasterPasswordSalt) + : this.masterPasswordService.emailToSalt(email); + + const authenticationData: MasterPasswordAuthenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + masterPassword, + config, + salt, + ); - if (encKey == null || !encKey[1].encryptedString) { - throw new Error("masterKeyEncryptedUserKey not found"); - } + const unlockData: MasterPasswordUnlockData = + await this.masterPasswordService.makeMasterPasswordUnlockData( + masterPassword, + config, + salt, + grantorUserKey, + ); - const request = new EmergencyAccessPasswordRequest(); - request.newMasterPasswordHash = masterKeyHash; - request.key = encKey[1].encryptedString; + const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData); await this.emergencyAccessApiService.postEmergencyAccessPassword(id, request); } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index c8a65f0377af..424f35fdc2f5 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -294,7 +294,6 @@ const safeProviders: SafeProvider[] = [ KeyServiceAbstraction, AccountApiServiceAbstraction, MasterPasswordServiceAbstraction, - ConfigService, OrganizationInviteService, PolicyApiServiceAbstraction, LogService, diff --git a/libs/angular/src/auth/password-management/change-password/change-password.component.ts b/libs/angular/src/auth/password-management/change-password/change-password.component.ts index 57ae84049233..e9c5b38e32d1 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.component.ts +++ b/libs/angular/src/auth/password-management/change-password/change-password.component.ts @@ -23,7 +23,6 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key- import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; import { AnonLayoutWrapperDataService, @@ -82,7 +81,6 @@ export class ChangePasswordComponent implements OnInit { private messagingService: MessagingService, private policyService: PolicyService, private toastService: ToastService, - private syncService: SyncService, private dialogService: DialogService, private logService: LogService, private logoutService: LogoutService, @@ -159,31 +157,10 @@ export class ChangePasswordComponent implements OnInit { throw new Error("activeAccount not found"); } - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - await this.changePasswordService.changePasswordAndRotateUserKey( - passwordInputResult, - this.activeAccount, - ); - this.passwordChanged.emit(); - return; // EARLY RETURN for flagged logic - } - - if ( - passwordInputResult.currentPassword == null || - passwordInputResult.newPasswordHint == null - ) { - throw new Error("currentPassword or newPasswordHint not found"); - } - - await this.syncService.fullSync(true); - - await this.changePasswordService.rotateUserKeyMasterPasswordAndEncryptedData( - passwordInputResult.currentPassword, - passwordInputResult.newPassword, + await this.changePasswordService.changePasswordAndRotateUserKey( + passwordInputResult, this.activeAccount, - passwordInputResult.newPasswordHint, ); - this.passwordChanged.emit(); } else { if (!this.userId) { @@ -210,17 +187,13 @@ export class ChangePasswordComponent implements OnInit { this.passwordChanged.emit(); - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - // TODO: investigate refactoring logout and follow-up routing in https://bitwarden.atlassian.net/browse/PM-32660 - await this.logoutService.logout(this.userId); + // TODO: investigate refactoring logout and follow-up routing in https://bitwarden.atlassian.net/browse/PM-32660 + await this.logoutService.logout(this.userId); - const shouldNavigateToRoot = this.changePasswordService.shouldNavigateToRoot(); - if (shouldNavigateToRoot) { - // navigate to root so redirect guard can properly route next active user (account switching) or null user to correct page - await this.router.navigate(["/"]); - } - } else { - this.messagingService.send("logout"); + const shouldNavigateToRoot = this.changePasswordService.shouldNavigateToRoot(); + if (shouldNavigateToRoot) { + // navigate to root so redirect guard can properly route next active user (account switching) or null user to correct page + await this.router.navigate(["/"]); } // Close the popout if we are in a browser extension popout. diff --git a/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts b/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts index 92b14c59605d..19f7a3937899 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts +++ b/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts @@ -27,26 +27,6 @@ export abstract class ChangePasswordService { user: Account, ): Promise; - /** - * @deprecated To be removed in PM-28143 - * - * Creates a new user key and re-encrypts all required data with it. - * - does so by calling the underlying method on the `UserKeyRotationService` - * - implemented in Web only - * - * @param currentPassword the current password - * @param newPassword the new password - * @param user the user account - * @param newPasswordHint the new password hint - * @throws if called from a non-Web client - */ - abstract rotateUserKeyMasterPasswordAndEncryptedData( - currentPassword: string, - newPassword: string, - user: Account, - newPasswordHint: string, - ): Promise; - /** * Changes the user's password by building a `PasswordRequest` object that gets POSTed to the server. * @@ -55,18 +35,6 @@ export abstract class ChangePasswordService { * @throws if required values are not found on the `PasswordInputResult` * @throws an `InvalidCurrentPasswordError` if `proofOfDecryption` fails (i.e. if the current password is incorrect) * @throws if there is an error during the API call - * - * OLD DESCRIPTION FOR UNFLAGGED LOGIC: (the rest of this JSDoc below can be removed in PM-28143) - * - * Changes the user's password and re-encrypts the user key with the `newMasterKey`. - * - Specifically, this method uses credentials from the `passwordInputResult` to: - * 1. Decrypt the user key with the `currentMasterKey` - * 2. Re-encrypt that user key with the `newMasterKey`, resulting in a `newMasterKeyEncryptedUserKey` - * 3. Build a `PasswordRequest` object that gets POSTed to `"/accounts/password"` - * - * @param passwordInputResult credentials object received from the `InputPasswordComponent` - * @param userId the `userId` - * @throws if the `userId`, `currentMasterKey`, or `currentServerMasterKeyHash` is not found */ abstract changePassword(passwordInputResult: PasswordInputResult, userId: UserId): Promise; @@ -83,17 +51,6 @@ export abstract class ChangePasswordService { * @throws if required values are not found on the `PasswordInputResult` * @throws an `InvalidCurrentPasswordError` if `proofOfDecryption` fails (i.e. if the current password is incorrect) * @throws if there is an error during the API call - * - * OLD DESCRIPTION FOR UNFLAGGED LOGIC: (the rest of this JSDoc below can be removed in PM-28143) - * - * Changes the user's password and re-encrypts the user key with the `newMasterKey`. - * - Specifically, this method uses credentials from the `passwordInputResult` to: - * 1. Decrypt the user key with the `currentMasterKey` - * 2. Re-encrypt that user key with the `newMasterKey`, resulting in a `newMasterKeyEncryptedUserKey` - * 3. Build a `PasswordRequest` object that gets PUTed to `"/accounts/update-temp-password"` so that the - * ForcePasswordReset gets set to false. - * @param passwordInputResult - * @param userId */ abstract changePasswordForAccountRecovery( passwordInputResult: PasswordInputResult, diff --git a/libs/angular/src/auth/password-management/change-password/default-change-password.service.spec.ts b/libs/angular/src/auth/password-management/change-password/default-change-password.service.spec.ts index 6012a878e2c9..68f61be57e1d 100644 --- a/libs/angular/src/auth/password-management/change-password/default-change-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/change-password/default-change-password.service.spec.ts @@ -8,7 +8,6 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { @@ -18,11 +17,10 @@ import { MasterPasswordSalt, MasterPasswordUnlockData, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { makeSymmetricCryptoKey, mockAccountInfoWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; +import { UserKey } from "@bitwarden/common/types/key"; +import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; import { ChangePasswordService, @@ -49,25 +47,6 @@ describe("DefaultChangePasswordService", () => { }), }; - const passwordInputResult: PasswordInputResult = { - currentMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, - currentServerMasterKeyHash: "currentServerMasterKeyHash", - - newPassword: "newPassword", - newPasswordHint: "newPasswordHint", - newMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, - newServerMasterKeyHash: "newServerMasterKeyHash", - - kdfConfig: PBKDF2KdfConfig.createDefault(), - newApisWithInputPasswordFlagEnabled: false, - }; - - const decryptedUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; - const newMasterKeyEncryptedUserKey: [UserKey, EncString] = [ - decryptedUserKey, - { encryptedString: "newMasterKeyEncryptedUserKey" } as EncString, - ]; - beforeEach(() => { keyService = mock(); masterPasswordApiService = mock(); @@ -80,9 +59,6 @@ describe("DefaultChangePasswordService", () => { masterPasswordService, masterPasswordUnlockService, ); - - masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(decryptedUserKey); - keyService.encryptUserKeyWithMasterKey.mockResolvedValue(newMasterKeyEncryptedUserKey); }); describe("changePasswordAndRotateUserKey()", () => { @@ -97,7 +73,6 @@ describe("DefaultChangePasswordService", () => { newPasswordHint: "new-password-hint", kdfConfig: DEFAULT_KDF_CONFIG, salt: "salt" as MasterPasswordSalt, - newApisWithInputPasswordFlagEnabled: true, }; }); @@ -112,7 +87,7 @@ describe("DefaultChangePasswordService", () => { }); }); - describe("changePassword() and changePasswordForAccountRecovery() [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { + describe("changePassword() and changePasswordForAccountRecovery()", () => { // Mock method params let passwordInputResult: PasswordInputResult; @@ -129,22 +104,21 @@ describe("DefaultChangePasswordService", () => { newPasswordHint: "new-password-hint", kdfConfig: DEFAULT_KDF_CONFIG, salt: "salt" as MasterPasswordSalt, - newApisWithInputPasswordFlagEnabled: true, }; // Mock method data userKey = makeSymmetricCryptoKey(64) as UserKey; newAuthenticationData = { - salt: passwordInputResult.salt, - kdf: passwordInputResult.kdfConfig, + salt: passwordInputResult.salt!, + kdf: passwordInputResult.kdfConfig!, masterPasswordAuthenticationHash: "newMasterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, }; newUnlockData = { - salt: passwordInputResult.salt, - kdf: passwordInputResult.kdfConfig, + salt: passwordInputResult.salt!, + kdf: passwordInputResult.kdfConfig!, masterKeyWrappedUserKey: "newMasterKeyWrappedUserKey" as MasterKeyWrappedUserKey, } as MasterPasswordUnlockData; @@ -160,8 +134,8 @@ describe("DefaultChangePasswordService", () => { beforeEach(() => { currentAuthenticationData = { - salt: passwordInputResult.salt, - kdf: passwordInputResult.kdfConfig, + salt: passwordInputResult.salt!, + kdf: passwordInputResult.kdfConfig!, masterPasswordAuthenticationHash: "currentMasterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, }; @@ -170,7 +144,7 @@ describe("DefaultChangePasswordService", () => { currentAuthenticationData.masterPasswordAuthenticationHash, newAuthenticationData, newUnlockData, - passwordInputResult.newPasswordHint, + passwordInputResult.newPasswordHint!, ); masterPasswordService.makeMasterPasswordAuthenticationData @@ -285,7 +259,7 @@ describe("DefaultChangePasswordService", () => { request = UpdateTempPasswordRequest.newConstructorWithHint( newAuthenticationData, newUnlockData, - passwordInputResult.newPasswordHint, + passwordInputResult.newPasswordHint!, ); masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( @@ -401,166 +375,4 @@ describe("DefaultChangePasswordService", () => { expect(shouldNavigateToRoot).toBe(false); }); }); - - /** - * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties - * in the test setup above that are now un-used and can also be removed. - */ - describe("changePassword() [PM27086_UpdateAuthenticationApisForInputPassword flag DISABLED]", () => { - it("should call the postPassword() API method with a the correct PasswordRequest credentials", async () => { - // Act - await sut.changePassword(passwordInputResult, userId); - - // Assert - expect(masterPasswordApiService.postPassword).toHaveBeenCalledWith( - expect.objectContaining({ - masterPasswordHash: passwordInputResult.currentServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - newMasterPasswordHash: passwordInputResult.newServerMasterKeyHash, - key: newMasterKeyEncryptedUserKey[1].encryptedString, - }), - ); - }); - - it("should call decryptUserKeyWithMasterKey and encryptUserKeyWithMasterKey", async () => { - // Act - await sut.changePassword(passwordInputResult, userId); - - // Assert - expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - passwordInputResult.currentMasterKey, - userId, - ); - expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith( - passwordInputResult.newMasterKey, - decryptedUserKey, - ); - }); - - it("should throw if a userId was not found", async () => { - // Arrange - const userId: null = null; - - // Act - const testFn = sut.changePassword(passwordInputResult, userId); - - // Assert - await expect(testFn).rejects.toThrow("userId not found"); - }); - - it("should throw if a currentMasterKey was not found", async () => { - // Arrange - const incorrectPasswordInputResult = { ...passwordInputResult }; - incorrectPasswordInputResult.currentMasterKey = undefined; - - // Act - const testFn = sut.changePassword(incorrectPasswordInputResult, userId); - - // Assert - await expect(testFn).rejects.toThrow( - "invalid PasswordInputResult credentials, could not change password", - ); - }); - - it("should throw if a currentServerMasterKeyHash was not found", async () => { - // Arrange - const incorrectPasswordInputResult = { ...passwordInputResult }; - incorrectPasswordInputResult.currentServerMasterKeyHash = undefined; - - // Act - const testFn = sut.changePassword(incorrectPasswordInputResult, userId); - - // Assert - await expect(testFn).rejects.toThrow( - "invalid PasswordInputResult credentials, could not change password", - ); - }); - - it("should throw an error if user key decryption fails", async () => { - // Arrange - masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(null); - - // Act - const testFn = sut.changePassword(passwordInputResult, userId); - - // Assert - await expect(testFn).rejects.toThrow("Could not decrypt user key"); - }); - - it("should throw an error if postPassword() fails", async () => { - // Arrange - masterPasswordApiService.postPassword.mockRejectedValueOnce(new Error("error")); - - // Act - const testFn = sut.changePassword(passwordInputResult, userId); - - // Assert - await expect(testFn).rejects.toThrow("Could not change password"); - expect(masterPasswordApiService.postPassword).toHaveBeenCalled(); - }); - }); - - /** - * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties - * in the test setup above that are now un-used and can also be removed. - */ - describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => { - it("should throw an error (the method is only implemented in Web)", async () => { - // Act - const promise = sut.rotateUserKeyMasterPasswordAndEncryptedData( - "currentPassword", - "newPassword", - user, - "newPasswordHint", - ); - - // Assert - await expect(promise).rejects.toThrow( - "rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web", - ); - }); - }); - - /** - * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties - * in the test setup above that are now un-used and can also be removed. - */ - describe("changePasswordForAccountRecovery() [PM27086_UpdateAuthenticationApisForInputPassword flag DISABLED]", () => { - it("should call the putUpdateTempPassword() API method with the correct UpdateTempPasswordRequest credentials", async () => { - // Act - await sut.changePasswordForAccountRecovery(passwordInputResult, userId); - - // Assert - expect(masterPasswordApiService.putUpdateTempPassword).toHaveBeenCalledWith( - expect.objectContaining({ - newMasterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - key: newMasterKeyEncryptedUserKey[1].encryptedString, - }), - ); - }); - - it("should throw an error if user key decryption fails", async () => { - // Arrange - masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(null); - - // Act - const testFn = sut.changePasswordForAccountRecovery(passwordInputResult, userId); - - // Assert - await expect(testFn).rejects.toThrow("Could not decrypt user key"); - }); - - it("should throw an error if putUpdateTempPassword() fails", async () => { - // Arrange - masterPasswordApiService.putUpdateTempPassword.mockRejectedValueOnce(new Error("error")); - - // Act - const testFn = sut.changePasswordForAccountRecovery(passwordInputResult, userId); - - // Assert - await expect(testFn).rejects.toThrow("Could not change password"); - expect(masterPasswordApiService.putUpdateTempPassword).toHaveBeenCalled(); - }); - }); }); diff --git a/libs/angular/src/auth/password-management/change-password/default-change-password.service.ts b/libs/angular/src/auth/password-management/change-password/default-change-password.service.ts index fb4fe8a86de6..7fac3eb68b5d 100644 --- a/libs/angular/src/auth/password-management/change-password/default-change-password.service.ts +++ b/libs/angular/src/auth/password-management/change-password/default-change-password.service.ts @@ -6,7 +6,6 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request"; import { assertNonNullish, assertTruthy } from "@bitwarden/common/auth/utils"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { @@ -39,187 +38,90 @@ export class DefaultChangePasswordService implements ChangePasswordService { throw new Error("changePasswordAndRotateUserKey() is only implemented in Web"); } - async rotateUserKeyMasterPasswordAndEncryptedData( - currentPassword: string, - newPassword: string, - user: Account, - hint: string, - ): Promise { - throw new Error("rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web"); - } - - /** - * @deprecated To be removed in PM-28143. - */ - private async preparePasswordChange( - passwordInputResult: PasswordInputResult, - userId: UserId | null, - request: PasswordRequest | UpdateTempPasswordRequest, - ): Promise<[UserKey, EncString]> { - if (!userId) { - throw new Error("userId not found"); - } - if ( - !passwordInputResult.currentMasterKey || - !passwordInputResult.currentServerMasterKeyHash || - !passwordInputResult.newMasterKey || - !passwordInputResult.newServerMasterKeyHash || - passwordInputResult.newPasswordHint == null - ) { - throw new Error("invalid PasswordInputResult credentials, could not change password"); - } - - const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - passwordInputResult.currentMasterKey, + async changePassword(passwordInputResult: PasswordInputResult, userId: UserId) { + const context = "Could not change password."; + assertTruthy(passwordInputResult.currentPassword, "currentPassword", context); + assertTruthy(passwordInputResult.newPassword, "newPassword", context); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", context); + assertTruthy(passwordInputResult.salt, "salt", context); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", context); // can have an empty string as a meaningful value, so check non-nullish + + // Verify that the current password is correct + const currentPasswordVerified = await this.masterPasswordUnlockService.proofOfDecryption( + passwordInputResult.currentPassword, userId, ); - if (decryptedUserKey == null) { - throw new Error("Could not decrypt user key"); + if (!currentPasswordVerified) { + throw new InvalidCurrentPasswordError(); } - const newKeyValue = await this.keyService.encryptUserKeyWithMasterKey( - passwordInputResult.newMasterKey, - decryptedUserKey, - ); + // Current password has been verified, so get the user key from state + const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey"); - if (request instanceof PasswordRequest) { - request.masterPasswordHash = passwordInputResult.currentServerMasterKeyHash; - request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash; - request.masterPasswordHint = passwordInputResult.newPasswordHint; - } else if (request instanceof UpdateTempPasswordRequest) { - request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash; - request.masterPasswordHint = passwordInputResult.newPasswordHint; - } - - return newKeyValue; - } - - async changePassword(passwordInputResult: PasswordInputResult, userId: UserId) { - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - const context = "Could not change password."; - assertTruthy(passwordInputResult.currentPassword, "currentPassword", context); - assertTruthy(passwordInputResult.newPassword, "newPassword", context); - assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", context); - assertTruthy(passwordInputResult.salt, "salt", context); - assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", context); // can have an empty string as a meaningful value, so check non-nullish - - // Verify that the current password is correct - const currentPasswordVerified = await this.masterPasswordUnlockService.proofOfDecryption( + // Use current password to make current auth data so we can send the current auth hash to the server + const currentAuthenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( passwordInputResult.currentPassword, - userId, - ); - - if (!currentPasswordVerified) { - throw new InvalidCurrentPasswordError(); - } - - // Current password has been verified, so get the user key from state - const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey"); - - // Use current password to make current auth data so we can send the current auth hash to the server - const currentAuthenticationData = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - passwordInputResult.currentPassword, - passwordInputResult.kdfConfig, - passwordInputResult.salt, - ); - - // Use new password to make new auth and unlock data that we can send to the server - const { newAuthenticationData, newUnlockData } = await this.makeNewAuthAndUnlockData( - passwordInputResult.newPassword, passwordInputResult.kdfConfig, passwordInputResult.salt, - userKey, - ); - - const request = PasswordRequest.newConstructor( - currentAuthenticationData.masterPasswordAuthenticationHash, - newAuthenticationData, - newUnlockData, - passwordInputResult.newPasswordHint, ); - await this.masterPasswordApiService.postPassword(request); - - return; // EARLY RETURN for flagged logic - } - - const request = new PasswordRequest(); - - const newMasterKeyEncryptedUserKey = await this.preparePasswordChange( - passwordInputResult, - userId, - request, + // Use new password to make new auth and unlock data that we can send to the server + const { newAuthenticationData, newUnlockData } = await this.makeNewAuthAndUnlockData( + passwordInputResult.newPassword, + passwordInputResult.kdfConfig, + passwordInputResult.salt, + userKey, ); - request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string; + const request = PasswordRequest.newConstructor( + currentAuthenticationData.masterPasswordAuthenticationHash, + newAuthenticationData, + newUnlockData, + passwordInputResult.newPasswordHint, + ); - try { - await this.masterPasswordApiService.postPassword(request); - } catch { - throw new Error("Could not change password"); - } + await this.masterPasswordApiService.postPassword(request); } async changePasswordForAccountRecovery(passwordInputResult: PasswordInputResult, userId: UserId) { - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - const context = "Could not change password for account recovery."; - assertTruthy(passwordInputResult.currentPassword, "currentPassword", context); - assertTruthy(passwordInputResult.newPassword, "newPassword", context); - assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", context); - assertTruthy(passwordInputResult.salt, "salt", context); - assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", context); // can have an empty string as a meaningful value, so check non-nullish - - // Verify that the current password is correct - const currentPasswordVerified = await this.masterPasswordUnlockService.proofOfDecryption( - passwordInputResult.currentPassword, - userId, - ); - - if (!currentPasswordVerified) { - throw new InvalidCurrentPasswordError(); - } - - // Current password has been verified, so get the user key from state - const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey"); - - // Use new password to make new auth and unlock data that we can send to the server - const { newAuthenticationData, newUnlockData } = await this.makeNewAuthAndUnlockData( - passwordInputResult.newPassword, - passwordInputResult.kdfConfig, - passwordInputResult.salt, - userKey, - ); - - const request = UpdateTempPasswordRequest.newConstructorWithHint( - newAuthenticationData, - newUnlockData, - passwordInputResult.newPasswordHint, - ); - - // TODO: PM-23047 will look to consolidate this into the change password endpoint. - await this.masterPasswordApiService.putUpdateTempPassword(request); + const context = "Could not change password for account recovery."; + assertTruthy(passwordInputResult.currentPassword, "currentPassword", context); + assertTruthy(passwordInputResult.newPassword, "newPassword", context); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", context); + assertTruthy(passwordInputResult.salt, "salt", context); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", context); // can have an empty string as a meaningful value, so check non-nullish + + // Verify that the current password is correct + const currentPasswordVerified = await this.masterPasswordUnlockService.proofOfDecryption( + passwordInputResult.currentPassword, + userId, + ); - return; // EARLY RETURN for flagged logic + if (!currentPasswordVerified) { + throw new InvalidCurrentPasswordError(); } - const request = new UpdateTempPasswordRequest(); + // Current password has been verified, so get the user key from state + const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey"); - const newMasterKeyEncryptedUserKey = await this.preparePasswordChange( - passwordInputResult, - userId, - request, + // Use new password to make new auth and unlock data that we can send to the server + const { newAuthenticationData, newUnlockData } = await this.makeNewAuthAndUnlockData( + passwordInputResult.newPassword, + passwordInputResult.kdfConfig, + passwordInputResult.salt, + userKey, ); - request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string; + const request = UpdateTempPasswordRequest.newConstructorWithHint( + newAuthenticationData, + newUnlockData, + passwordInputResult.newPasswordHint, + ); - try { - // TODO: PM-23047 will look to consolidate this into the change password endpoint. - await this.masterPasswordApiService.putUpdateTempPassword(request); - } catch { - throw new Error("Could not change password"); - } + // TODO: PM-23047 will look to consolidate this into the change password endpoint. + await this.masterPasswordApiService.putUpdateTempPassword(request); } private async makeNewAuthAndUnlockData( diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts index e0e0315ab12d..f8ce2c307ef4 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -46,7 +46,6 @@ import { SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordUserType, - SetInitialPasswordTdeOffboardingCredentialsOld, SetInitialPasswordTdeOffboardingCredentials, SetInitialPasswordTdeUserWithPermissionCredentials, } from "./set-initial-password.service.abstraction"; @@ -68,8 +67,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi ) {} /** - * @deprecated To be removed in PM-28143. When you remove this, also check for any objects/methods - * in this default service that are now un-used and can also be removed. + * @deprecated use `initializePasswordJitPasswordUserV2Encryption()` instead */ async setInitialPassword( credentials: SetInitialPasswordCredentials, @@ -265,49 +263,6 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); } - /** - * @deprecated To be removed in PM-28143 - */ - async setInitialPasswordTdeOffboardingOld( - credentials: SetInitialPasswordTdeOffboardingCredentialsOld, - userId: UserId, - ) { - const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials; - for (const [key, value] of Object.entries(credentials)) { - if (value == null) { - throw new Error(`${key} not found. Could not set password.`); - } - } - - if (userId == null) { - throw new Error("userId not found. Could not set password."); - } - - const userKey = await firstValueFrom(this.keyService.userKey$(userId)); - if (userKey == null) { - throw new Error("userKey not found. Could not set password."); - } - - const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey( - newMasterKey, - userKey, - ); - - if (!newMasterKeyEncryptedUserKey[1].encryptedString) { - throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password."); - } - - const request = new UpdateTdeOffboardingPasswordRequest(); - request.key = newMasterKeyEncryptedUserKey[1].encryptedString; - request.newMasterPasswordHash = newServerMasterKeyHash; - request.masterPasswordHint = newPasswordHint; - - await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request); - - // Clear force set password reason to allow navigation back to vault. - await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); - } - async initializePasswordJitPasswordUserV2Encryption( credentials: InitializeJitPasswordCredentials, userId: UserId, @@ -469,7 +424,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi } /** - * @deprecated To be removed in PM-28143 + * @deprecated along with `setInitialPassword()` deprecation */ private async makeMasterKeyEncryptedUserKey( masterKey: MasterKey, @@ -491,6 +446,9 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi return masterKeyEncryptedUserKey; } + /** + * @deprecated along with `setInitialPassword()` deprecation + */ private async updateAccountDecryptionProperties( masterKey: MasterKey, kdfConfig: KdfConfig, @@ -548,7 +506,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi } /** - * @deprecated To be removed in PM-28143 + * @deprecated along with `setInitialPassword()` deprecation * * As part of [PM-28494], adding this setting path to accommodate the changes that are * emerging with pm-23246-unlock-with-master-password-unlock-data. @@ -574,7 +532,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi } /** - * @deprecated To be removed in PM-28143 + * @deprecated * * This method is now deprecated because it is used with the deprecated `setInitialPassword()` method, * which handles both JIT MP and TDE + Permission user flows. diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts index 7974ee58214b..a40bc0a5606e 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -64,7 +64,6 @@ import { SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, - SetInitialPasswordTdeOffboardingCredentialsOld, SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; @@ -129,7 +128,9 @@ describe("DefaultSetInitialPasswordService", () => { }); /** - * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties + * @deprecated use `initializePasswordJitPasswordUserV2Encryption()` instead + * + * When you remove this, check also if there are any imports/properties * in the test setup above that are now un-used and can also be removed. */ describe("setInitialPassword(...)", () => { @@ -830,7 +831,7 @@ describe("DefaultSetInitialPasswordService", () => { it(`should throw if the userId was not passed in`, async () => { // Arrange - userId = null; + userId = null as unknown as UserId; // Act const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); @@ -893,120 +894,6 @@ describe("DefaultSetInitialPasswordService", () => { }); }); - /** - * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties - * in the test setup above that are now un-used and can also be removed. - */ - describe("setInitialPasswordTdeOffboardingOld(...)", () => { - // Mock function parameters - let credentials: SetInitialPasswordTdeOffboardingCredentialsOld; - - beforeEach(() => { - // Mock function parameters - credentials = { - newMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, - newServerMasterKeyHash: "newServerMasterKeyHash", - newPasswordHint: "newPasswordHint", - }; - }); - - function setupTdeOffboardingMocks() { - keyService.userKey$.mockReturnValue(of(userKey)); - keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey); - } - - it("should successfully set an initial password for the TDE offboarding user", async () => { - // Arrange - setupTdeOffboardingMocks(); - - const request = new UpdateTdeOffboardingPasswordRequest(); - request.key = masterKeyEncryptedUserKey[1].encryptedString; - request.newMasterPasswordHash = credentials.newServerMasterKeyHash; - request.masterPasswordHint = credentials.newPasswordHint; - - // Act - await sut.setInitialPasswordTdeOffboardingOld(credentials, userId); - - // Assert - expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1); - expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledWith( - request, - ); - }); - - describe("given the initial password has been successfully set", () => { - it("should clear the ForceSetPasswordReason by setting it to None", async () => { - // Arrange - setupTdeOffboardingMocks(); - - // Act - await sut.setInitialPasswordTdeOffboardingOld(credentials, userId); - - // Assert - expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1); - expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( - ForceSetPasswordReason.None, - userId, - ); - }); - }); - - describe("general error handling", () => { - ["newMasterKey", "newServerMasterKeyHash", "newPasswordHint"].forEach((key) => { - it(`should throw if ${key} is not provided on the SetInitialPasswordTdeOffboardingCredentials object`, async () => { - // Arrange - const invalidCredentials: SetInitialPasswordTdeOffboardingCredentialsOld = { - ...credentials, - [key]: null, - }; - - // Act - const promise = sut.setInitialPasswordTdeOffboardingOld(invalidCredentials, userId); - - // Assert - await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`); - }); - }); - - it(`should throw if the userId was not passed in`, async () => { - // Arrange - userId = null; - - // Act - const promise = sut.setInitialPasswordTdeOffboardingOld(credentials, userId); - - // Assert - await expect(promise).rejects.toThrow("userId not found. Could not set password."); - }); - - it(`should throw if the userKey was not found`, async () => { - // Arrange - keyService.userKey$.mockReturnValue(of(null)); - - // Act - const promise = sut.setInitialPasswordTdeOffboardingOld(credentials, userId); - - // Assert - await expect(promise).rejects.toThrow("userKey not found. Could not set password."); - }); - - it(`should throw if a newMasterKeyEncryptedUserKey was not returned`, async () => { - // Arrange - masterKeyEncryptedUserKey[1].encryptedString = "" as EncryptedString; - - setupTdeOffboardingMocks(); - - // Act - const promise = sut.setInitialPasswordTdeOffboardingOld(credentials, userId); - - // Assert - await expect(promise).rejects.toThrow( - "newMasterKeyEncryptedUserKey not found. Could not set password.", - ); - }); - }); - }); - describe("initializePasswordJitPasswordUserV2Encryption()", () => { let mockSdkRef: { value: MockProxy; @@ -1345,7 +1232,7 @@ describe("DefaultSetInitialPasswordService", () => { it("should throw if userId is not given", async () => { // Arrange - userId = null; + userId = null as unknown as UserId; // Act const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); @@ -1472,12 +1359,14 @@ describe("DefaultSetInitialPasswordService", () => { enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); enrollmentRequest.masterPasswordHash = authenticationData.masterPasswordAuthenticationHash; - enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString!; }); it("should throw if organization keys are not found", async () => { // Arrange - organizationApiService.getKeys.mockResolvedValue(null); + organizationApiService.getKeys.mockResolvedValue( + null as unknown as OrganizationKeysResponse, + ); // Act const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); @@ -1490,7 +1379,7 @@ describe("DefaultSetInitialPasswordService", () => { it("should throw if orgPublicKeyEncryptedUserKey is not found", async () => { // Arrange - encryptService.encapsulateKeyUnsigned.mockResolvedValue(null); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(null as unknown as EncString); // Act const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); @@ -1503,7 +1392,7 @@ describe("DefaultSetInitialPasswordService", () => { it("should throw if orgPublicKeyEncryptedUserKey.encryptedString is not found", async () => { // Arrange - orgPublicKeyEncryptedUserKey.encryptedString = null; + orgPublicKeyEncryptedUserKey.encryptedString = null as unknown as any; // Act const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts index 0db6f82cda7d..46bfa156f0ad 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -47,7 +47,6 @@ import { SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, - SetInitialPasswordTdeOffboardingCredentialsOld, SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; @@ -122,84 +121,49 @@ export class SetInitialPasswordComponent implements OnInit { switch (this.userType) { case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER: { - /** - * "KM flag" = EnableAccountEncryptionV2JitPasswordRegistration - * "Auth flag" = PM27086_UpdateAuthenticationApisForInputPassword (checked in InputPasswordComponent and - * passed through via PasswordInputResult) - * - * Flag unwinding for this specific `case` will depend on which flag gets unwound first: - * - If KM flag gets unwound first, remove all code (in this `case`) after the call - * to setInitialPasswordJitMPUserV2Encryption(), as the V2Encryption method is the - * end-goal for this `case`. - * - If Auth flag gets unwound first (in PM-28143), keep the KM code & early return, - * but unwind the auth flagging logic and then remove the method call marked with - * the "Default Scenario" comment. - */ - const accountEncryptionV2 = await this.configService.getFeatureFlag( FeatureFlag.EnableAccountEncryptionV2JitPasswordRegistration, ); - // Scenario 1: KM flag ON if (accountEncryptionV2) { await this.setInitialPasswordJitMPUserV2Encryption(passwordInputResult); return; } - // Scenario 2: KM flag OFF, Auth flag ON - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - /** - * If the Auth flag is enabled, it means the InputPasswordComponent will not emit a newMasterKey, - * newServerMasterKeyHash. So we must create them here and add them late - * to the PasswordInputResult before calling setInitialPassword(). - * - * This is a temporary state. The end-goal will be to use KM's V2Encryption method above. - */ - const ctx = "Could not set initial password."; - assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); - assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); - assertTruthy(this.email, "email", ctx); - - const newMasterKey = await this.keyService.makeMasterKey( - passwordInputResult.newPassword, - this.email.trim().toLowerCase(), - passwordInputResult.kdfConfig, - ); - - const newServerMasterKeyHash = await this.keyService.hashMasterKey( - passwordInputResult.newPassword, - newMasterKey, - ); - - passwordInputResult.newMasterKey = newMasterKey; - passwordInputResult.newServerMasterKeyHash = newServerMasterKeyHash; - - await this.setInitialPassword(passwordInputResult); // passwordInputResult masterKey properties generated on the SetInitialPasswordComponent (just above) - return; - } + /** + * Assuming the KM flag above is off, this JIT_PROVISIONED_MP_ORG_USER case still relies on us making + * `newMasterKey` and `newServerMasterKeyHash` here in the component. This is a temporary state. This + * flow will be updated to use the new `MasterPasswordAuthenticationData` and `MasterPasswordUnlockData` + * as part of https://bitwarden.atlassian.net/browse/PM-32526 + */ - // Default Scenario: both flags OFF - await this.setInitialPassword(passwordInputResult); // passwordInputResult masterKey properties generated on the InputPasswordComponent (default) + const ctx = "Could not set initial password."; + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertTruthy(this.email, "email", ctx); + const newMasterKey = await this.keyService.makeMasterKey( + passwordInputResult.newPassword, + this.email.trim().toLowerCase(), + passwordInputResult.kdfConfig, + ); + + const newServerMasterKeyHash = await this.keyService.hashMasterKey( + passwordInputResult.newPassword, + newMasterKey, + ); + + passwordInputResult.newMasterKey = newMasterKey; + passwordInputResult.newServerMasterKeyHash = newServerMasterKeyHash; + + await this.setInitialPassword(passwordInputResult); break; } case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - await this.setInitialPasswordTdeUserWithPermission(passwordInputResult); - return; // EARLY RETURN for flagged logic - } - - await this.setInitialPassword(passwordInputResult); - + await this.setInitialPasswordTdeUserWithPermission(passwordInputResult); break; case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER: - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - await this.setInitialPasswordTdeOffboarding(passwordInputResult); - return; - } - - await this.setInitialPasswordTdeOffboardingOld(passwordInputResult); - + await this.setInitialPasswordTdeOffboarding(passwordInputResult); break; default: this.logService.error( @@ -347,7 +311,7 @@ export class SetInitialPasswordComponent implements OnInit { } /** - * @deprecated To be removed in PM-28143 + * @deprecated use `setInitialPasswordJitMPUserV2Encryption()` instead */ private async setInitialPassword(passwordInputResult: PasswordInputResult) { const ctx = "Could not set initial password."; @@ -468,41 +432,6 @@ export class SetInitialPasswordComponent implements OnInit { } } - /** - * @deprecated To be removed in PM-28143 - */ - private async setInitialPasswordTdeOffboardingOld(passwordInputResult: PasswordInputResult) { - const ctx = "Could not set initial password."; - assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx); - assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx); - assertTruthy(this.userId, "userId", ctx); - assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish - - try { - const credentials: SetInitialPasswordTdeOffboardingCredentialsOld = { - newMasterKey: passwordInputResult.newMasterKey, - newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash, - newPasswordHint: passwordInputResult.newPasswordHint, - }; - - await this.setInitialPasswordService.setInitialPasswordTdeOffboardingOld( - credentials, - this.userId, - ); - - this.showSuccessToastByUserType(); - - await this.logoutService.logout(this.userId); - // navigate to root so redirect guard can properly route next active user or null user to correct page - await this.router.navigate(["/"]); - } catch (e) { - this.logService.error("Error setting initial password during TDE offboarding", e); - this.validationService.showError(e); - } finally { - this.submitting = false; - } - } - private showSuccessToastByUserType() { if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { this.toastService.showToast({ diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts index c00780a159e2..5e8420024ed4 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts @@ -42,6 +42,9 @@ export const SetInitialPasswordUserType: Readonly<{ [K in keyof typeof _SetInitialPasswordUserType]: SetInitialPasswordUserType; }> = Object.freeze(_SetInitialPasswordUserType); +/** + * @deprecated along with `setInitialPassword()` deprecation + */ export interface SetInitialPasswordCredentials { newMasterKey: MasterKey; newServerMasterKeyHash: string; @@ -64,15 +67,6 @@ export interface SetInitialPasswordTdeUserWithPermissionCredentials { resetPasswordAutoEnroll: boolean; } -/** - * @deprecated To be removed in PM-28143 - */ -export interface SetInitialPasswordTdeOffboardingCredentialsOld { - newMasterKey: MasterKey; - newServerMasterKeyHash: string; - newPasswordHint: string; -} - export interface SetInitialPasswordTdeOffboardingCredentials { newPassword: string; salt: MasterPasswordSalt; @@ -106,7 +100,7 @@ export interface InitializeJitPasswordCredentials { */ export abstract class SetInitialPasswordService { /** - * @deprecated To be removed in PM-28143 + * @deprecated use `initializePasswordJitPasswordUserV2Encryption()` instead * * Sets an initial password for an existing authed user who is either: * - {@link SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER} @@ -135,21 +129,6 @@ export abstract class SetInitialPasswordService { userId: UserId, ) => Promise; - /** - * @deprecated To be removed in PM-28143 - * - * Sets an initial password for a user who logs in after their org offboarded from - * trusted device encryption and is now a master-password-encryption org: - * - {@link SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER} - * - * @param passwordInputResult credentials object received from the `InputPasswordComponent` - * @param userId the account `userId` - */ - abstract setInitialPasswordTdeOffboardingOld: ( - credentials: SetInitialPasswordTdeOffboardingCredentialsOld, - userId: UserId, - ) => Promise; - /** * Initializes a JIT-provisioned user's cryptographic state and enrolls them in master password unlock. * @param credentials The credentials needed to initialize the JIT password user diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index f57aa2616145..c916fdc06de0 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1734,12 +1734,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: RegistrationFinishServiceAbstraction, useClass: DefaultRegistrationFinishService, - deps: [ - KeyService, - AccountApiServiceAbstraction, - MasterPasswordServiceAbstraction, - ConfigService, - ], + deps: [KeyService, AccountApiServiceAbstraction, MasterPasswordServiceAbstraction], }), safeProvider({ provide: TwoFactorAuthComponentService, diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 67e551729fc6..d7f57df8a5b0 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -11,9 +11,7 @@ import { import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -36,12 +34,7 @@ import { Translation, } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { - DEFAULT_KDF_CONFIG, - KdfConfig, - KdfConfigService, - KeyService, -} from "@bitwarden/key-management"; +import { DEFAULT_KDF_CONFIG, KdfConfig, KdfConfigService } from "@bitwarden/key-management"; import { PasswordCalloutComponent } from "../password-callout/password-callout.component"; import { compareInputs, ValidationGoal } from "../validators/compare-inputs.validator"; @@ -210,12 +203,10 @@ export class InputPasswordComponent implements OnInit { constructor( private auditService: AuditService, private cipherService: CipherService, - private configService: ConfigService, private dialogService: DialogService, private formBuilder: FormBuilder, private i18nService: I18nService, private kdfConfigService: KdfConfigService, - private keyService: KeyService, private masterPasswordService: MasterPasswordServiceAbstraction, private passwordGenerationService: PasswordGenerationServiceAbstraction, private platformUtilsService: PlatformUtilsService, @@ -331,7 +322,7 @@ export class InputPasswordComponent implements OnInit { throw new Error("KdfConfig not found."); } - // Determine salt. Branches on userId presence: + // 2. Determine salt. Branches on userId presence: // - SetInitialPasswordAccountRegistration: no userId -> derives salt from email via emailToSalt() // - SetInitialPasswordAuthedUser, ChangePassword, ChangePasswordWithOptionalUserKeyRotation: // have an active userId -> retrieves stored salt via saltForUser$() @@ -351,29 +342,6 @@ export class InputPasswordComponent implements OnInit { throw new Error("Salt not found."); } - // When you unwind the flag in PM-28143, also remove the ConfigService if it is un-used. - const newApisWithInputPasswordFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword, - ); - - // Remove this current password verification block in PM-28143. Current password verification - // is performed by consumers when flag is on. - if (!newApisWithInputPasswordFlagEnabled) { - // 2. Verify current password is correct (if necessary) - if ( - this.flow === InputPasswordFlow.ChangePassword || - this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation - ) { - const currentPasswordVerified = await this.verifyCurrentPassword( - currentPassword, - this.kdfConfig, - ); - if (!currentPasswordVerified) { - return; - } - } - } - // 3. Verify new password const newPasswordVerified = await this.verifyNewPassword( newPassword, @@ -384,79 +352,26 @@ export class InputPasswordComponent implements OnInit { return; } - if (newApisWithInputPasswordFlagEnabled) { - // 4. Build a PasswordInputResult object - const passwordInputResult: PasswordInputResult = { - newPassword, - kdfConfig: this.kdfConfig, - salt, - newPasswordHint, - newApisWithInputPasswordFlagEnabled, // To be removed in PM-28143 - }; - - if ( - this.flow === InputPasswordFlow.ChangePassword || - this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation - ) { - passwordInputResult.currentPassword = currentPassword; - } - - if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) { - passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value; - } - - // 5. Emit and return PasswordInputResult object - this.onPasswordFormSubmit.emit(passwordInputResult); - return passwordInputResult; - } - - /******************************************************************* - * The following code (within this `try`) to be removed in PM-28143 - *******************************************************************/ - - // 4. Create cryptographic keys and build a PasswordInputResult object - const newMasterKey = await this.keyService.makeMasterKey( - newPassword, - this.email, - this.kdfConfig, - ); - - const newServerMasterKeyHash = await this.keyService.hashMasterKey(newPassword, newMasterKey); - + // 4. Build a PasswordInputResult object const passwordInputResult: PasswordInputResult = { newPassword, + kdfConfig: this.kdfConfig, salt, - newMasterKey, - newServerMasterKeyHash, newPasswordHint, - kdfConfig: this.kdfConfig, }; if ( this.flow === InputPasswordFlow.ChangePassword || this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation ) { - const currentMasterKey = await this.keyService.makeMasterKey( - currentPassword, - this.email, - this.kdfConfig, - ); - - const currentServerMasterKeyHash = await this.keyService.hashMasterKey( - currentPassword, - currentMasterKey, - ); - passwordInputResult.currentPassword = currentPassword; - passwordInputResult.currentMasterKey = currentMasterKey; - passwordInputResult.currentServerMasterKeyHash = currentServerMasterKeyHash; } if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) { passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value; } - // 5. Emit cryptographic keys and other password related properties + // 5. Emit and return PasswordInputResult object this.onPasswordFormSubmit.emit(passwordInputResult); return passwordInputResult; } catch (e) { @@ -532,47 +447,6 @@ export class InputPasswordComponent implements OnInit { return passwordInputResult; } - /** - * @deprecated To be removed in PM-28143 - * - * Returns `true` if the current password is correct (it can be used to successfully decrypt - * the masterKeyEncryptedUserKey), `false` otherwise - */ - private async verifyCurrentPassword( - currentPassword: string, - kdfConfig: KdfConfig, - ): Promise { - if (!this.email) { - throw new Error("Email is required to verify current password."); - } - if (!this.userId) { - throw new Error("userId is required to verify current password."); - } - - const currentMasterKey = await this.keyService.makeMasterKey( - currentPassword, - this.email, - kdfConfig, - ); - - const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - currentMasterKey, - this.userId, - ); - - if (decryptedUserKey == null) { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("invalidMasterPassword"), - }); - - return false; - } - - return true; - } - /** * Returns `true` if the new password is not weak or breached and it passes * any enforced org policy options, `false` otherwise diff --git a/libs/auth/src/angular/input-password/input-password.stories.ts b/libs/auth/src/angular/input-password/input-password.stories.ts index 9e3a6419d2ae..abd7d746e6df 100644 --- a/libs/auth/src/angular/input-password/input-password.stories.ts +++ b/libs/auth/src/angular/input-password/input-password.stories.ts @@ -10,7 +10,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -20,7 +19,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi // eslint-disable-next-line no-restricted-imports import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { DEFAULT_KDF_CONFIG, KdfConfigService } from "@bitwarden/key-management"; // FIXME: remove `/apps` import from `/libs` // FIXME: remove `src` and fix import @@ -60,13 +59,6 @@ export default { getAllDecrypted: () => Promise.resolve([]), }, }, - // Can remove ConfigService from component and stories in PM-28143 (if it is no longer used) - { - provide: ConfigService, - useValue: { - getFeatureFlag: () => false, // default to false since flag does not effect UI - }, - }, { provide: KdfConfigService, useValue: { @@ -86,13 +78,6 @@ export default { copyToClipboard: () => true, }, }, - { - provide: KeyService, - useValue: { - makeMasterKey: () => Promise.resolve("example-master-key"), - hashMasterKey: () => Promise.resolve("example-master-key-hash"), - }, - }, { provide: DialogService, useValue: { diff --git a/libs/auth/src/angular/input-password/password-input-result.ts b/libs/auth/src/angular/input-password/password-input-result.ts index cd3465813f67..600c09d173fa 100644 --- a/libs/auth/src/angular/input-password/password-input-result.ts +++ b/libs/auth/src/angular/input-password/password-input-result.ts @@ -11,26 +11,15 @@ export interface PasswordInputResult { rotateUserKey?: boolean; /** - * Temporary property that persists the flag state through the entire set/change password process. - * This allows flows to consume this value instead of re-checking the flag state via ConfigService themselves. - * - * The ChangePasswordDelegation flows (Emergency Access Takeover and Account Recovery), however, only ever - * require a raw newPassword from the InputPasswordComponent regardless of whether the flag is on or off. - * Flagging for those 2 flows will be done via the ConfigService in their respective services. - * - * To be removed in PM-28143 + * @deprecated Still required by the JIT_PROVISIONED_MP_ORG_USER flow in SetInitialPasswordComponent. + * Will be removed when that flow is updated to use MasterPasswordAuthenticationData and + * MasterPasswordUnlockData as part of https://bitwarden.atlassian.net/browse/PM-32526 */ - newApisWithInputPasswordFlagEnabled?: boolean; - - // The deprecated properties below will be removed in PM-28143: https://bitwarden.atlassian.net/browse/PM-28143 - - /** @deprecated This low-level cryptographic state will be removed. It will be replaced by high level calls to masterpassword service, in the consumers of this interface. */ - currentMasterKey?: MasterKey; - /** @deprecated */ - currentServerMasterKeyHash?: string; - - /** @deprecated */ newMasterKey?: MasterKey; - /** @deprecated */ + /** + * @deprecated Still required by the JIT_PROVISIONED_MP_ORG_USER flow in SetInitialPasswordComponent. + * Will be removed when that flow is updated to use MasterPasswordAuthenticationData and + * MasterPasswordUnlockData as part of https://bitwarden.atlassian.net/browse/PM-32526 + */ newServerMasterKeyHash?: string; } diff --git a/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap b/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap index 6a82899dcc18..237f57ca8ad1 100644 --- a/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap +++ b/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap @@ -1,32 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`DefaultRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user with KDF fields when given valid email verification input 1`] = ` +exports[`DefaultRegistrationFinishService finishRegistration() derives the master key and registers the user 1`] = ` RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": "emailVerificationToken", - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`DefaultRegistrationFinishService finishRegistration() when feature flag is ON (new API) derives the master key and registers the user with new data types 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index 3e190f71afa5..4f40d4fa4b43 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -1,7 +1,6 @@ import { MockProxy, mock } from "jest-mock-extended"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -12,10 +11,9 @@ import { MasterPasswordSalt, MasterKeyWrappedUserKey, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KeyService, KdfType } from "@bitwarden/key-management"; +import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -27,19 +25,16 @@ describe("DefaultRegistrationFinishService", () => { let keyService: MockProxy; let accountApiService: MockProxy; let masterPasswordService: MockProxy; - let configService: MockProxy; beforeEach(() => { keyService = mock(); accountApiService = mock(); masterPasswordService = mock(); - configService = mock(); service = new DefaultRegistrationFinishService( keyService, accountApiService, masterPasswordService, - configService, ); }); @@ -71,163 +66,119 @@ describe("DefaultRegistrationFinishService", () => { let userKey: UserKey; let userKeyEncString: EncString; let userKeyPair: [string, EncString]; + let salt: MasterPasswordSalt; + let masterPasswordAuthentication: MasterPasswordAuthenticationData; + let masterPasswordUnlock: MasterPasswordUnlockData; beforeEach(() => { email = "test@email.com"; emailVerificationToken = "emailVerificationToken"; masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey; + salt = "test@email.com" as MasterPasswordSalt; + passwordInputResult = { - newMasterKey: masterKey, - newServerMasterKeyHash: "newServerMasterKeyHash", + newPassword: "newPassword", kdfConfig: DEFAULT_KDF_CONFIG, newPasswordHint: "newPasswordHint", - newPassword: "newPassword", + salt: salt, }; userKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; userKeyEncString = new EncString("userKeyEncrypted"); - userKeyPair = ["publicKey", new EncString("privateKey")]; + + keyService.makeMasterKey.mockResolvedValue(masterKey); + + masterPasswordAuthentication = { + salt, + kdf: DEFAULT_KDF_CONFIG, + masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash, + }; + masterPasswordUnlock = new MasterPasswordUnlockData( + salt, + DEFAULT_KDF_CONFIG, + "wrappedUserKey" as MasterKeyWrappedUserKey, + ); + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + masterPasswordAuthentication, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + }); + + ["newPassword", "salt"].forEach((key) => { + it(`should throw if ${key} is an empty string (falsy) on the PasswordInputResult object`, async () => { + // Arrange + const invalidPasswordInputResult: PasswordInputResult = { + ...passwordInputResult, + [key]: "", + }; + + // Act + const promise = service.finishRegistration(email, invalidPasswordInputResult); + + // Assert + await expect(promise).rejects.toThrow(`${key} is falsy. Could not finish registration.`); + }); + }); + + it("should throw if kdfConfig is undefined on the PasswordInputResult object", async () => { + // Arrange + const invalidPasswordInputResult: PasswordInputResult = { + ...passwordInputResult, + kdfConfig: undefined, + }; + + // Act + const promise = service.finishRegistration(email, invalidPasswordInputResult); + + // Assert + await expect(promise).rejects.toThrow( + "kdfConfig is null or undefined. Could not finish registration.", + ); }); it("throws an error if the user key cannot be created", async () => { - keyService.makeUserKey.mockResolvedValue([null, null]); + keyService.makeUserKey.mockResolvedValue([null, null] as any); await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow( "User key could not be created", ); }); - describe("when feature flag is OFF (old API)", () => { - it("registers the user with KDF fields when given valid email verification input", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ - email, - emailVerificationToken: emailVerificationToken, - masterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - userSymmetricKey: userKeyEncString.encryptedString, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - kdf: KdfType.PBKDF2_SHA256, - kdfIterations: DEFAULT_KDF_CONFIG.iterations, - kdfMemory: undefined, - kdfParallelism: undefined, - }), - ); - - // Verify makeMasterKey is NOT called — the master key comes from passwordInputResult directly - expect(keyService.makeMasterKey).not.toHaveBeenCalled(); - - // Verify old API fields are present - const registerCall = accountApiService.registerFinish.mock.calls[0][0]; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - expect((registerCall as RegisterFinishRequest).kdf).toBeDefined(); - expect((registerCall as RegisterFinishRequest).kdfIterations).toBeDefined(); - - // Verify new API fields are NOT present - expect((registerCall as any).masterPasswordAuthentication).toBeUndefined(); - expect((registerCall as any).masterPasswordUnlock).toBeUndefined(); - - expect(registerCall).toMatchSnapshot(); - }); - }); + it("derives the master key and registers the user", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); - describe("when feature flag is ON (new API)", () => { - let salt: MasterPasswordSalt; - let masterPasswordAuthentication: MasterPasswordAuthenticationData; - let masterPasswordUnlock: MasterPasswordUnlockData; - - beforeEach(() => { - salt = "test@email.com" as MasterPasswordSalt; - - // When the Auth flag is ON, InputPasswordComponent emits newApisWithInputPasswordFlagEnabled: true - // and does NOT emit newMasterKey, newServerMasterKeyHash. - passwordInputResult = { - newPassword: "newPassword", - kdfConfig: DEFAULT_KDF_CONFIG, - newPasswordHint: "newPasswordHint", - newApisWithInputPasswordFlagEnabled: true, - salt: salt, - }; + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - // The service derives the master key internally when the Auth flag is ON - keyService.makeMasterKey.mockResolvedValue(masterKey); + expect(keyService.makeMasterKey).toHaveBeenCalledWith( + passwordInputResult.newPassword, + passwordInputResult.salt, + passwordInputResult.kdfConfig, + ); + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHint: passwordInputResult.newPasswordHint, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); - masterPasswordAuthentication = { - salt, - kdf: DEFAULT_KDF_CONFIG, - masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash, - }; - masterPasswordUnlock = new MasterPasswordUnlockData( - salt, - DEFAULT_KDF_CONFIG, - "wrappedUserKey" as MasterKeyWrappedUserKey, - ); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); - }); + const registerCall = accountApiService.registerFinish.mock.calls[0][0]; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect((registerCall as RegisterFinishRequest).masterPasswordAuthentication).toBeDefined(); + expect((registerCall as RegisterFinishRequest).masterPasswordUnlock).toBeDefined(); - it("derives the master key and registers the user with new data types", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - // Verify master key is derived internally (not from passwordInputResult) - expect(keyService.makeMasterKey).toHaveBeenCalledWith( - passwordInputResult.newPassword, - passwordInputResult.salt, - passwordInputResult.kdfConfig, - ); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ - email, - emailVerificationToken: emailVerificationToken, - masterPasswordHint: passwordInputResult.newPasswordHint, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - masterPasswordAuthentication: masterPasswordAuthentication, - masterPasswordUnlock: masterPasswordUnlock, - }), - ); - - // Verify new API fields are present - const registerCall = accountApiService.registerFinish.mock.calls[0][0]; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect( - (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes) - .masterPasswordAuthentication, - ).toBeDefined(); - expect( - (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes).masterPasswordUnlock, - ).toBeDefined(); - - // Verify old API fields are NOT present (including masterPasswordHash which is in masterPasswordAuthentication) - expect((registerCall as any).masterPasswordHash).toBeUndefined(); - expect((registerCall as any).userSymmetricKey).toBeUndefined(); - expect((registerCall as any).kdf).toBeUndefined(); - expect((registerCall as any).kdfIterations).toBeUndefined(); - - expect(registerCall).toMatchSnapshot(); - }); + expect(registerCall).toMatchSnapshot(); }); }); }); diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 43c73e0b68df..94d53035125f 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -2,16 +2,11 @@ // @ts-strict-ignore import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { assertNonNullish, assertTruthy } from "@bitwarden/common/auth/utils"; -import { - EncryptedString, - EncString, -} from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; @@ -24,7 +19,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi protected keyService: KeyService, protected accountApiService: AccountApiService, protected masterPasswordService: MasterPasswordServiceAbstraction, - protected configService: ConfigService, ) {} getOrgNameFromOrgInvite(): Promise { @@ -45,56 +39,18 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi providerInviteToken?: string, providerUserId?: string, ): Promise { - /** - * "KM flag" = (KM team has ongoing work for this here: PM-24223) - * "Auth flag" = PM27086_UpdateAuthenticationApisForInputPassword (checked in InputPasswordComponent - * and passed through via PasswordInputResult.newApisWithInputPasswordFlagEnabled) - * - * Flag unwinding will depend on which flag gets unwound first: - * - If KM flag gets unwound first, remove all code after the KM V2 path, - * as the V2Encryption method is the end-goal. - * - If Auth flag gets unwound first (in PM-28143), keep the KM code & early return, - * but unwind the auth flagging logic and remove the unflagged `else` block logic following - * the "Scenario 2" code. - */ - - // Scenario 1: KM V2 flag ON (placeholder — to be added when KM's registration V2 PR lands) - // const accountEncryptionV2 = await this.configService.getFeatureFlag( - // FeatureFlag.EnableAccountEncryptionV2Registration, // flag name TBD by KM team - // ); - // if (accountEncryptionV2) { - // // SDK path — end goal. Replaces all key derivation below. - // return; - // } - - let newUserKey: UserKey; - let newEncUserKey: EncString; - - // Scenario 2: KM flag OFF, Auth flag ON - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - /** - * If the Auth flag is enabled, it means the InputPasswordComponent will not emit a newMasterKey. - * So we must create it here for registration. - * - * This is a temporary state. The end-goal will be to use KM's V2Encryption method above. - */ - const ctx = "Could not finish registration."; - assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); - assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); - assertTruthy(passwordInputResult.salt, "salt", ctx); - - const newMasterKey = await this.keyService.makeMasterKey( - passwordInputResult.newPassword, - passwordInputResult.salt, - passwordInputResult.kdfConfig, - ); + const ctx = "Could not finish registration."; + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertTruthy(passwordInputResult.salt, "salt", ctx); + + const newMasterKey = await this.keyService.makeMasterKey( + passwordInputResult.newPassword, + passwordInputResult.salt, + passwordInputResult.kdfConfig, + ); - [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(newMasterKey); - } else { - [newUserKey, newEncUserKey] = await this.keyService.makeUserKey( - passwordInputResult.newMasterKey, - ); - } + const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(newMasterKey); if (!newUserKey || !newEncUserKey) { throw new Error("User key could not be created"); @@ -105,7 +61,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi newUserKey, email, passwordInputResult, - newEncUserKey.encryptedString, userAsymmetricKeys, emailVerificationToken, orgSponsoredFreeFamilyPlanToken, @@ -122,7 +77,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi newUserKey: UserKey, email: string, passwordInputResult: PasswordInputResult, - encryptedUserKey: EncryptedString, userAsymmetricKeys: [string, EncString], emailVerificationToken?: string, orgSponsoredFreeFamilyPlanToken?: string, // web only @@ -130,62 +84,38 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi emergencyAccessId?: string, // web only providerInviteToken?: string, // web only providerUserId?: string, // web only - ): Promise { + ): Promise { const userAsymmetricKeysRequest = new KeysRequest( userAsymmetricKeys[0], userAsymmetricKeys[1].encryptedString, ); - const useNewApi = passwordInputResult.newApisWithInputPasswordFlagEnabled ?? false; - - if (useNewApi) { - // New API path - use new request with new data types - - const masterPasswordAuthentication = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - passwordInputResult.newPassword, - passwordInputResult.kdfConfig, - passwordInputResult.salt, - ); - - const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData( + const masterPasswordAuthentication = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( passwordInputResult.newPassword, passwordInputResult.kdfConfig, passwordInputResult.salt, - newUserKey, - ); - - const registerFinishRequest = new RegisterFinishRequestWithAuthUnlockDataTypes( - email, - passwordInputResult.newPasswordHint, - userAsymmetricKeysRequest, - masterPasswordAuthentication, - masterPasswordUnlock, ); - if (emailVerificationToken) { - registerFinishRequest.emailVerificationToken = emailVerificationToken; - } - - return registerFinishRequest; - } else { - // Old API path - use original request with KDF fields - - const registerFinishRequest = new RegisterFinishRequest( - email, - passwordInputResult.newServerMasterKeyHash, - passwordInputResult.newPasswordHint, - encryptedUserKey, - userAsymmetricKeysRequest, - passwordInputResult.kdfConfig.kdfType, - passwordInputResult.kdfConfig.iterations, - ); + const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData( + passwordInputResult.newPassword, + passwordInputResult.kdfConfig, + passwordInputResult.salt, + newUserKey, + ); - if (emailVerificationToken) { - registerFinishRequest.emailVerificationToken = emailVerificationToken; - } + const registerFinishRequest = new RegisterFinishRequest( + email, + passwordInputResult.newPasswordHint, + userAsymmetricKeysRequest, + masterPasswordAuthentication, + masterPasswordUnlock, + ); - return registerFinishRequest; + if (emailVerificationToken) { + registerFinishRequest.emailVerificationToken = emailVerificationToken; } + + return registerFinishRequest; } } diff --git a/libs/common/src/auth/abstractions/account-api.service.ts b/libs/common/src/auth/abstractions/account-api.service.ts index 972c7d848dea..1c38f2676810 100644 --- a/libs/common/src/auth/abstractions/account-api.service.ts +++ b/libs/common/src/auth/abstractions/account-api.service.ts @@ -1,4 +1,3 @@ -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "../models/request/registration/register-finish-request-with-auth-unlock-data.types"; import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; @@ -48,19 +47,9 @@ export abstract class AccountApiService { * @param request - The user's email verification token, the email, newly created user key, and new * asymmetric user key pair along with the KDF information, hashed MP and salt (in the form of the * MasterPasswordAuthenticationData and MasterPasswordUnlockData) used during the process. - * - * OLD DESCRIPTION FOR ORIGINAL API CONTRACT: (the rest of this JSDoc below can be removed in PM-28143) - * - * Completes the registration process. - * - * @param request - The request object containing the user's email verification token, - * the email, hashed MP, newly created user key, and new asymmetric user key pair along - * with the KDF information used during the process. * @returns A promise that resolves when the registration process is successfully completed. */ - abstract registerFinish( - request: RegisterFinishRequest | RegisterFinishRequestWithAuthUnlockDataTypes, - ): Promise; + abstract registerFinish(request: RegisterFinishRequest): Promise; /** * Sets the [dbo].[User].[VerifyDevices] flag to true or false. diff --git a/libs/common/src/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types.ts b/libs/common/src/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types.ts deleted file mode 100644 index 7c3adfeb7447..000000000000 --- a/libs/common/src/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - MasterPasswordAuthenticationData, - MasterPasswordUnlockData, -} from "../../../../key-management/master-password/types/master-password.types"; -import { KeysRequest } from "../../../../models/request/keys.request"; - -export class RegisterFinishRequestWithAuthUnlockDataTypes { - constructor( - public email: string, - public masterPasswordHint: string, - - public userAsymmetricKeys: KeysRequest, - - public masterPasswordAuthentication: MasterPasswordAuthenticationData, - public masterPasswordUnlock: MasterPasswordUnlockData, - - public emailVerificationToken?: string, - public orgSponsoredFreeFamilyPlanToken?: string, - public acceptEmergencyAccessInviteToken?: string, - public acceptEmergencyAccessId?: string, - public providerInviteToken?: string, - public providerUserId?: string, - - // Org Invite data (only applies on web) - public organizationUserId?: string, - public orgInviteToken?: string, - ) {} -} diff --git a/libs/common/src/auth/models/request/registration/register-finish.request.ts b/libs/common/src/auth/models/request/registration/register-finish.request.ts index b388e8aee497..5c2d04a28882 100644 --- a/libs/common/src/auth/models/request/registration/register-finish.request.ts +++ b/libs/common/src/auth/models/request/registration/register-finish.request.ts @@ -1,24 +1,18 @@ -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { KdfType } from "@bitwarden/key-management"; - -import { EncryptedString } from "../../../../key-management/crypto/models/enc-string"; +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "../../../../key-management/master-password/types/master-password.types"; import { KeysRequest } from "../../../../models/request/keys.request"; export class RegisterFinishRequest { constructor( public email: string, - - public masterPasswordHash: string, public masterPasswordHint: string, - public userSymmetricKey: EncryptedString, public userAsymmetricKeys: KeysRequest, - public kdf: KdfType, - public kdfIterations: number, - public kdfMemory?: number, - public kdfParallelism?: number, + public masterPasswordAuthentication: MasterPasswordAuthenticationData, + public masterPasswordUnlock: MasterPasswordUnlockData, public emailVerificationToken?: string, public orgSponsoredFreeFamilyPlanToken?: string, diff --git a/libs/common/src/auth/services/account-api.service.ts b/libs/common/src/auth/services/account-api.service.ts index b922484b3fd1..923153a0a0b1 100644 --- a/libs/common/src/auth/services/account-api.service.ts +++ b/libs/common/src/auth/services/account-api.service.ts @@ -7,7 +7,6 @@ import { LogService } from "../../platform/abstractions/log.service"; import { AccountApiService } from "../abstractions/account-api.service"; import { InternalAccountService } from "../abstractions/account.service"; import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction"; -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "../models/request/registration/register-finish-request-with-auth-unlock-data.types"; import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; @@ -85,9 +84,7 @@ export class AccountApiServiceImplementation implements AccountApiService { } } - async registerFinish( - request: RegisterFinishRequest | RegisterFinishRequestWithAuthUnlockDataTypes, - ): Promise { + async registerFinish(request: RegisterFinishRequest): Promise { const env = await firstValueFrom(this.environmentService.environment$); try { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index c2cb5f15627f..f5b13477eda0 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -18,7 +18,6 @@ export enum FeatureFlag { PolicyDrawers = "pm-34804-policy-drawers", /* Auth */ - PM27086_UpdateAuthenticationApisForInputPassword = "pm-27086-update-authentication-apis-for-input-password", SafariAccountSwitching = "pm-5594-safari-account-switching", PM30811_ChangeEmailNewAuthenticationApis = "pm-30811-change-email-new-authentication-apis", PM31088_MasterPasswordServiceEmitSalt = "pm-31088-master-password-service-emit-salt", @@ -163,7 +162,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM34500_StrictCipherDecryption]: FALSE, /* Auth */ - [FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword]: FALSE, [FeatureFlag.SafariAccountSwitching]: FALSE, [FeatureFlag.PM30811_ChangeEmailNewAuthenticationApis]: FALSE, [FeatureFlag.PM31088_MasterPasswordServiceEmitSalt]: FALSE,