1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

[PM-10741] Refactor biometrics interface & add dynamic status (#10973)

This commit is contained in:
Bernd Schoolmann
2025-01-08 10:46:00 +01:00
committed by GitHub
parent 0bd988dac8
commit 72121cda94
66 changed files with 1840 additions and 1459 deletions

View File

@@ -3,8 +3,4 @@
*/
export { LockComponent } from "./lock/components/lock.component";
export {
LockComponentService,
BiometricsDisableReason,
UnlockOptions,
} from "./lock/services/lock-component.service";
export { LockComponentService, UnlockOptions } from "./lock/services/lock-component.service";

View File

@@ -86,12 +86,13 @@
<p class="tw-text-center">{{ "or" | i18n }}</p>
<ng-container *ngIf="unlockOptions.biometrics.enabled">
<ng-container *ngIf="showBiometrics">
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
[disabled]="!biometricsAvailable"
block
(click)="activeUnlockOption = UnlockOption.Biometrics"
>
@@ -156,12 +157,13 @@
<p class="tw-text-center">{{ "or" | i18n }}</p>
<ng-container *ngIf="unlockOptions.biometrics.enabled">
<ng-container *ngIf="showBiometrics">
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
[disabled]="!biometricsAvailable"
block
(click)="activeUnlockOption = UnlockOption.Biometrics"
>

View File

@@ -4,7 +4,16 @@ 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 {
BehaviorSubject,
firstValueFrom,
interval,
mergeMap,
Subject,
switchMap,
take,
takeUntil,
} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
@@ -27,7 +36,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
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 { UserKey } from "@bitwarden/common/types/key";
@@ -42,6 +50,8 @@ import {
import {
KeyService,
BiometricStateService,
BiometricsService,
BiometricsStatus,
UserAsymmetricKeysRegenerationService,
} from "@bitwarden/key-management";
@@ -115,9 +125,6 @@ export class LockComponent implements OnInit, OnDestroy {
private deferFocus: boolean = null;
private biometricAsked = false;
// Browser extension properties:
private isInitialLockScreen = (window as any).previousPopupUrl == null;
defaultUnlockOptionSetForUser = false;
unlockingViaBiometrics = false;
@@ -144,6 +151,8 @@ export class LockComponent implements OnInit, OnDestroy {
private toastService: ToastService,
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
private biometricService: BiometricsService,
private lockComponentService: LockComponentService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
@@ -157,14 +166,31 @@ export class LockComponent implements OnInit, OnDestroy {
// Listen for active account changes
this.listenForActiveAccountChanges();
this.listenForUnlockOptionsChanges();
// Identify client
this.clientType = this.platformUtilsService.getClientType();
if (this.clientType === "desktop") {
await this.desktopOnInit();
} else if (this.clientType === ClientType.Browser) {
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
}
}
private listenForUnlockOptionsChanges() {
interval(1000)
.pipe(
mergeMap(async () => {
this.unlockOptions = await firstValueFrom(
this.lockComponentService.getAvailableUnlockOptions$(this.activeAccount.id),
);
}),
takeUntil(this.destroy$),
)
.subscribe();
}
// Base component methods
private listenForActiveUnlockOptionChanges() {
this.activeUnlockOption$
@@ -234,7 +260,6 @@ export class LockComponent implements OnInit, OnDestroy {
this.unlockOptions = null;
this.activeUnlockOption = null;
this.formGroup = null; // new form group will be created based on new active unlock option
this.isInitialLockScreen = true;
// Desktop properties:
this.biometricAsked = false;
@@ -276,8 +301,9 @@ export class LockComponent implements OnInit, OnDestroy {
if (
this.unlockOptions.biometrics.enabled &&
autoPromptBiometrics &&
this.isInitialLockScreen // only autoprompt biometrics on initial lock screen
(await this.biometricService.getShouldAutopromptNow())
) {
await this.biometricService.setShouldAutopromptNow(false);
await this.unlockViaBiometrics();
}
}
@@ -316,8 +342,7 @@ export class LockComponent implements OnInit, OnDestroy {
try {
await this.biometricStateService.setUserPromptCancelled();
const userKey = await this.keyService.getUserKeyFromStorage(
KeySuffixOptions.Biometric,
const userKey = await this.biometricService.unlockWithBiometricsForUser(
this.activeAccount.id,
);
@@ -587,6 +612,8 @@ export class LockComponent implements OnInit, OnDestroy {
// -----------------------------------------------------------------------------------------------
async desktopOnInit() {
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
// TODO: move this into a WindowService and subscribe to messages via MessageListener service.
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
@@ -617,6 +644,10 @@ export class LockComponent implements OnInit, OnDestroy {
return;
}
if (!(await this.biometricService.getShouldAutopromptNow())) {
return;
}
// prevent the biometric prompt from showing if the user has already cancelled it
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
return;
@@ -650,4 +681,47 @@ export class LockComponent implements OnInit, OnDestroy {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
}
get biometricsAvailable(): boolean {
return this.unlockOptions.biometrics.enabled;
}
get showBiometrics(): boolean {
return (
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.PlatformUnsupported &&
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.NotEnabledLocally
);
}
get biometricUnavailabilityReason(): string {
switch (this.unlockOptions.biometrics.biometricsStatus) {
case BiometricsStatus.Available:
return "";
case BiometricsStatus.UnlockNeeded:
return this.i18nService.t("biometricsStatusHelptextUnlockNeeded");
case BiometricsStatus.HardwareUnavailable:
return this.i18nService.t("biometricsStatusHelptextHardwareUnavailable");
case BiometricsStatus.AutoSetupNeeded:
return this.i18nService.t("biometricsStatusHelptextAutoSetupNeeded");
case BiometricsStatus.ManualSetupNeeded:
return this.i18nService.t("biometricsStatusHelptextManualSetupNeeded");
case BiometricsStatus.NotEnabledInConnectedDesktopApp:
return this.i18nService.t(
"biometricsStatusHelptextNotEnabledInDesktop",
this.activeAccount.email,
);
case BiometricsStatus.NotEnabledLocally:
return this.i18nService.t(
"biometricsStatusHelptextNotEnabledInDesktop",
this.activeAccount.email,
);
case BiometricsStatus.DesktopDisconnected:
return this.i18nService.t("biometricsStatusHelptextDesktopDisconnected");
default:
return (
this.i18nService.t("biometricsStatusHelptextUnavailableReasonUnknown") +
this.unlockOptions.biometrics.biometricsStatus
);
}
}
}

View File

@@ -1,12 +1,7 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
export enum BiometricsDisableReason {
NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem",
EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable",
SystemBiometricsUnavailable = "SystemBiometricsUnavailable",
}
import { BiometricsStatus } from "@bitwarden/key-management";
// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics"
export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption];
@@ -26,7 +21,7 @@ export type UnlockOptions = {
};
biometrics: {
enabled: boolean;
disableReason: BiometricsDisableReason | null;
biometricsStatus: BiometricsStatus;
};
};