1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-15 07:54:55 +00:00
This commit is contained in:
Bernd Schoolmann
2025-07-23 12:54:54 +02:00
parent 29f87f2b95
commit f2edf37cf3
77 changed files with 541 additions and 3592 deletions

View File

@@ -1,232 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnDestroy, OnInit } from "@angular/core";
import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
@Directive()
export class ChangePasswordComponent implements OnInit, OnDestroy {
masterPassword: string;
masterPasswordRetype: string;
formPromise: Promise<any>;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
passwordStrengthResult: any;
color: string;
text: string;
leakedPassword: boolean;
minimumLength = Utils.minimumPasswordLength;
protected email: string;
protected kdfConfig: KdfConfig;
protected destroy$ = new Subject<void>();
constructor(
protected accountService: AccountService,
protected dialogService: DialogService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected messagingService: MessagingService,
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
protected toastService: ToastService,
) {}
async ngOnInit() {
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
this.accountService.activeAccount$
.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
takeUntil(this.destroy$),
)
.subscribe(
(enforcedPasswordPolicyOptions) =>
(this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions),
);
if (this.enforcedPolicyOptions?.minLength) {
this.minimumLength = this.enforcedPolicyOptions.minLength;
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async submit() {
if (!(await this.strongPassword())) {
return;
}
if (!(await this.setupSubmitActions())) {
return;
}
const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
);
if (this.kdfConfig == null) {
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
}
// Create new master key
const newMasterKey = await this.keyService.makeMasterKey(
this.masterPassword,
email.trim().toLowerCase(),
this.kdfConfig,
);
const newMasterKeyHash = await this.keyService.hashMasterKey(this.masterPassword, newMasterKey);
let newProtectedUserKey: [UserKey, EncString] = null;
const userKey = await this.keyService.getUserKey();
if (userKey == null) {
newProtectedUserKey = await this.keyService.makeUserKey(newMasterKey);
} else {
newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
}
await this.performSubmitActions(newMasterKeyHash, newMasterKey, newProtectedUserKey);
}
async setupSubmitActions(): Promise<boolean> {
// Override in sub-class
// Can be used for additional validation and/or other processes the should occur before changing passwords
return true;
}
async performSubmitActions(
newMasterKeyHash: string,
newMasterKey: MasterKey,
newUserKey: [UserKey, EncString],
) {
// Override in sub-class
}
async strongPassword(): Promise<boolean> {
if (this.masterPassword == null || this.masterPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
if (this.masterPassword.length < this.minimumLength) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength),
});
return false;
}
if (this.masterPassword !== this.masterPasswordRetype) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPassDoesntMatch"),
});
return false;
}
const strengthResult = this.passwordStrengthResult;
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
this.masterPassword,
this.enforcedPolicyOptions,
)
) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
});
return false;
}
const weakPassword = strengthResult != null && strengthResult.score < 3;
if (weakPassword && this.leakedPassword) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakAndExposedMasterPassword" },
content: { key: "weakAndBreachedMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return false;
}
} else {
if (weakPassword) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakMasterPassword" },
content: { key: "weakMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return false;
}
}
if (this.leakedPassword) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "exposedMasterPassword" },
content: { key: "exposedMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return false;
}
}
}
return true;
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
acceptButtonText: { key: "logOut" },
type: "warning",
});
if (confirmed) {
this.messagingService.send("logout");
}
}
getStrengthResult(result: any) {
this.passwordStrengthResult = result;
}
getPasswordScoreText(event: PasswordColorText) {
this.color = event.color;
this.text = event.text;
}
}

View File

@@ -1,300 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, of } from "rxjs";
import { filter, first, switchMap, tap } from "rxjs/operators";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
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 { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@Directive()
export class SetPasswordComponent extends BaseChangePasswordComponent implements OnInit {
syncLoading = true;
showPassword = false;
hint = "";
orgSsoIdentifier: string = null;
orgId: string;
resetPasswordAutoEnroll = false;
onSuccessfulChangePassword: () => Promise<void>;
successRoute = "vault";
activeUserId: UserId;
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
ForceSetPasswordReason = ForceSetPasswordReason;
constructor(
protected accountService: AccountService,
protected dialogService: DialogService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected messagingService: MessagingService,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected platformUtilsService: PlatformUtilsService,
protected policyApiService: PolicyApiServiceAbstraction,
protected policyService: PolicyService,
protected route: ActivatedRoute,
protected router: Router,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected syncService: SyncService,
protected toastService: ToastService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
async ngOnInit() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
super.ngOnInit();
await this.syncService.fullSync(true);
this.syncLoading = false;
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
);
this.route.queryParams
.pipe(
first(),
switchMap((qParams) => {
if (qParams.identifier != null) {
return of(qParams.identifier);
} else {
// Try to get orgSsoId from state as fallback
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
}
}),
filter((orgSsoId) => orgSsoId != null),
tap((orgSsoId: string) => {
this.orgSsoIdentifier = orgSsoId;
}),
switchMap((orgSsoId: string) => this.organizationApiService.getAutoEnrollStatus(orgSsoId)),
tap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => {
this.orgId = orgAutoEnrollStatusResponse.id;
this.resetPasswordAutoEnroll = orgAutoEnrollStatusResponse.resetPasswordEnabled;
}),
switchMap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) =>
// Must get org id from response to get master password policy options
this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
orgAutoEnrollStatusResponse.id,
),
),
tap((masterPasswordPolicyOptions: MasterPasswordPolicyOptions) => {
this.enforcedPolicyOptions = masterPasswordPolicyOptions;
}),
)
.subscribe({
error: () => {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
},
});
}
async setupSubmitActions() {
this.kdfConfig = DEFAULT_KDF_CONFIG;
return true;
}
async performSubmitActions(
masterPasswordHash: string,
masterKey: MasterKey,
userKey: [UserKey, EncString],
) {
let keysRequest: KeysRequest | null = null;
let newKeyPair: [string, EncString] | null = null;
if (
this.forceSetPasswordReason !=
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
) {
// Existing JIT provisioned user in a MP encryption org setting first password
// Users in this state will not already have a user asymmetric key pair so must create it for them
// We don't want to re-create the user key pair if the user already has one (TDE user case)
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
const existingUserPrivateKey = (await firstValueFrom(
this.keyService.userPrivateKey$(this.activeUserId),
)) as Uint8Array;
const existingUserPublicKey = await firstValueFrom(
this.keyService.userPublicKey$(this.activeUserId),
);
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
newKeyPair = [
existingUserPublicKeyB64,
await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]),
];
} else {
newKeyPair = await this.keyService.makeKeyPair(userKey[0]);
}
keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
}
const request = new SetPasswordRequest(
masterPasswordHash,
userKey[1].encryptedString,
this.hint,
this.orgSsoIdentifier,
keysRequest,
this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions
this.kdfConfig.iterations,
);
try {
if (this.resetPasswordAutoEnroll) {
this.formPromise = this.masterPasswordApiService
.setPassword(request)
.then(async () => {
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
return this.organizationApiService.getKeys(this.orgId);
})
.then(async (response) => {
if (response == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user key with organization public key
const userKey = await this.keyService.getUserKey();
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
userKey,
publicKey,
);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.masterPasswordHash = masterPasswordHash;
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
this.orgId,
this.activeUserId,
resetRequest,
);
});
} else {
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
});
}
await this.formPromise;
if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulChangePassword();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
}
} catch {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
}
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
}
protected async onSetPasswordSuccess(
masterKey: MasterKey,
userKey: [UserKey, EncString],
keyPair: [string, EncString] | null,
) {
// Clear force set password reason to allow navigation back to vault.
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.None,
this.activeUserId,
);
// User now has a password so update account decryption options in state
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
userDecryptionOpts.hasMasterPassword = true;
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
await this.keyService.setUserKey(userKey[0], this.activeUserId);
// Set private key only for new JIT provisioned users in MP encryption orgs
// Existing TDE users will have private key set on sync or on login
if (
keyPair !== null &&
this.forceSetPasswordReason !=
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
) {
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
}
const localMasterKeyHash = await this.keyService.hashMasterKey(
this.masterPassword,
masterKey,
HashPurpose.LocalAuthorization,
);
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
}
}

View File

@@ -1,141 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { Verification } from "@bitwarden/common/auth/types/verification";
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@Directive()
export class UpdatePasswordComponent extends BaseChangePasswordComponent {
hint: string;
key: string;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showPassword = false;
currentMasterPassword: string;
onSuccessfulChangePassword: () => Promise<void>;
constructor(
protected router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
keyService: KeyService,
messagingService: MessagingService,
private masterPasswordApiService: MasterPasswordApiService,
private userVerificationService: UserVerificationService,
private logService: LogService,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
toastService: ToastService,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
}
async cancel() {
await this.router.navigate(["/vault"]);
}
async setupSubmitActions(): Promise<boolean> {
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
const secret: Verification = {
type: VerificationType.MasterPassword,
secret: this.currentMasterPassword,
};
try {
await this.userVerificationService.verifyUser(secret);
} catch (e) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
return false;
}
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
return true;
}
async performSubmitActions(
newMasterKeyHash: string,
newMasterKey: MasterKey,
newUserKey: [UserKey, EncString],
) {
try {
// Create Request
const request = new PasswordRequest();
request.masterPasswordHash = await this.keyService.hashMasterKey(
this.currentMasterPassword,
await this.keyService.getOrDeriveMasterKey(this.currentMasterPassword),
);
request.newMasterPasswordHash = newMasterKeyHash;
request.key = newUserKey[1].encryptedString;
// Update user's password
await this.masterPasswordApiService.postPassword(request);
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("masterPasswordChanged"),
message: this.i18nService.t("logBackIn"),
});
if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulChangePassword();
} else {
this.messagingService.send("logout");
}
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,232 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@Directive()
export class UpdateTempPasswordComponent extends BaseChangePasswordComponent implements OnInit {
hint: string;
key: string;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showPassword = false;
reason: ForceSetPasswordReason = ForceSetPasswordReason.None;
verification: MasterPasswordVerification = {
type: VerificationType.MasterPassword,
secret: "",
};
onSuccessfulChangePassword: () => Promise<any>;
get requireCurrentPassword(): boolean {
return this.reason === ForceSetPasswordReason.WeakMasterPassword;
}
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
keyService: KeyService,
messagingService: MessagingService,
private masterPasswordApiService: MasterPasswordApiService,
private syncService: SyncService,
private logService: LogService,
private userVerificationService: UserVerificationService,
protected router: Router,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
accountService: AccountService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
toastService: ToastService,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
async ngOnInit() {
await this.syncService.fullSync(true);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId));
// If we somehow end up here without a reason, go back to the home page
if (this.reason == ForceSetPasswordReason.None) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/"]);
return;
}
await super.ngOnInit();
}
get masterPasswordWarningText(): string {
if (this.reason == ForceSetPasswordReason.WeakMasterPassword) {
return this.i18nService.t("updateWeakMasterPasswordWarning");
} else if (this.reason == ForceSetPasswordReason.TdeOffboarding) {
return this.i18nService.t("tdeDisabledMasterPasswordRequired");
} else {
return this.i18nService.t("updateMasterPasswordWarning");
}
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
}
async setupSubmitActions(): Promise<boolean> {
const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
);
this.email = email;
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
return true;
}
async submit() {
// Validation
if (!(await this.strongPassword())) {
return;
}
if (!(await this.setupSubmitActions())) {
return;
}
try {
// Create new key and hash new password
const newMasterKey = await this.keyService.makeMasterKey(
this.masterPassword,
this.email.trim().toLowerCase(),
this.kdfConfig,
);
const newPasswordHash = await this.keyService.hashMasterKey(
this.masterPassword,
newMasterKey,
);
// Grab user key
const userKey = await this.keyService.getUserKey();
// Encrypt user key with new master key
const newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
newMasterKey,
userKey,
);
await this.performSubmitActions(newPasswordHash, newMasterKey, newProtectedUserKey);
} catch (e) {
this.logService.error(e);
}
}
async performSubmitActions(
masterPasswordHash: string,
masterKey: MasterKey,
userKey: [UserKey, EncString],
) {
try {
switch (this.reason) {
case ForceSetPasswordReason.AdminForcePasswordReset:
this.formPromise = this.updateTempPassword(masterPasswordHash, userKey);
break;
case ForceSetPasswordReason.WeakMasterPassword:
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
break;
case ForceSetPasswordReason.TdeOffboarding:
this.formPromise = this.updateTdeOffboardingPassword(masterPasswordHash, userKey);
break;
}
await this.formPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("updatedMasterPassword"),
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.None,
userId,
);
if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulChangePassword();
} else {
this.messagingService.send("logout");
}
} catch (e) {
this.logService.error(e);
}
}
private async updateTempPassword(masterPasswordHash: string, userKey: [UserKey, EncString]) {
const request = new UpdateTempPasswordRequest();
request.key = userKey[1].encryptedString;
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.masterPasswordApiService.putUpdateTempPassword(request);
}
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
const request = await this.userVerificationService.buildRequest(
this.verification,
PasswordRequest,
);
request.masterPasswordHint = this.hint;
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = userKey[1].encryptedString;
return this.masterPasswordApiService.postPassword(request);
}
private async updateTdeOffboardingPassword(
masterPasswordHash: string,
userKey: [UserKey, EncString],
) {
const request = new UpdateTdeOffboardingPasswordRequest();
request.key = userKey[1].encryptedString;
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
}
}

View File

@@ -1,28 +0,0 @@
@if (initializing) {
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
} @else {
<bit-callout
*ngIf="this.forceSetPasswordReason !== ForceSetPasswordReason.AdminForcePasswordReset"
type="warning"
>{{ "changePasswordWarning" | i18n }}</bit-callout
>
<auth-input-password
[flow]="inputPasswordFlow"
[email]="email"
[userId]="userId"
[loading]="submitting"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[inlineButtons]="true"
[primaryButtonText]="{ key: 'changeMasterPassword' }"
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
[secondaryButtonText]="secondaryButtonText()"
(onSecondaryButtonClick)="logOut()"
>
</auth-input-password>
}

View File

@@ -1,203 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
InputPasswordComponent,
InputPasswordFlow,
PasswordInputResult,
} from "@bitwarden/auth/angular";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import {
AnonLayoutWrapperDataService,
DialogService,
ToastService,
Icons,
CalloutComponent,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { ChangePasswordService } from "./change-password.service.abstraction";
/**
* Change Password Component
*
* NOTE: The change password component uses the input-password component which will show the
* current password input form in some flows, although it could be left off. This is intentional
* and by design to maintain a strong security posture as some flows could have the user
* end up at a change password without having one before.
*/
@Component({
selector: "auth-change-password",
templateUrl: "change-password.component.html",
imports: [InputPasswordComponent, I18nPipe, CalloutComponent, CommonModule],
})
export class ChangePasswordComponent implements OnInit {
@Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword;
activeAccount: Account | null = null;
email?: string;
userId?: UserId;
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
initializing = true;
submitting = false;
formPromise?: Promise<any>;
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
protected readonly ForceSetPasswordReason = ForceSetPasswordReason;
constructor(
private accountService: AccountService,
private changePasswordService: ChangePasswordService,
private i18nService: I18nService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private organizationInviteService: OrganizationInviteService,
private messagingService: MessagingService,
private policyService: PolicyService,
private toastService: ToastService,
private syncService: SyncService,
private dialogService: DialogService,
private logService: LogService,
) {}
async ngOnInit() {
this.activeAccount = await firstValueFrom(this.accountService.activeAccount$);
if (!this.activeAccount) {
throw new Error("No active active account found while trying to change passwords.");
}
this.userId = this.activeAccount.id;
this.email = this.activeAccount.email;
if (!this.userId) {
throw new Error("userId not found");
}
this.masterPasswordPolicyOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$(this.userId),
);
this.forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(this.userId),
);
if (this.forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset) {
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageIcon: Icons.LockIcon,
pageTitle: { key: "updateMasterPassword" },
pageSubtitle: { key: "accountRecoveryUpdateMasterPasswordSubtitle" },
});
} else if (this.forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) {
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageIcon: Icons.LockIcon,
pageTitle: { key: "updateMasterPassword" },
pageSubtitle: { key: "updateMasterPasswordSubtitle" },
maxWidth: "lg",
});
}
this.initializing = false;
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
acceptButtonText: { key: "logOut" },
type: "warning",
});
if (confirmed) {
await this.organizationInviteService.clearOrganizationInvitation();
if (this.changePasswordService.clearDeeplinkState) {
await this.changePasswordService.clearDeeplinkState();
}
// TODO: PM-23515 eventually use the logout service instead of messaging service once it is available without circular dependencies
this.messagingService.send("logout");
}
}
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
this.submitting = true;
try {
if (passwordInputResult.rotateUserKey) {
if (this.activeAccount == null) {
throw new Error("activeAccount not found");
}
if (
passwordInputResult.currentPassword == null ||
passwordInputResult.newPasswordHint == null
) {
throw new Error("currentPassword or newPasswordHint not found");
}
await this.syncService.fullSync(true);
await this.changePasswordService.rotateUserKeyMasterPasswordAndEncryptedData(
passwordInputResult.currentPassword,
passwordInputResult.newPassword,
this.activeAccount,
passwordInputResult.newPasswordHint,
);
} else {
if (!this.userId) {
throw new Error("userId not found");
}
if (this.forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset) {
await this.changePasswordService.changePasswordForAccountRecovery(
passwordInputResult,
this.userId,
);
} else {
await this.changePasswordService.changePassword(passwordInputResult, this.userId);
}
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("masterPasswordChanged"),
});
// TODO: PM-23515 eventually use the logout service instead of messaging service once it is available without circular dependencies
this.messagingService.send("logout");
}
} catch (error) {
this.logService.error(error);
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("errorOccurred"),
});
} finally {
this.submitting = false;
}
}
/**
* Shows the logout button in the case of admin force reset password or weak password upon login.
*/
protected secondaryButtonText(): { key: string } | undefined {
return this.forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
this.forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword
? { key: "logOut" }
: undefined;
}
}

View File

@@ -41,7 +41,7 @@ describe("DefaultChangePasswordService", () => {
newServerMasterKeyHash: "newServerMasterKeyHash",
newLocalMasterKeyHash: "newLocalMasterKeyHash",
kdfConfig: new PBKDF2KdfConfig(),
kdf: new PBKDF2KdfConfig(),
};
const decryptedUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;

View File

@@ -1,3 +1,5 @@
import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { PasswordInputResult } from "@bitwarden/auth/angular";
@@ -5,10 +7,8 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
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 { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { ChangePasswordService } from "./change-password.service.abstraction";
@@ -18,7 +18,7 @@ export class DefaultChangePasswordService implements ChangePasswordService {
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {}
) { }
async rotateUserKeyMasterPasswordAndEncryptedData(
currentPassword: string,
@@ -29,60 +29,20 @@ export class DefaultChangePasswordService implements ChangePasswordService {
throw new Error("rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web");
}
private async preparePasswordChange(
passwordInputResult: PasswordInputResult,
userId: UserId | null,
request: PasswordRequest | UpdateTempPasswordRequest,
): Promise<[UserKey, EncString]> {
if (!userId) {
throw new Error("userId not found");
}
if (
!passwordInputResult.currentMasterKey ||
!passwordInputResult.currentServerMasterKeyHash ||
!passwordInputResult.newMasterKey ||
!passwordInputResult.newServerMasterKeyHash ||
passwordInputResult.newPasswordHint == null
) {
throw new Error("invalid PasswordInputResult credentials, could not change password");
}
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
passwordInputResult.currentMasterKey,
userId,
);
if (decryptedUserKey == null) {
throw new Error("Could not decrypt user key");
}
const newKeyValue = await this.keyService.encryptUserKeyWithMasterKey(
passwordInputResult.newMasterKey,
decryptedUserKey,
);
if (request instanceof PasswordRequest) {
request.masterPasswordHash = passwordInputResult.currentServerMasterKeyHash;
request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash;
request.masterPasswordHint = passwordInputResult.newPasswordHint;
} else if (request instanceof UpdateTempPasswordRequest) {
request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash;
request.masterPasswordHint = passwordInputResult.newPasswordHint;
}
return newKeyValue;
}
async changePassword(passwordInputResult: PasswordInputResult, userId: UserId | null) {
const request = new PasswordRequest();
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("Can't find UserKey");
}
const salt = await firstValueFrom(this.masterPasswordService.saltForAccount$(userId));
const authenticationData = await this.masterPasswordService.makeMasterPasswordAuthenticationData(passwordInputResult.newPassword, passwordInputResult.kdf, salt);
const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(passwordInputResult.newPassword, passwordInputResult.kdf, passwordInputResult.salt, userKey);
const newMasterKeyEncryptedUserKey = await this.preparePasswordChange(
passwordInputResult,
userId,
request,
);
const request = new PasswordRequest(authenticationData, unlockData);
request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string;
const oldAuthenticationData = await this.masterPasswordService.makeMasterPasswordAuthenticationData(passwordInputResult.currentPassword, passwordInputResult.kdf, salt);
request.masterPasswordHash = oldAuthenticationData.masterPasswordAuthenticationHash;
request.masterPasswordHint = passwordInputResult.newPasswordHint;
try {
await this.masterPasswordApiService.postPassword(request);
@@ -92,15 +52,16 @@ export class DefaultChangePasswordService implements ChangePasswordService {
}
async changePasswordForAccountRecovery(passwordInputResult: PasswordInputResult, userId: UserId) {
const request = new UpdateTempPasswordRequest();
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("Can't find UserKey");
}
const salt = await firstValueFrom(this.masterPasswordService.saltForAccount$(userId));
const authenticationData = await this.masterPasswordService.makeMasterPasswordAuthenticationData(passwordInputResult.newPassword, passwordInputResult.kdf, salt);
const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(passwordInputResult.newPassword, passwordInputResult.kdf, passwordInputResult.salt, userKey);
const newMasterKeyEncryptedUserKey = await this.preparePasswordChange(
passwordInputResult,
userId,
request,
);
request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string;
const request = new UpdateTempPasswordRequest(authenticationData, unlockData);
request.masterPasswordHint = passwordInputResult.newPasswordHint;
try {
// TODO: PM-23047 will look to consolidate this into the change password endpoint.

View File

@@ -1,3 +1,2 @@
export * from "./change-password.component";
export * from "./change-password.service.abstraction";
export * from "./default-change-password.service";

View File

@@ -18,8 +18,10 @@ import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/mode
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 { MasterPasswordAuthenticationData } 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 { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
@@ -44,7 +46,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
) {}
) { }
async setInitialPassword(
credentials: SetInitialPasswordCredentials,
@@ -52,11 +54,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
userId: UserId,
): Promise<void> {
const {
newMasterKey,
newServerMasterKeyHash,
newLocalMasterKeyHash,
newPasswordHint,
kdfConfig,
orgSsoIdentifier,
orgId,
resetPasswordAutoEnroll,
@@ -74,13 +72,18 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
throw new Error("userType not found. Could not set password.");
}
const masterKeyEncryptedUserKey = await this.makeMasterKeyEncryptedUserKey(
newMasterKey,
userId,
const userKey = await this.keyService.makeUserKeyV1Raw();
const authenticationData = await this.masterPasswordService.makeMasterPasswordAuthenticationData(
credentials.initialPassword,
credentials.kdf,
credentials.salt,
);
const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(
credentials.initialPassword,
credentials.kdf,
credentials.salt,
userKey,
);
if (masterKeyEncryptedUserKey == null || !masterKeyEncryptedUserKey[1].encryptedString) {
throw new Error("masterKeyEncryptedUserKey not found. Could not set password.");
}
let keyPair: [string, EncString] | null = null;
let keysRequest: KeysRequest | null = null;
@@ -113,12 +116,12 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
existingUserPublicKeyB64,
await this.encryptService.wrapDecapsulationKey(
existingUserPrivateKey,
masterKeyEncryptedUserKey[0],
userKey,
),
];
} else {
// New key pair
keyPair = await this.keyService.makeKeyPair(masterKeyEncryptedUserKey[0]);
keyPair = await this.keyService.makeKeyPair(userKey);
}
if (keyPair == null) {
@@ -132,13 +135,11 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
}
const request = new SetPasswordRequest(
newServerMasterKeyHash,
masterKeyEncryptedUserKey[1].encryptedString,
authenticationData,
unlockData,
newPasswordHint,
orgSsoIdentifier,
keysRequest,
kdfConfig.kdfType,
kdfConfig.iterations,
);
await this.masterPasswordApiService.setPassword(request);
@@ -147,10 +148,18 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
// User now has a password so update account decryption options in state
// TODO: REMOVE when masterKey is no longer used
const masterKey = await this.keyService.makeMasterKey(credentials.initialPassword, credentials.salt, credentials.kdf);
const localMasterKeyHash = await this.keyService.hashMasterKey(
credentials.initialPassword,
masterKey,
HashPurpose.LocalAuthorization
) as string;
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
await this.updateAccountDecryptionProperties(
newMasterKey,
kdfConfig,
masterKeyEncryptedUserKey,
masterKey,
credentials.kdf,
userKey,
userId,
);
@@ -165,34 +174,17 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
}
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
if (resetPasswordAutoEnroll) {
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
await this.handleResetPasswordAutoEnroll(authenticationData, orgId, userId);
}
}
private async makeMasterKeyEncryptedUserKey(
masterKey: MasterKey,
userId: UserId,
): Promise<[UserKey, EncString]> {
let masterKeyEncryptedUserKey: [UserKey, EncString] | null = null;
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey);
} else {
masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
}
return masterKeyEncryptedUserKey;
}
private async updateAccountDecryptionProperties(
/** @deprecated */
masterKey: MasterKey,
kdfConfig: KdfConfig,
masterKeyEncryptedUserKey: [UserKey, EncString],
userKey: UserKey,
userId: UserId,
) {
const userDecryptionOpts = await firstValueFrom(
@@ -201,12 +193,13 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
userDecryptionOpts.hasMasterPassword = true;
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
// TODO: Remove when masterKey is no longer used
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
await this.keyService.setUserKey(userKey, userId);
}
private async handleResetPasswordAutoEnroll(
masterKeyHash: string,
authenticationData: MasterPasswordAuthenticationData,
orgId: string,
userId: UserId,
) {
@@ -238,7 +231,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
}
const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
enrollmentRequest.masterPasswordHash = masterKeyHash;
enrollmentRequest.masterPasswordHash = authenticationData.masterPasswordAuthenticationHash;
enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString;
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
@@ -252,7 +245,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
credentials: SetInitialPasswordTdeOffboardingCredentials,
userId: UserId,
) {
const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials;
const { initialPassword, kdf, salt, newPasswordHint } = credentials;
for (const [key, value] of Object.entries(credentials)) {
if (value == null) {
throw new Error(`${key} not found. Could not set password.`);
@@ -262,24 +255,24 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
if (userId == null) {
throw new Error("userId not found. Could not set password.");
}
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("userKey not found. Could not set password.");
}
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
newMasterKey,
const authenticationData = await this.masterPasswordService.makeMasterPasswordAuthenticationData(
initialPassword,
kdf,
salt,
);
const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(
initialPassword,
kdf,
salt,
userKey,
);
if (!newMasterKeyEncryptedUserKey[1].encryptedString) {
throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password.");
}
const request = new UpdateTdeOffboardingPasswordRequest();
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
request.newMasterPasswordHash = newServerMasterKeyHash;
const request = new UpdateTdeOffboardingPasswordRequest(authenticationData, unlockData);
request.masterPasswordHint = newPasswordHint;
await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);

View File

@@ -157,8 +157,6 @@ describe("DefaultSetInitialPasswordService", () => {
credentials.newPasswordHint,
credentials.orgSsoIdentifier,
keysRequest,
credentials.kdfConfig.kdfType,
credentials.kdfConfig.iterations,
);
enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();

View File

@@ -85,7 +85,7 @@ export class SetInitialPasswordComponent implements OnInit {
private syncService: SyncService,
private toastService: ToastService,
private validationService: ValidationService,
) {}
) { }
async ngOnInit() {
await this.syncService.fullSync(true);
@@ -209,10 +209,9 @@ export class SetInitialPasswordComponent implements OnInit {
private async setInitialPassword(passwordInputResult: PasswordInputResult) {
const ctx = "Could not set initial password.";
assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx);
assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx);
assertTruthy(passwordInputResult.newLocalMasterKeyHash, "newLocalMasterKeyHash", ctx);
assertTruthy(passwordInputResult.kdfConfig, "kdfConfig", ctx);
assertTruthy(passwordInputResult.newPassword, "newPassword", ctx);
assertTruthy(passwordInputResult.kdf, "kdf", ctx);
assertTruthy(passwordInputResult.salt, "salt", ctx);
assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx);
assertTruthy(this.orgId, "orgId", ctx);
assertTruthy(this.userType, "userType", ctx);
@@ -222,11 +221,10 @@ export class SetInitialPasswordComponent implements OnInit {
try {
const credentials: SetInitialPasswordCredentials = {
newMasterKey: passwordInputResult.newMasterKey,
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
initialPassword: passwordInputResult.newPassword,
kdf: passwordInputResult.kdf,
salt: passwordInputResult.salt,
newPasswordHint: passwordInputResult.newPasswordHint,
kdfConfig: passwordInputResult.kdfConfig,
orgSsoIdentifier: this.orgSsoIdentifier,
orgId: this.orgId,
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
@@ -251,15 +249,17 @@ export class SetInitialPasswordComponent implements OnInit {
private async setInitialPasswordTdeOffboarding(passwordInputResult: PasswordInputResult) {
const ctx = "Could not set initial password.";
assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx);
assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx);
assertTruthy(passwordInputResult.newPassword, "newPassword", ctx);
assertTruthy(passwordInputResult.kdf, "kdf", ctx);
assertTruthy(passwordInputResult.salt, "salt", ctx);
assertTruthy(this.userId, "userId", ctx);
assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish
try {
const credentials: SetInitialPasswordTdeOffboardingCredentials = {
newMasterKey: passwordInputResult.newMasterKey,
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
initialPassword: passwordInputResult.newPassword,
kdf: passwordInputResult.kdf,
salt: passwordInputResult.salt,
newPasswordHint: passwordInputResult.newPasswordHint,
};

View File

@@ -1,5 +1,5 @@
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";
export const _SetInitialPasswordUserType = {
@@ -42,19 +42,21 @@ export const SetInitialPasswordUserType: Readonly<{
}> = Object.freeze(_SetInitialPasswordUserType);
export interface SetInitialPasswordCredentials {
newMasterKey: MasterKey;
newServerMasterKeyHash: string;
newLocalMasterKeyHash: string;
initialPassword: string,
kdf: KdfConfig,
salt: MasterPasswordSalt,
newPasswordHint: string;
kdfConfig: KdfConfig;
orgSsoIdentifier: string;
orgId: string;
resetPasswordAutoEnroll: boolean;
}
export interface SetInitialPasswordTdeOffboardingCredentials {
newMasterKey: MasterKey;
newServerMasterKeyHash: string;
initialPassword: string;
kdf: KdfConfig;
salt: MasterPasswordSalt;
newPasswordHint: string;
}

View File

@@ -423,11 +423,11 @@ const safeProviders: SafeProvider[] = [
provide: LOGOUT_CALLBACK,
useFactory:
(messagingService: MessagingServiceAbstraction) =>
async (logoutReason: LogoutReason, userId?: string) => {
return Promise.resolve(
messagingService.send("logout", { logoutReason: logoutReason, userId: userId }),
);
},
async (logoutReason: LogoutReason, userId?: string) => {
return Promise.resolve(
messagingService.send("logout", { logoutReason: logoutReason, userId: userId }),
);
},
deps: [MessagingServiceAbstraction],
}),
safeProvider({
@@ -1024,6 +1024,7 @@ const safeProviders: SafeProvider[] = [
EncryptService,
LogService,
CryptoFunctionServiceAbstraction,
AccountServiceAbstraction
],
}),
safeProvider({
@@ -1614,4 +1615,4 @@ const safeProviders: SafeProvider[] = [
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: safeProviders,
})
export class JslibServicesModule {}
export class JslibServicesModule { }