mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
* Move ownership of biometrics to key-management * Move biometrics ipc ownership to km * Move further files to km; split off preload / ipc to km * Fix linting * Fix linting * Fix tests * Fix tests * Update .github/CODEOWNERS Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update .github/CODEOWNERS Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Change ownership of native messaging to key-management * Move biometrics to libs/key-management * Add README to capital whitelist * Update package-lock.json * Move km to key-management * Move km to key-management * Fix build for cli * Import fixes * Apply prettier fix * Fix test * Import fixes * Import fixes * Update libs/key-management/README.md Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update libs/key-management/package.json Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update lock file * Change imports to top level km package --------- Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
375 lines
14 KiB
TypeScript
375 lines
14 KiB
TypeScript
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
|
|
import { Router } from "@angular/router";
|
|
import { firstValueFrom, Subject } from "rxjs";
|
|
import { concatMap, map, take, takeUntil } from "rxjs/operators";
|
|
|
|
import { PinServiceAbstraction, PinLockType } from "@bitwarden/auth/common";
|
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
|
import { InternalPolicyService } 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.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 {
|
|
MasterPasswordVerification,
|
|
MasterPasswordVerificationResponse,
|
|
} from "@bitwarden/common/auth/types/verification";
|
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
import { UserKey } from "@bitwarden/common/types/key";
|
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
|
import { DialogService, ToastService } from "@bitwarden/components";
|
|
import { BiometricStateService, BiometricsService } from "@bitwarden/key-management";
|
|
|
|
@Directive()
|
|
export class LockComponent implements OnInit, OnDestroy {
|
|
masterPassword = "";
|
|
pin = "";
|
|
showPassword = false;
|
|
email: string;
|
|
pinEnabled = false;
|
|
masterPasswordEnabled = false;
|
|
webVaultHostname = "";
|
|
formPromise: Promise<MasterPasswordVerificationResponse>;
|
|
supportsBiometric: boolean;
|
|
biometricLock: boolean;
|
|
|
|
private activeUserId: UserId;
|
|
protected successRoute = "vault";
|
|
protected forcePasswordResetRoute = "update-temp-password";
|
|
protected onSuccessfulSubmit: () => Promise<void>;
|
|
|
|
private invalidPinAttempts = 0;
|
|
private pinLockType: PinLockType;
|
|
|
|
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
|
|
|
private destroy$ = new Subject<void>();
|
|
|
|
constructor(
|
|
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
|
protected router: Router,
|
|
protected i18nService: I18nService,
|
|
protected platformUtilsService: PlatformUtilsService,
|
|
protected messagingService: MessagingService,
|
|
protected cryptoService: CryptoService,
|
|
protected vaultTimeoutService: VaultTimeoutService,
|
|
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
protected environmentService: EnvironmentService,
|
|
protected stateService: StateService,
|
|
protected apiService: ApiService,
|
|
protected logService: LogService,
|
|
protected ngZone: NgZone,
|
|
protected policyApiService: PolicyApiServiceAbstraction,
|
|
protected policyService: InternalPolicyService,
|
|
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
|
protected dialogService: DialogService,
|
|
protected deviceTrustService: DeviceTrustServiceAbstraction,
|
|
protected userVerificationService: UserVerificationService,
|
|
protected pinService: PinServiceAbstraction,
|
|
protected biometricStateService: BiometricStateService,
|
|
protected biometricsService: BiometricsService,
|
|
protected accountService: AccountService,
|
|
protected authService: AuthService,
|
|
protected kdfConfigService: KdfConfigService,
|
|
protected syncService: SyncService,
|
|
protected toastService: ToastService,
|
|
) {}
|
|
|
|
async ngOnInit() {
|
|
this.accountService.activeAccount$
|
|
.pipe(
|
|
concatMap(async (account) => {
|
|
this.activeUserId = account?.id;
|
|
await this.load(account?.id);
|
|
}),
|
|
takeUntil(this.destroy$),
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
async submit() {
|
|
if (this.pinEnabled) {
|
|
return await this.handlePinRequiredUnlock();
|
|
}
|
|
|
|
await this.handleMasterPasswordRequiredUnlock();
|
|
}
|
|
|
|
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", { userId: this.activeUserId });
|
|
}
|
|
}
|
|
|
|
async unlockBiometric(): Promise<boolean> {
|
|
if (!this.biometricLock) {
|
|
return;
|
|
}
|
|
|
|
await this.biometricStateService.setUserPromptCancelled();
|
|
const userKey = await this.cryptoService.getUserKeyFromStorage(
|
|
KeySuffixOptions.Biometric,
|
|
this.activeUserId,
|
|
);
|
|
|
|
if (userKey) {
|
|
await this.setUserKeyAndContinue(userKey, this.activeUserId, false);
|
|
}
|
|
|
|
return !!userKey;
|
|
}
|
|
|
|
async isBiometricUnlockAvailable(): Promise<boolean> {
|
|
if (!(await this.biometricsService.supportsBiometric())) {
|
|
return false;
|
|
}
|
|
return this.biometricsService.isBiometricUnlockAvailable();
|
|
}
|
|
|
|
togglePassword() {
|
|
this.showPassword = !this.showPassword;
|
|
const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword");
|
|
if (this.ngZone.isStable) {
|
|
input.focus();
|
|
} else {
|
|
this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus());
|
|
}
|
|
}
|
|
|
|
private async handlePinRequiredUnlock() {
|
|
if (this.pin == null || this.pin === "") {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("pinRequired"),
|
|
});
|
|
return;
|
|
}
|
|
|
|
return await this.doUnlockWithPin();
|
|
}
|
|
|
|
private async doUnlockWithPin() {
|
|
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
|
|
|
|
try {
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId);
|
|
|
|
if (userKey) {
|
|
await this.setUserKeyAndContinue(userKey, userId);
|
|
return; // successfully unlocked
|
|
}
|
|
|
|
// Failure state: invalid PIN or failed decryption
|
|
this.invalidPinAttempts++;
|
|
|
|
// Log user out if they have entered an invalid PIN too many times
|
|
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: null,
|
|
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
|
|
});
|
|
this.messagingService.send("logout");
|
|
return;
|
|
}
|
|
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("invalidPin"),
|
|
});
|
|
} catch {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("unexpectedError"),
|
|
});
|
|
}
|
|
}
|
|
|
|
private async handleMasterPasswordRequiredUnlock() {
|
|
if (this.masterPassword == null || this.masterPassword === "") {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("masterPasswordRequired"),
|
|
});
|
|
return;
|
|
}
|
|
await this.doUnlockWithMasterPassword();
|
|
}
|
|
|
|
private async doUnlockWithMasterPassword() {
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
|
|
const verification = {
|
|
type: VerificationType.MasterPassword,
|
|
secret: this.masterPassword,
|
|
} as MasterPasswordVerification;
|
|
|
|
let passwordValid = false;
|
|
let response: MasterPasswordVerificationResponse;
|
|
try {
|
|
this.formPromise = this.userVerificationService.verifyUserByMasterPassword(
|
|
verification,
|
|
userId,
|
|
this.email,
|
|
);
|
|
response = await this.formPromise;
|
|
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(
|
|
response.policyOptions,
|
|
);
|
|
passwordValid = true;
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
} finally {
|
|
this.formPromise = null;
|
|
}
|
|
|
|
if (!passwordValid) {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("invalidMasterPassword"),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
|
response.masterKey,
|
|
);
|
|
await this.setUserKeyAndContinue(userKey, userId, true);
|
|
}
|
|
|
|
private async setUserKeyAndContinue(
|
|
key: UserKey,
|
|
userId: UserId,
|
|
evaluatePasswordAfterUnlock = false,
|
|
) {
|
|
await this.cryptoService.setUserKey(key, userId);
|
|
|
|
// Now that we have a decrypted user key in memory, we can check if we
|
|
// need to establish trust on the current device
|
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
|
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
|
|
|
await this.doContinue(evaluatePasswordAfterUnlock);
|
|
}
|
|
|
|
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
|
|
await this.biometricStateService.resetUserPromptCancelled();
|
|
this.messagingService.send("unlocked");
|
|
|
|
if (evaluatePasswordAfterUnlock) {
|
|
try {
|
|
// If we do not have any saved policies, attempt to load them from the service
|
|
if (this.enforcedMasterPasswordOptions == undefined) {
|
|
this.enforcedMasterPasswordOptions = await firstValueFrom(
|
|
this.policyService.masterPasswordPolicyOptions$(),
|
|
);
|
|
}
|
|
|
|
if (this.requirePasswordChange()) {
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
await this.masterPasswordService.setForceSetPasswordReason(
|
|
ForceSetPasswordReason.WeakMasterPassword,
|
|
userId,
|
|
);
|
|
// 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.forcePasswordResetRoute]);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// Do not prevent unlock if there is an error evaluating policies
|
|
this.logService.error(e);
|
|
}
|
|
}
|
|
|
|
// Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service.
|
|
await this.syncService.fullSync(false);
|
|
|
|
if (this.onSuccessfulSubmit != null) {
|
|
await this.onSuccessfulSubmit();
|
|
} else if (this.router != 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.router.navigate([this.successRoute]);
|
|
}
|
|
}
|
|
|
|
private async load(userId: UserId) {
|
|
this.pinLockType = await this.pinService.getPinLockType(userId);
|
|
|
|
this.pinEnabled = await this.pinService.isPinDecryptionAvailable(userId);
|
|
|
|
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
|
|
|
|
this.supportsBiometric = await this.biometricsService.supportsBiometric();
|
|
this.biometricLock =
|
|
(await this.vaultTimeoutSettingsService.isBiometricLockSet()) &&
|
|
((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) ||
|
|
!this.platformUtilsService.supportsSecureStorage());
|
|
this.email = await firstValueFrom(
|
|
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
|
);
|
|
|
|
this.webVaultHostname = (await this.environmentService.getEnvironment()).getHostname();
|
|
}
|
|
|
|
/**
|
|
* Checks if the master password meets the enforced policy requirements
|
|
* If not, returns false
|
|
*/
|
|
private requirePasswordChange(): boolean {
|
|
if (
|
|
this.enforcedMasterPasswordOptions == undefined ||
|
|
!this.enforcedMasterPasswordOptions.enforceOnLogin
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
|
this.masterPassword,
|
|
this.email,
|
|
)?.score;
|
|
|
|
return !this.policyService.evaluateMasterPassword(
|
|
passwordStrength,
|
|
this.masterPassword,
|
|
this.enforcedMasterPasswordOptions,
|
|
);
|
|
}
|
|
}
|