1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 05:00:10 +00:00

Merge branch 'main' of https://github.com/bitwarden/clients into vault/pm-27632/sdk-cipher-ops

This commit is contained in:
Nik Gilmore
2025-12-29 16:05:22 -08:00
506 changed files with 41322 additions and 7914 deletions

View File

@@ -15,9 +15,11 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -44,6 +46,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected accountCryptographicStateService: AccountCryptographicStateService,
) {}
async setInitialPassword(
@@ -60,6 +63,8 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
orgSsoIdentifier,
orgId,
resetPasswordAutoEnroll,
newPassword,
salt,
} = credentials;
for (const [key, value] of Object.entries(credentials)) {
@@ -153,6 +158,20 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
userId,
);
// Set master password unlock data for unlock path pointed to with
// MasterPasswordUnlockData feature development
// (requires: password, salt, kdf, userKey).
// As migration to this strategy continues, both unlock paths need supported.
// Several invocations in this file become redundant and can be removed once
// the feature is enshrined/unwound. These are marked with [PM-23246] below.
await this.setMasterPasswordUnlockData(
newPassword,
salt,
kdfConfig,
masterKeyEncryptedUserKey[0],
userId,
);
/**
* Set the private key only for new JIT provisioned users in MP encryption orgs.
* (Existing TDE users will have their private key set on sync or on login.)
@@ -162,8 +181,17 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
throw new Error("encrypted private key not found. Could not set private key in state.");
}
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
await this.accountCryptographicStateService.setAccountCryptographicState(
{
V1: {
private_key: keyPair[1].encryptedString,
},
},
userId,
);
}
// [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
if (resetPasswordAutoEnroll) {
@@ -206,10 +234,40 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
userDecryptionOpts,
);
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
// [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
await this.masterPasswordService.setMasterKey(masterKey, userId);
// [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
masterKeyEncryptedUserKey[1],
userId,
);
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
}
/**
* 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.
* Without this, immediately locking/unlocking the vault with the new password _may_ still fail
* if sync has not completed. Sync will eventually set this data, but we want to ensure it's
* set right away here to prevent a race condition UX issue that prevents immediate unlock.
*/
private async setMasterPasswordUnlockData(
password: string,
salt: MasterPasswordSalt,
kdfConfig: KdfConfig,
userKey: UserKey,
userId: UserId,
): Promise<void> {
const masterPasswordUnlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(
password,
kdfConfig,
salt,
userKey,
);
await this.masterPasswordService.setMasterPasswordUnlockData(masterPasswordUnlockData, userId);
}
private async handleResetPasswordAutoEnroll(
masterKeyHash: string,
orgId: string,

View File

@@ -20,6 +20,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import {
EncryptedString,
@@ -56,6 +57,7 @@ describe("DefaultSetInitialPasswordService", () => {
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
let userId: UserId;
let userKey: UserKey;
@@ -73,6 +75,7 @@ describe("DefaultSetInitialPasswordService", () => {
organizationApiService = mock<OrganizationApiServiceAbstraction>();
organizationUserApiService = mock<OrganizationUserApiService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
accountCryptographicStateService = mock<AccountCryptographicStateService>();
userId = "userId" as UserId;
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
@@ -90,6 +93,7 @@ describe("DefaultSetInitialPasswordService", () => {
organizationApiService,
organizationUserApiService,
userDecryptionOptionsService,
accountCryptographicStateService,
);
});
@@ -130,6 +134,8 @@ describe("DefaultSetInitialPasswordService", () => {
orgSsoIdentifier: "orgSsoIdentifier",
orgId: "orgId",
resetPasswordAutoEnroll: false,
newPassword: "Test@Password123!",
salt: "user@example.com" as any,
};
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
@@ -222,6 +228,8 @@ describe("DefaultSetInitialPasswordService", () => {
"orgSsoIdentifier",
"orgId",
"resetPasswordAutoEnroll",
"newPassword",
"salt",
].forEach((key) => {
it(`should throw if ${key} is not provided on the SetInitialPasswordCredentials object`, async () => {
// Arrange
@@ -353,6 +361,10 @@ describe("DefaultSetInitialPasswordService", () => {
ForceSetPasswordReason.None,
userId,
);
expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
masterKeyEncryptedUserKey[1],
userId,
);
});
it("should update account decryption properties", async () => {
@@ -386,6 +398,16 @@ describe("DefaultSetInitialPasswordService", () => {
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId);
expect(
accountCryptographicStateService.setAccountCryptographicState,
).toHaveBeenCalledWith(
{
V1: {
private_key: keyPair[1].encryptedString as EncryptedString,
},
},
userId,
);
});
it("should set the local master key hash to state", async () => {
@@ -403,6 +425,36 @@ describe("DefaultSetInitialPasswordService", () => {
);
});
it("should create and set master password unlock data to prevent race condition with sync", async () => {
// Arrange
setupMocks();
const mockUnlockData = {
salt: credentials.salt,
kdf: credentials.kdfConfig,
masterKeyWrappedUserKey: "wrapped_key_string",
};
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(
mockUnlockData as any,
);
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith(
credentials.newPassword,
credentials.kdfConfig,
credentials.salt,
masterKeyEncryptedUserKey[0],
);
expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith(
mockUnlockData,
userId,
);
});
describe("given resetPasswordAutoEnroll is true", () => {
it(`should handle reset password (account recovery) auto enroll`, async () => {
// Arrange
@@ -572,6 +624,10 @@ describe("DefaultSetInitialPasswordService", () => {
credentials.newMasterKey,
userId,
);
expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
masterKeyEncryptedUserKey[1],
userId,
);
expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId);
});
@@ -602,6 +658,36 @@ describe("DefaultSetInitialPasswordService", () => {
);
});
it("should create and set master password unlock data to prevent race condition with sync", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
const mockUnlockData = {
salt: credentials.salt,
kdf: credentials.kdfConfig,
masterKeyWrappedUserKey: "wrapped_key_string",
};
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(
mockUnlockData as any,
);
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith(
credentials.newPassword,
credentials.kdfConfig,
credentials.salt,
masterKeyEncryptedUserKey[0],
);
expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith(
mockUnlockData,
userId,
);
});
describe("given resetPasswordAutoEnroll is true", () => {
it(`should handle reset password (account recovery) auto enroll`, async () => {
// Arrange

View File

@@ -214,6 +214,8 @@ export class SetInitialPasswordComponent implements OnInit {
assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx);
assertTruthy(passwordInputResult.newLocalMasterKeyHash, "newLocalMasterKeyHash", ctx);
assertTruthy(passwordInputResult.kdfConfig, "kdfConfig", ctx);
assertTruthy(passwordInputResult.newPassword, "newPassword", ctx);
assertTruthy(passwordInputResult.salt, "salt", ctx);
assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx);
assertTruthy(this.orgId, "orgId", ctx);
assertTruthy(this.userType, "userType", ctx);
@@ -231,6 +233,8 @@ export class SetInitialPasswordComponent implements OnInit {
orgSsoIdentifier: this.orgSsoIdentifier,
orgId: this.orgId,
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
newPassword: passwordInputResult.newPassword,
salt: passwordInputResult.salt,
};
await this.setInitialPasswordService.setInitialPassword(

View File

@@ -1,3 +1,4 @@
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey } from "@bitwarden/common/types/key";
import { KdfConfig } from "@bitwarden/key-management";
@@ -50,6 +51,8 @@ export interface SetInitialPasswordCredentials {
orgSsoIdentifier: string;
orgId: string;
resetPasswordAutoEnroll: boolean;
newPassword: string;
salt: MasterPasswordSalt;
}
export interface SetInitialPasswordTdeOffboardingCredentials {

View File

@@ -168,6 +168,8 @@ import { OrganizationBillingService } from "@bitwarden/common/billing/services/o
import { DefaultSubscriptionPricingService } from "@bitwarden/common/billing/services/subscription-pricing.service";
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service";
import {
DefaultKeyGenerationService,
KeyGenerationService,
@@ -528,7 +530,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: ChangeKdfService,
useClass: DefaultChangeKdfService,
deps: [ChangeKdfApiService, SdkService],
deps: [ChangeKdfApiService, SdkService, KeyService, InternalMasterPasswordServiceAbstraction],
}),
safeProvider({
provide: EncryptedMigrator,
@@ -572,6 +574,7 @@ const safeProviders: SafeProvider[] = [
KdfConfigService,
TaskSchedulerService,
ConfigService,
AccountCryptographicStateService,
],
}),
safeProvider({
@@ -897,8 +900,14 @@ const safeProviders: SafeProvider[] = [
StateProvider,
SecurityStateService,
KdfConfigService,
AccountCryptographicStateService,
],
}),
safeProvider({
provide: AccountCryptographicStateService,
useClass: DefaultAccountCryptographicStateService,
deps: [StateProvider],
}),
safeProvider({
provide: BroadcasterService,
useClass: DefaultBroadcasterService,
@@ -1336,7 +1345,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: ChangeKdfService,
useClass: DefaultChangeKdfService,
deps: [ChangeKdfApiService, SdkService],
deps: [ChangeKdfApiService, SdkService, KeyService, InternalMasterPasswordServiceAbstraction],
}),
safeProvider({
provide: AuthRequestServiceAbstraction,
@@ -1568,6 +1577,7 @@ const safeProviders: SafeProvider[] = [
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
AccountCryptographicStateService,
],
}),
safeProvider({