1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

Merge branch 'main' into auth/pm-8111/browser-refresh-login-component

This commit is contained in:
Alec Rippberger
2024-10-02 09:45:07 -05:00
172 changed files with 4678 additions and 919 deletions

View File

@@ -9,7 +9,9 @@
'tw-min-h-[calc(100vh-54px)]': clientType === 'desktop',
}"
>
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-w-[128px] [&>*]:tw-align-top"></bit-icon>
<a *ngIf="!hideLogo" [routerLink]="['/']" class="tw-w-[128px] [&>*]:tw-align-top">
<bit-icon [icon]="logo"></bit-icon>
</a>
<div class="tw-text-center">
<div class="tw-mx-auto tw-max-w-28 sm:tw-max-w-32">

View File

@@ -1,5 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core";
import { RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { ClientType } from "@bitwarden/common/enums";
@@ -15,7 +16,7 @@ import { BitwardenLogo, VaultIcon } from "../icons";
standalone: true,
selector: "auth-anon-layout",
templateUrl: "./anon-layout.component.html",
imports: [IconModule, CommonModule, TypographyModule, SharedModule],
imports: [IconModule, CommonModule, TypographyModule, SharedModule, RouterModule],
})
export class AnonLayoutComponent implements OnInit, OnChanges {
@Input() title: string;

View File

@@ -49,5 +49,9 @@ export * from "./user-verification/user-verification-dialog.component";
export * from "./user-verification/user-verification-dialog.types";
export * from "./user-verification/user-verification-form-input.component";
// vault timeout input
// lock
export * from "./lock/lock.component";
export * from "./lock/lock-component.service";
// vault timeout
export * from "./vault-timeout-input/vault-timeout-input.component";

View File

@@ -0,0 +1,48 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
export enum BiometricsDisableReason {
NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem",
EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable",
SystemBiometricsUnavailable = "SystemBiometricsUnavailable",
}
// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics"
export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption];
export const UnlockOption = Object.freeze({
MasterPassword: "masterPassword",
Pin: "pin",
Biometrics: "biometrics",
}) satisfies { [Prop in keyof UnlockOptions as Capitalize<Prop>]: Prop };
export type UnlockOptions = {
masterPassword: {
enabled: boolean;
};
pin: {
enabled: boolean;
};
biometrics: {
enabled: boolean;
disableReason: BiometricsDisableReason | null;
};
};
/**
* The LockComponentService is a service which allows the single libs/auth LockComponent to delegate all
* client specific functionality to client specific services implementations of LockComponentService.
*/
export abstract class LockComponentService {
// Extension
abstract getBiometricsError(error: any): string | null;
abstract getPreviousUrl(): string | null;
// Desktop only
abstract isWindowVisible(): Promise<boolean>;
abstract getBiometricsUnlockBtnText(): string;
// Multi client
abstract getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions>;
}

View File

@@ -0,0 +1,191 @@
<ng-template #loading>
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
</ng-template>
<ng-container *ngIf="unlockOptions; else loading">
<!-- Biometrics Unlock -->
<ng-container
*ngIf="unlockOptions.biometrics.enabled && activeUnlockOption === UnlockOption.Biometrics"
>
<button
type="button"
bitButton
buttonType="primary"
class="tw-mb-3"
[disabled]="unlockingViaBiometrics"
[loading]="unlockingViaBiometrics"
block
(click)="unlockViaBiometrics()"
>
<span> {{ biometricUnlockBtnText | i18n }}</span>
</button>
<div class="tw-flex tw-flex-col tw-space-y-3">
<p class="tw-text-center tw-mb-0">{{ "or" | i18n }}</p>
<ng-container *ngIf="unlockOptions.pin.enabled">
<button
type="button"
bitButton
buttonType="secondary"
block
(click)="activeUnlockOption = UnlockOption.Pin"
>
{{ "unlockWithPin" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
<button
type="button"
bitButton
buttonType="secondary"
block
(click)="activeUnlockOption = UnlockOption.MasterPassword"
>
{{ "unlockWithMasterPassword" | i18n }}
</button>
</ng-container>
<button type="button" bitButton block (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</ng-container>
<!-- PIN Unlock -->
<ng-container *ngIf="unlockOptions.pin.enabled && activeUnlockOption === UnlockOption.Pin">
<form [bitSubmit]="submit" [formGroup]="formGroup">
<bit-form-field>
<bit-label>{{ "pin" | i18n }}</bit-label>
<input
type="password"
formControlName="pin"
bitInput
appAutofocus
name="pin"
class="tw-font-mono"
required
appInputVerbatim
/>
<button
type="button"
bitIconButton
bitSuffix
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
</bit-form-field>
<div class="tw-flex tw-flex-col tw-space-y-3">
<button type="submit" bitButton bitFormButton buttonType="primary" block>
{{ "unlock" | i18n }}
</button>
<p class="tw-text-center">{{ "or" | i18n }}</p>
<ng-container *ngIf="unlockOptions.biometrics.enabled">
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
block
(click)="activeUnlockOption = UnlockOption.Biometrics"
>
<span> {{ biometricUnlockBtnText | i18n }}</span>
</button>
</ng-container>
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
block
(click)="activeUnlockOption = UnlockOption.MasterPassword"
>
{{ "unlockWithMasterPassword" | i18n }}
</button>
</ng-container>
<button type="button" bitButton bitFormButton block (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</form>
</ng-container>
<!-- MP Unlock -->
<ng-container
*ngIf="
unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword
"
>
<form [bitSubmit]="submit" [formGroup]="formGroup">
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
type="password"
formControlName="masterPassword"
bitInput
appAutofocus
name="masterPassword"
class="tw-font-mono"
required
appInputVerbatim
/>
<button
type="button"
bitIconButton
bitSuffix
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
<!-- [attr.aria-pressed]="showPassword" -->
</bit-form-field>
<div class="tw-flex tw-flex-col tw-space-y-3">
<button type="submit" bitButton bitFormButton buttonType="primary" block>
{{ "unlock" | i18n }}
</button>
<p class="tw-text-center">{{ "or" | i18n }}</p>
<ng-container *ngIf="unlockOptions.biometrics.enabled">
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
block
(click)="activeUnlockOption = UnlockOption.Biometrics"
>
<span> {{ biometricUnlockBtnText | i18n }}</span>
</button>
</ng-container>
<ng-container *ngIf="unlockOptions.pin.enabled">
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
block
(click)="activeUnlockOption = UnlockOption.Pin"
>
{{ "unlockWithPin" | i18n }}
</button>
</ng-container>
<button type="button" bitButton bitFormButton block (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</form>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,638 @@
import { CommonModule } from "@angular/common";
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
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 { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
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 { ClientType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { SyncService } from "@bitwarden/common/platform/sync";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import {
AsyncActionsModule,
ButtonModule,
DialogService,
FormFieldModule,
IconButtonModule,
ToastService,
} from "@bitwarden/components";
import { BiometricStateService } from "@bitwarden/key-management";
import { PinServiceAbstraction } from "../../common/abstractions";
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
import {
UnlockOption,
LockComponentService,
UnlockOptions,
UnlockOptionValue,
} from "./lock-component.service";
const BroadcasterSubscriptionId = "LockComponent";
const clientTypeToSuccessRouteRecord: Partial<Record<ClientType, string>> = {
[ClientType.Web]: "vault",
[ClientType.Desktop]: "vault",
[ClientType.Browser]: "/tabs/current",
};
@Component({
selector: "bit-lock",
templateUrl: "lock.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
ReactiveFormsModule,
ButtonModule,
FormFieldModule,
AsyncActionsModule,
IconButtonModule,
],
})
export class LockV2Component implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
activeAccount: { id: UserId | undefined } & AccountInfo;
clientType: ClientType;
ClientType = ClientType;
unlockOptions: UnlockOptions = null;
UnlockOption = UnlockOption;
private _activeUnlockOptionBSubject: BehaviorSubject<UnlockOptionValue> =
new BehaviorSubject<UnlockOptionValue>(null);
activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable();
set activeUnlockOption(value: UnlockOptionValue) {
this._activeUnlockOptionBSubject.next(value);
}
get activeUnlockOption(): UnlockOptionValue {
return this._activeUnlockOptionBSubject.value;
}
private invalidPinAttempts = 0;
biometricUnlockBtnText: string;
// masterPassword = "";
showPassword = false;
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
forcePasswordResetRoute = "update-temp-password";
formGroup: FormGroup;
// Desktop properties:
private deferFocus: boolean = null;
private biometricAsked = false;
// Browser extension properties:
private isInitialLockScreen = (window as any).previousPopupUrl == null;
defaultUnlockOptionSetForUser = false;
unlockingViaBiometrics = false;
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private userVerificationService: UserVerificationService,
private cryptoService: CryptoService,
private platformUtilsService: PlatformUtilsService,
private router: Router,
private dialogService: DialogService,
private messagingService: MessagingService,
private biometricStateService: BiometricStateService,
private ngZone: NgZone,
private i18nService: I18nService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private logService: LogService,
private deviceTrustService: DeviceTrustServiceAbstraction,
private syncService: SyncService,
private policyService: InternalPolicyService,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private formBuilder: FormBuilder,
private toastService: ToastService,
private lockComponentService: LockComponentService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
// desktop deps
private broadcasterService: BroadcasterService,
) {}
async ngOnInit() {
this.listenForActiveUnlockOptionChanges();
// Listen for active account changes
this.listenForActiveAccountChanges();
// Identify client
this.clientType = this.platformUtilsService.getClientType();
if (this.clientType === "desktop") {
await this.desktopOnInit();
}
}
// Base component methods
private listenForActiveUnlockOptionChanges() {
this.activeUnlockOption$
.pipe(takeUntil(this.destroy$))
.subscribe((activeUnlockOption: UnlockOptionValue) => {
if (activeUnlockOption === UnlockOption.Pin) {
this.buildPinForm();
} else if (activeUnlockOption === UnlockOption.MasterPassword) {
this.buildMasterPasswordForm();
}
});
}
private buildMasterPasswordForm() {
this.formGroup = this.formBuilder.group(
{
masterPassword: ["", [Validators.required]],
},
{ updateOn: "submit" },
);
}
private buildPinForm() {
this.formGroup = this.formBuilder.group(
{
pin: ["", [Validators.required]],
},
{ updateOn: "submit" },
);
}
private listenForActiveAccountChanges() {
this.accountService.activeAccount$
.pipe(
switchMap((account) => {
return this.handleActiveAccountChange(account);
}),
takeUntil(this.destroy$),
)
.subscribe();
}
private async handleActiveAccountChange(activeAccount: { id: UserId | undefined } & AccountInfo) {
this.activeAccount = activeAccount;
this.resetDataOnActiveAccountChange();
this.setEmailAsPageSubtitle(activeAccount.email);
this.unlockOptions = await firstValueFrom(
this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id),
);
this.setDefaultActiveUnlockOption(this.unlockOptions);
if (this.unlockOptions.biometrics.enabled) {
await this.handleBiometricsUnlockEnabled();
}
}
private resetDataOnActiveAccountChange() {
this.defaultUnlockOptionSetForUser = false;
this.unlockOptions = null;
this.activeUnlockOption = null;
this.formGroup = null; // new form group will be created based on new active unlock option
// Desktop properties:
this.biometricAsked = false;
}
private setEmailAsPageSubtitle(email: string) {
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageSubtitle: {
subtitle: email,
translate: false,
},
});
}
private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions) {
// Priorities should be Biometrics > Pin > Master Password for speed
if (unlockOptions.biometrics.enabled) {
this.activeUnlockOption = UnlockOption.Biometrics;
} else if (unlockOptions.pin.enabled) {
this.activeUnlockOption = UnlockOption.Pin;
} else if (unlockOptions.masterPassword.enabled) {
this.activeUnlockOption = UnlockOption.MasterPassword;
}
}
private async handleBiometricsUnlockEnabled() {
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
const autoPromptBiometrics = await firstValueFrom(
this.biometricStateService.promptAutomatically$,
);
// TODO: PM-12546 - we need to make our biometric autoprompt experience consistent between the
// desktop and extension.
if (this.clientType === "desktop") {
if (autoPromptBiometrics) {
await this.desktopAutoPromptBiometrics();
}
}
if (this.clientType === "browser") {
if (
this.unlockOptions.biometrics.enabled &&
autoPromptBiometrics &&
this.isInitialLockScreen // only autoprompt biometrics on initial lock screen
) {
await this.unlockViaBiometrics();
}
}
}
// Note: this submit method is only used for unlock methods that require a form and user input.
// For biometrics unlock, the method is called directly.
submit = async (): Promise<void> => {
if (this.activeUnlockOption === UnlockOption.Pin) {
return await this.unlockViaPin();
}
await this.unlockViaMasterPassword();
};
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.activeAccount.id });
}
}
async unlockViaBiometrics(): Promise<void> {
this.unlockingViaBiometrics = true;
if (!this.unlockOptions.biometrics.enabled) {
this.unlockingViaBiometrics = false;
return;
}
try {
await this.biometricStateService.setUserPromptCancelled();
const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Biometric,
this.activeAccount.id,
);
// If user cancels biometric prompt, userKey is undefined.
if (userKey) {
await this.setUserKeyAndContinue(userKey, false);
}
this.unlockingViaBiometrics = false;
} catch (e) {
// Cancelling is a valid action.
if (e?.message === "canceled") {
this.unlockingViaBiometrics = false;
return;
}
let biometricTranslatedErrorDesc;
if (this.clientType === "browser") {
const biometricErrorDescTranslationKey = this.lockComponentService.getBiometricsError(e);
if (biometricErrorDescTranslationKey) {
biometricTranslatedErrorDesc = this.i18nService.t(biometricErrorDescTranslationKey);
}
}
// if no translation key found, show generic error message
if (!biometricTranslatedErrorDesc) {
biometricTranslatedErrorDesc = this.i18nService.t("unexpectedError");
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "error" },
content: biometricTranslatedErrorDesc,
acceptButtonText: { key: "tryAgain" },
type: "danger",
});
if (confirmed) {
// try again
await this.unlockViaBiometrics();
}
this.unlockingViaBiometrics = false;
}
}
togglePassword() {
this.showPassword = !this.showPassword;
const input = document.getElementById(
this.unlockOptions.pin.enabled ? "pin" : "masterPassword",
);
if (this.ngZone.isStable) {
input.focus();
} else {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus());
}
}
private validatePin(): boolean {
if (this.formGroup.invalid) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("pinRequired"),
});
return false;
}
return true;
}
private async unlockViaPin() {
if (!this.validatePin()) {
return;
}
const pin = this.formGroup.controls.pin.value;
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
try {
const userKey = await this.pinService.decryptUserKeyWithPin(pin, this.activeAccount.id);
if (userKey) {
await this.setUserKeyAndContinue(userKey);
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 validateMasterPassword(): boolean {
if (this.formGroup.invalid) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
return true;
}
private async unlockViaMasterPassword() {
if (!this.validateMasterPassword()) {
return;
}
const masterPassword = this.formGroup.controls.masterPassword.value;
const verification = {
type: VerificationType.MasterPassword,
secret: masterPassword,
} as MasterPasswordVerification;
let passwordValid = false;
let masterPasswordVerificationResponse: MasterPasswordVerificationResponse;
try {
masterPasswordVerificationResponse =
await this.userVerificationService.verifyUserByMasterPassword(
verification,
this.activeAccount.id,
this.activeAccount.email,
);
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(
masterPasswordVerificationResponse.policyOptions,
);
passwordValid = true;
} catch (e) {
this.logService.error(e);
}
if (!passwordValid) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidMasterPassword"),
});
return;
}
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterPasswordVerificationResponse.masterKey,
);
await this.setUserKeyAndContinue(userKey, true);
}
private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) {
await this.cryptoService.setUserKey(key, this.activeAccount.id);
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
await this.deviceTrustService.trustDeviceIfRequired(this.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,
);
await 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.clientType === "browser") {
const previousUrl = this.lockComponentService.getPreviousUrl();
if (previousUrl) {
await this.router.navigateByUrl(previousUrl);
}
}
// determine success route based on client type
const successRoute = clientTypeToSuccessRouteRecord[this.clientType];
await this.router.navigate([successRoute]);
}
/**
* 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 masterPassword = this.formGroup.controls.masterPassword.value;
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
this.activeAccount.email,
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
masterPassword,
this.enforcedMasterPasswordOptions,
);
}
// -----------------------------------------------------------------------------------------------
// Desktop methods:
// -----------------------------------------------------------------------------------------------
async desktopOnInit() {
// TODO: move this into a WindowService and subscribe to messages via MessageListener service.
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
}
private async desktopAutoPromptBiometrics() {
if (!this.unlockOptions?.biometrics?.enabled || this.biometricAsked) {
return;
}
// prevent the biometric prompt from showing if the user has already cancelled it
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
return;
}
const windowVisible = await this.lockComponentService.isWindowVisible();
if (windowVisible) {
this.biometricAsked = true;
await this.unlockViaBiometrics();
}
}
onWindowHidden() {
this.showPassword = false;
}
private focusInput() {
if (this.unlockOptions) {
document.getElementById(this.unlockOptions.pin.enabled ? "pin" : "masterPassword")?.focus();
}
}
// -----------------------------------------------------------------------------------------------
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
if (this.clientType === "desktop") {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
}
}

View File

@@ -15,6 +15,7 @@ import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-con
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
@@ -33,6 +34,7 @@ describe("DefaultSetPasswordJitService", () => {
let apiService: MockProxy<ApiService>;
let cryptoService: MockProxy<CryptoService>;
let encryptService: MockProxy<EncryptService>;
let i18nService: MockProxy<I18nService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
@@ -43,6 +45,7 @@ describe("DefaultSetPasswordJitService", () => {
beforeEach(() => {
apiService = mock<ApiService>();
cryptoService = mock<CryptoService>();
encryptService = mock<EncryptService>();
i18nService = mock<I18nService>();
kdfConfigService = mock<KdfConfigService>();
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
@@ -53,6 +56,7 @@ describe("DefaultSetPasswordJitService", () => {
sut = new DefaultSetPasswordJitService(
apiService,
cryptoService,
encryptService,
i18nService,
kdfConfigService,
masterPasswordService,
@@ -168,7 +172,7 @@ describe("DefaultSetPasswordJitService", () => {
}
cryptoService.userKey$.mockReturnValue(of(userKey));
cryptoService.rsaEncrypt.mockResolvedValue(userKeyEncString);
encryptService.rsaEncrypt.mockResolvedValue(userKeyEncString);
organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
undefined,
@@ -210,7 +214,7 @@ describe("DefaultSetPasswordJitService", () => {
// Assert
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalled();

View File

@@ -14,6 +14,7 @@ import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
@@ -29,6 +30,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
constructor(
protected apiService: ApiService,
protected cryptoService: CryptoService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
@@ -157,7 +159,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
throw new Error("userKey not found. Could not handle reset password auto enroll.");
}
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.masterPasswordHash = masterKeyHash;

View File

@@ -226,7 +226,7 @@ describe("WebAuthnLoginStrategy", () => {
const mockUserKey = new SymmetricCryptoKey(mockUserKeyArray) as UserKey;
encryptService.decryptToBytes.mockResolvedValue(mockPrfPrivateKey);
cryptoService.rsaDecrypt.mockResolvedValue(mockUserKeyArray);
encryptService.rsaDecrypt.mockResolvedValue(mockUserKeyArray);
// Act
await webAuthnLoginStrategy.logIn(webAuthnCredentials);
@@ -244,9 +244,9 @@ describe("WebAuthnLoginStrategy", () => {
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedPrivateKey,
webAuthnCredentials.prfKey,
);
expect(cryptoService.rsaDecrypt).toHaveBeenCalledTimes(1);
expect(cryptoService.rsaDecrypt).toHaveBeenCalledWith(
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
expect(encryptService.rsaDecrypt).toHaveBeenCalledTimes(1);
expect(encryptService.rsaDecrypt).toHaveBeenCalledWith(
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey,
mockPrfPrivateKey,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
@@ -273,7 +273,7 @@ describe("WebAuthnLoginStrategy", () => {
// Assert
expect(encryptService.decryptToBytes).not.toHaveBeenCalled();
expect(cryptoService.rsaDecrypt).not.toHaveBeenCalled();
expect(encryptService.rsaDecrypt).not.toHaveBeenCalled();
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
});
@@ -325,7 +325,7 @@ describe("WebAuthnLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
cryptoService.rsaDecrypt.mockResolvedValue(null);
encryptService.rsaDecrypt.mockResolvedValue(null);
// Act
await webAuthnLoginStrategy.logIn(webAuthnCredentials);

View File

@@ -4,6 +4,7 @@ import { Jsonify } from "type-fest";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
@@ -86,8 +87,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
);
// decrypt user key with private key
const userKey = await this.cryptoService.rsaDecrypt(
webAuthnPrfOption.encryptedUserKey.encryptedString,
const userKey = await this.encryptService.rsaDecrypt(
new EncString(webAuthnPrfOption.encryptedUserKey.encryptedString),
privateKey,
);

View File

@@ -6,6 +6,7 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@@ -24,6 +25,7 @@ describe("AuthRequestService", () => {
let masterPasswordService: FakeMasterPasswordService;
const appIdService = mock<AppIdService>();
const cryptoService = mock<CryptoService>();
const encryptService = mock<EncryptService>();
const apiService = mock<ApiService>();
let mockPrivateKey: Uint8Array;
@@ -40,6 +42,7 @@ describe("AuthRequestService", () => {
accountService,
masterPasswordService,
cryptoService,
encryptService,
apiService,
stateProvider,
);
@@ -82,7 +85,7 @@ describe("AuthRequestService", () => {
describe("approveOrDenyAuthRequest", () => {
beforeEach(() => {
cryptoService.rsaEncrypt.mockResolvedValue({
encryptService.rsaEncrypt.mockResolvedValue({
encryptedString: "ENCRYPTED_STRING",
} as EncString);
appIdService.getAppId.mockResolvedValue("APP_ID");
@@ -108,7 +111,7 @@ describe("AuthRequestService", () => {
new AuthRequestResponse({ id: "123", publicKey: "KEY" }),
);
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
});
it("should use the user key if the master key and hash do not exist", async () => {
@@ -119,7 +122,7 @@ describe("AuthRequestService", () => {
new AuthRequestResponse({ id: "123", publicKey: "KEY" }),
);
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
});
});
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
@@ -211,7 +214,7 @@ describe("AuthRequestService", () => {
const mockDecryptedUserKeyBytes = new Uint8Array(64);
const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey;
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
// Act
const result = await sut.decryptPubKeyEncryptedUserKey(
@@ -220,7 +223,10 @@ describe("AuthRequestService", () => {
);
// Assert
expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey);
expect(encryptService.rsaDecrypt).toBeCalledWith(
new EncString(mockPubKeyEncryptedUserKey),
mockPrivateKey,
);
expect(result).toEqual(mockDecryptedUserKey);
});
});
@@ -238,7 +244,7 @@ describe("AuthRequestService", () => {
const mockDecryptedMasterKeyHashBytes = new Uint8Array(64);
const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes);
cryptoService.rsaDecrypt
encryptService.rsaDecrypt
.mockResolvedValueOnce(mockDecryptedMasterKeyBytes)
.mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes);
@@ -250,14 +256,14 @@ describe("AuthRequestService", () => {
);
// Assert
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith(
1,
mockPubKeyEncryptedMasterKey,
new EncString(mockPubKeyEncryptedMasterKey),
mockPrivateKey,
);
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith(
2,
mockPubKeyEncryptedMasterKeyHash,
new EncString(mockPubKeyEncryptedMasterKeyHash),
mockPrivateKey,
);
expect(result.masterKey).toEqual(mockDecryptedMasterKey);

View File

@@ -10,7 +10,9 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
AUTH_REQUEST_DISK_LOCAL,
@@ -44,6 +46,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private apiService: ApiService,
private stateProvider: StateProvider,
) {
@@ -102,7 +105,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
if (masterKey && masterKeyHash) {
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
encryptedMasterKeyHash = await this.encryptService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey,
);
@@ -112,7 +115,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
const encryptedKey = await this.encryptService.rsaEncrypt(keyToEncrypt, pubKey);
const response = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
@@ -161,8 +164,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
pubKeyEncryptedUserKey: string,
privateKey: Uint8Array,
): Promise<UserKey> {
const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedUserKey,
const decryptedUserKeyBytes = await this.encryptService.rsaDecrypt(
new EncString(pubKeyEncryptedUserKey),
privateKey,
);
@@ -174,13 +177,13 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
pubKeyEncryptedMasterKeyHash: string,
privateKey: Uint8Array,
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKey,
const decryptedMasterKeyArrayBuffer = await this.encryptService.rsaDecrypt(
new EncString(pubKeyEncryptedMasterKey),
privateKey,
);
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKeyHash,
const decryptedMasterKeyHashArrayBuffer = await this.encryptService.rsaDecrypt(
new EncString(pubKeyEncryptedMasterKeyHash),
privateKey,
);