1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 10:43:35 +00:00

[PM-25373] Windows native biometric rewrite (#16432)

* Extract windows biometrics v2 changes

Co-authored-by: Bernd Schoolmann <mail@quexten.com>

* Handle TDE edge cases

* Make windows rust code async and fix restoring focus freezes

* Add unit test coverage

---------

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
Thomas Avery
2025-10-20 14:47:15 -05:00
committed by GitHub
parent d2c6757626
commit f65e5d52c2
35 changed files with 1971 additions and 182 deletions

View File

@@ -142,6 +142,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
userHasPinSet: boolean;
pinEnabled$: Observable<boolean> = of(true);
isWindowsV2BiometricsEnabled: boolean = false;
form = this.formBuilder.group({
// Security
@@ -149,6 +150,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
vaultTimeoutAction: [VaultTimeoutAction.Lock],
pin: [null as boolean | null],
biometric: false,
requireMasterPasswordOnAppRestart: true,
autoPromptBiometrics: false,
// Account Preferences
clearClipboard: [null],
@@ -281,6 +283,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.isWindowsV2BiometricsEnabled = await this.biometricsService.isWindowsV2BiometricsEnabled();
this.vaultTimeoutOptions = await this.generateVaultTimeoutOptions();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
@@ -372,6 +376,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
),
pin: this.userHasPinSet,
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
requireMasterPasswordOnAppRestart: !(await this.biometricsService.hasPersistentKey(
activeAccount.id,
)),
autoPromptBiometrics: await firstValueFrom(this.biometricStateService.promptAutomatically$),
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
minimizeOnCopyToClipboard: await firstValueFrom(this.desktopSettingsService.minimizeOnCopy$),
@@ -479,6 +486,15 @@ export class SettingsComponent implements OnInit, OnDestroy {
)
.subscribe();
this.form.controls.requireMasterPasswordOnAppRestart.valueChanges
.pipe(
concatMap(async (value) => {
await this.updateRequireMasterPasswordOnAppRestartHandler(value, activeAccount.id);
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.enableBrowserIntegration.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((enabled) => {
@@ -588,6 +604,19 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.form.controls.pin.setValue(this.userHasPinSet, { emitEvent: false });
} else {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
// On Windows if a user turned off PIN without having a MP and has biometrics + require MP/PIN on restart enabled.
if (
this.isWindows &&
this.isWindowsV2BiometricsEnabled &&
this.supportsBiometric &&
this.form.value.requireMasterPasswordOnAppRestart &&
this.form.value.biometric &&
!this.userHasMasterPassword
) {
// Allow biometric unlock on app restart so the user doesn't get into a bad state.
await this.enrollPersistentBiometricIfNeeded(userId);
}
await this.pinService.unsetPin(userId);
}
}
@@ -639,6 +668,16 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Recommended settings for Windows Hello
this.form.controls.autoPromptBiometrics.setValue(false);
await this.biometricStateService.setPromptAutomatically(false);
if (this.isWindowsV2BiometricsEnabled) {
// If the user doesn't have a MP or PIN then they have to use biometrics on app restart.
if (!this.userHasMasterPassword && !this.userHasPinSet) {
// Allow biometric unlock on app restart so the user doesn't get into a bad state.
await this.enrollPersistentBiometricIfNeeded(activeUserId);
} else {
this.form.controls.requireMasterPasswordOnAppRestart.setValue(true);
}
}
} else if (this.isLinux) {
// Similar to Windows
this.form.controls.autoPromptBiometrics.setValue(false);
@@ -656,6 +695,37 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
}
async updateRequireMasterPasswordOnAppRestartHandler(enabled: boolean, userId: UserId) {
try {
await this.updateRequireMasterPasswordOnAppRestart(enabled, userId);
} catch (error) {
this.logService.error("Error updating require master password on app restart: ", error);
this.validationService.showError(error);
}
}
async updateRequireMasterPasswordOnAppRestart(enabled: boolean, userId: UserId) {
if (enabled) {
// Require master password or PIN on app restart
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
await this.biometricsService.deleteBiometricUnlockKeyForUser(userId);
await this.biometricsService.setBiometricProtectedUnlockKeyForUser(userId, userKey);
} else {
// Allow biometric unlock on app restart
await this.enrollPersistentBiometricIfNeeded(userId);
}
}
private async enrollPersistentBiometricIfNeeded(userId: UserId): Promise<void> {
if (!(await this.biometricsService.hasPersistentKey(userId))) {
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
await this.biometricsService.enrollPersistent(userId, userKey);
this.form.controls.requireMasterPasswordOnAppRestart.setValue(false, {
emitEvent: false,
});
}
}
async updateAutoPromptBiometrics() {
if (this.form.value.autoPromptBiometrics) {
await this.biometricStateService.setPromptAutomatically(true);