mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 14:04:03 +00:00
Merge main
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -572,6 +574,7 @@ const safeProviders: SafeProvider[] = [
|
||||
KdfConfigService,
|
||||
TaskSchedulerService,
|
||||
ConfigService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -894,8 +897,14 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
SecurityStateService,
|
||||
KdfConfigService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AccountCryptographicStateService,
|
||||
useClass: DefaultAccountCryptographicStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BroadcasterService,
|
||||
useClass: DefaultBroadcasterService,
|
||||
@@ -1137,6 +1146,10 @@ const safeProviders: SafeProvider[] = [
|
||||
KeyGenerationService,
|
||||
LOGOUT_CALLBACK,
|
||||
StateProvider,
|
||||
ConfigService,
|
||||
RegisterSdkService,
|
||||
SecurityStateService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1565,6 +1578,7 @@ const safeProviders: SafeProvider[] = [
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Note: Nudge related code is exported from `libs/angular` because it is consumed by multiple
|
||||
// `libs/*` packages. Exporting from the `libs/vault` package creates circular dependencies.
|
||||
export { NudgesService, NudgeStatus, NudgeType } from "./services/nudges.service";
|
||||
export { AUTOFILL_NUDGE_SERVICE } from "./services/nudge-injection-tokens";
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./empty-vault-nudge.service";
|
||||
export * from "./vault-settings-import-nudge.service";
|
||||
export * from "./new-item-nudge.service";
|
||||
export * from "./new-account-nudge.service";
|
||||
export * from "./noop-nudge.service";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { SingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
/**
|
||||
* A no-op nudge service that always returns dismissed status.
|
||||
* Use this for nudges that should be completely ignored/hidden in certain clients.
|
||||
* For example, browser-specific nudges can use this as the default in non-browser clients.
|
||||
*/
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class NoOpNudgeService implements SingleNudgeService {
|
||||
nudgeStatus$(_nudgeType: NudgeType, _userId: UserId): Observable<NudgeStatus> {
|
||||
return of({ hasBadgeDismissed: true, hasSpotlightDismissed: true });
|
||||
}
|
||||
|
||||
async setNudgeStatus(
|
||||
_nudgeType: NudgeType,
|
||||
_newStatus: NudgeStatus,
|
||||
_userId: UserId,
|
||||
): Promise<void> {
|
||||
// No-op: state changes are ignored
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { InjectionToken } from "@angular/core";
|
||||
|
||||
import { SingleNudgeService } from "./default-single-nudge.service";
|
||||
|
||||
export const AUTOFILL_NUDGE_SERVICE = new InjectionToken<SingleNudgeService>(
|
||||
"AutofillNudgeService",
|
||||
);
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
NewItemNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
VaultSettingsImportNudgeService,
|
||||
NoOpNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||
import { AUTOFILL_NUDGE_SERVICE } from "./nudge-injection-tokens";
|
||||
|
||||
export type NudgeStatus = {
|
||||
hasBadgeDismissed: boolean;
|
||||
@@ -56,6 +58,12 @@ export class NudgesService {
|
||||
private newItemNudgeService = inject(NewItemNudgeService);
|
||||
private newAcctNudgeService = inject(NewAccountNudgeService);
|
||||
|
||||
// NoOp service that always returns dismissed
|
||||
private noOpNudgeService = inject(NoOpNudgeService);
|
||||
|
||||
// Optional Browser-specific service provided via injection token (not all clients have autofill)
|
||||
private autofillNudgeService = inject(AUTOFILL_NUDGE_SERVICE, { optional: true });
|
||||
|
||||
/**
|
||||
* Custom nudge services to use for specific nudge types
|
||||
* Each nudge type can have its own service to determine when to show the nudge
|
||||
@@ -66,7 +74,7 @@ export class NudgesService {
|
||||
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||
[NudgeType.VaultSettingsImportNudge]: inject(VaultSettingsImportNudgeService),
|
||||
[NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
|
||||
[NudgeType.AutofillNudge]: this.newAcctNudgeService,
|
||||
[NudgeType.AutofillNudge]: this.autofillNudgeService ?? this.noOpNudgeService,
|
||||
[NudgeType.DownloadBitwarden]: this.newAcctNudgeService,
|
||||
[NudgeType.GeneratorNudgeStatus]: this.newAcctNudgeService,
|
||||
[NudgeType.NewLoginItemStatus]: this.newItemNudgeService,
|
||||
|
||||
Reference in New Issue
Block a user