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:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user