From 27c6aa812149652cbed92b80c888b9dad1303c4b Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:06:04 -0800 Subject: [PATCH] refactor(input-password-flows): [Auth/PM-27086] Use new KM Data Types in InputPasswordComponent flows - Account Recovery (#18423) Update Account Recovery flow to use new KM data types from `master-password.types.ts` / `MasterPasswordService`: - `MasterPasswordAuthenticationData` - `MasterPasswordUnlockData` This allows us to move away from the deprecated `makeMasterKey()` method (which takes email as salt) as we seek to eventually separate the email from the salt. Changes are behind feature flag: `pm-27086-update-authentication-apis-for-input-password` --- ...zation-user-reset-password.service.spec.ts | 188 +++++++++++++++++- ...rganization-user-reset-password.service.ts | 44 ++++ 2 files changed, 230 insertions(+), 2 deletions(-) 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 afc16e72373..69feb2b86bc 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 @@ -6,6 +6,7 @@ import { BehaviorSubject, of } from "rxjs"; import { OrganizationUserApiService, OrganizationUserResetPasswordDetailsResponse, + OrganizationUserResetPasswordRequest, } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -13,6 +14,15 @@ import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; +import { + MasterKeyWrappedUserKey, + MasterPasswordAuthenticationData, + MasterPasswordAuthenticationHash, + 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"; @@ -21,7 +31,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp 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 { KdfType, KeyService } from "@bitwarden/key-management"; +import { DEFAULT_KDF_CONFIG, KdfConfig, KdfType, KeyService } from "@bitwarden/key-management"; import { OrganizationUserResetPasswordService } from "./organization-user-reset-password.service"; @@ -39,6 +49,8 @@ describe("OrganizationUserResetPasswordService", () => { let i18nService: MockProxy; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let configService: MockProxy; beforeAll(() => { keyService = mock(); @@ -48,6 +60,8 @@ describe("OrganizationUserResetPasswordService", () => { organizationApiService = mock(); i18nService = mock(); accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); + configService = mock(); sut = new OrganizationUserResetPasswordService( keyService, @@ -57,6 +71,8 @@ describe("OrganizationUserResetPasswordService", () => { organizationApiService, i18nService, accountService, + masterPasswordService, + configService, ); }); @@ -129,13 +145,23 @@ describe("OrganizationUserResetPasswordService", () => { }); }); - describe("resetMasterPassword", () => { + /** + * @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, @@ -185,6 +211,164 @@ describe("OrganizationUserResetPasswordService", () => { }); }); + describe("resetMasterPassword [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { + // 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; + let kdfConfig: KdfConfig; + let authenticationData: MasterPasswordAuthenticationData; + let unlockData: MasterPasswordUnlockData; + let userKey: UserKey; + + beforeEach(() => { + // Mock feature flag value + configService.getFeatureFlag.mockResolvedValue( + PM27086_UpdateAuthenticationApisForInputPasswordFlagEnabled, + ); + + // Mock method data + kdfConfig = DEFAULT_KDF_CONFIG; + + organizationUserResetPasswordDetailsResponse = + new OrganizationUserResetPasswordDetailsResponse({ + organizationUserId: orgUserId, + kdf: kdfConfig.kdfType, + kdfIterations: kdfConfig.iterations, + resetPasswordKey: "test-reset-password-key", + encryptedPrivateKey: "test-encrypted-private-key", + }); + + organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue( + organizationUserResetPasswordDetailsResponse, + ); + + const mockDecryptedOrgKeyBytes = new Uint8Array(64).fill(1); + const mockDecryptedOrgKey = new SymmetricCryptoKey(mockDecryptedOrgKeyBytes) as OrgKey; + + keyService.orgKeys$.mockReturnValue( + of({ [orgId]: mockDecryptedOrgKey } as Record), + ); + + const mockDecryptedPrivateKeyBytes = new Uint8Array(64).fill(2); + encryptService.unwrapDecapsulationKey.mockResolvedValue(mockDecryptedPrivateKeyBytes); + + const mockDecryptedUserKeyBytes = new Uint8Array(64).fill(3); + const mockUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes); + encryptService.decapsulateKeyUnsigned.mockResolvedValue(mockUserKey); // returns `SymmetricCryptoKey` + userKey = mockUserKey as UserKey; // type cast to `UserKey` (see code implementation). Points to same object as mockUserKey. + + 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); + }); + + it("should throw an error if the organizationUserResetPasswordDetailsResponse is nullish", async () => { + // Arrange + organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null); + + // Act + const promise = sut.resetMasterPassword(newMasterPassword, email, orgUserId, orgId); + + // Assert + await expect(promise).rejects.toThrow(); + }); + + it("should throw an error if the org key cannot be found", async () => { + // Arrange + keyService.orgKeys$.mockReturnValue(of({} as Record)); + + // Act + const promise = sut.resetMasterPassword(newMasterPassword, email, orgUserId, orgId); + + // Assert + await expect(promise).rejects.toThrow("No org key found"); + }); + + it("should throw an error if orgKeys$ returns null", async () => { + // Arrange + keyService.orgKeys$.mockReturnValue(of(null)); + + // Act + const promise = sut.resetMasterPassword(newMasterPassword, email, orgUserId, orgId); + + // Assert + await expect(promise).rejects.toThrow(); + }); + + it("should call makeMasterPasswordAuthenticationData and makeMasterPasswordUnlockData with the correct parameters", async () => { + // Act + await sut.resetMasterPassword(newMasterPassword, email, orgUserId, orgId); + + // Assert + const request = OrganizationUserResetPasswordRequest.newConstructor( + authenticationData, + unlockData, + ); + + expect(masterPasswordService.mock.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + newMasterPassword, + kdfConfig, + salt, + ); + + expect(masterPasswordService.mock.makeMasterPasswordUnlockData).toHaveBeenCalledWith( + newMasterPassword, + kdfConfig, + salt, + userKey, + ); + + expect(organizationUserApiService.putOrganizationUserResetPassword).toHaveBeenCalledWith( + orgId, + orgUserId, + request, + ); + }); + + it("should call the API method to reset the user's master password", async () => { + // Act + await sut.resetMasterPassword(newMasterPassword, email, orgUserId, orgId); + + // Assert + const request = OrganizationUserResetPasswordRequest.newConstructor( + authenticationData, + unlockData, + ); + expect(organizationUserApiService.putOrganizationUserResetPassword).toHaveBeenCalledTimes(1); + expect(organizationUserApiService.putOrganizationUserResetPassword).toHaveBeenCalledWith( + orgId, + orgUserId, + request, + ); + }); + }); + describe("getPublicKeys", () => { it("should return public keys for organizations that have reset password enrolled", async () => { const result = await sut.getPublicKeys("userId" as UserId); 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 88797f86650..bd3dd7fbb0b 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,11 +12,15 @@ 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, 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 { 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"; @@ -47,6 +51,8 @@ export class OrganizationUserResetPasswordService implements UserKeyRotationKeyR private organizationApiService: OrganizationApiServiceAbstraction, private i18nService: I18nService, private accountService: AccountService, + private masterPasswordService: MasterPasswordServiceAbstraction, + private configService: ConfigService, ) {} /** @@ -140,6 +146,44 @@ export class OrganizationUserResetPasswordService implements UserKeyRotationKeyR ? new PBKDF2KdfConfig(response.kdfIterations) : new Argon2KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism); + const newApisWithInputPasswordFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword, + ); + + if (newApisWithInputPasswordFlagEnabled) { + const salt: MasterPasswordSalt = this.masterPasswordService.emailToSalt(email); + + // Create authentication and unlock data + const authenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + newMasterPassword, + kdfConfig, + salt, + ); + + const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData( + newMasterPassword, + kdfConfig, + salt, + existingUserKey, + ); + + // Create request + const request = OrganizationUserResetPasswordRequest.newConstructor( + authenticationData, + unlockData, + ); + + // Change user's password + await this.organizationUserApiService.putOrganizationUserResetPassword( + orgId, + orgUserId, + request, + ); + + return; // EARLY RETURN for flagged code + } + // Create new master key and hash new password const newMasterKey = await this.keyService.makeMasterKey( newMasterPassword,