1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

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`
This commit is contained in:
rr-bw
2026-02-02 09:06:04 -08:00
committed by GitHub
parent 98060d15bc
commit 27c6aa8121
2 changed files with 230 additions and 2 deletions

View File

@@ -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<I18nService>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService;
let configService: MockProxy<ConfigService>;
beforeAll(() => {
keyService = mock<KeyService>();
@@ -48,6 +60,8 @@ describe("OrganizationUserResetPasswordService", () => {
organizationApiService = mock<OrganizationApiService>();
i18nService = mock<I18nService>();
accountService = mockAccountServiceWith(mockUserId);
masterPasswordService = new FakeMasterPasswordService();
configService = mock<ConfigService>();
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<OrganizationId, OrgKey>),
);
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<OrganizationId, OrgKey>));
// 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);

View File

@@ -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,