1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 11:13:44 +00:00

Re-add biometric unlock on app start to Windows Hello

This commit is contained in:
Bernd Schoolmann
2025-08-28 15:14:02 +02:00
parent 3d3a02ccb9
commit 2ea35579d5
9 changed files with 87 additions and 1 deletions

View File

@@ -80,6 +80,19 @@
<small class="help-block" *ngIf="this.form.value.biometric && this.isMac">{{
"additionalTouchIdSettings" | i18n
}}</small>
<!-- Textbox for biometric app start -->
<div class="form-group tw-mt-2" *ngIf="this.form.value.biometric && this.isWindows">
<div class="checkbox">
<label for="allowBiometricUnlockOnAppRestart">
<input
id="allowBiometricUnlockOnAppRestart"
type="checkbox"
formControlName="allowBiometricUnlockOnAppRestart"
/>
{{ "allowBiometricUnlockOnAppRestart" | i18n }}
</label>
</div>
</div>
</div>
<div
class="form-group"

View File

@@ -134,6 +134,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
vaultTimeoutAction: [VaultTimeoutAction.Lock],
pin: [null as boolean | null],
biometric: false,
allowBiometricUnlockOnAppRestart: false,
autoPromptBiometrics: false,
// Account Preferences
clearClipboard: [null],
@@ -348,6 +349,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
),
pin: this.userHasPinSet,
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
allowBiometricUnlockOnAppRestart: await this.biometricsService.hasPersistentKey(
activeAccount.id,
),
autoPromptBiometrics: await firstValueFrom(this.biometricStateService.promptAutomatically$),
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
minimizeOnCopyToClipboard: await firstValueFrom(this.desktopSettingsService.minimizeOnCopy$),
@@ -440,6 +444,25 @@ export class SettingsComponent implements OnInit, OnDestroy {
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.allowBiometricUnlockOnAppRestart.valueChanges
.pipe(
concatMap(async (enabled) => {
const userKey = await firstValueFrom(this.keyService.userKey$(activeAccount.id));
if (enabled) {
if (!(await this.biometricsService.hasPersistentKey(activeAccount.id))) {
await this.biometricsService.enrollPersistent(activeAccount.id, userKey);
}
} else {
await this.biometricsService.deleteBiometricUnlockKeyForUser(activeAccount.id);
await this.biometricsService.setBiometricProtectedUnlockKeyForUser(
activeAccount.id,
userKey,
);
}
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.enableBrowserIntegration.valueChanges
.pipe(takeUntil(this.destroy$))

View File

@@ -13,4 +13,6 @@ export abstract class DesktopBiometricsService extends BiometricsService {
): Promise<void>;
abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void>;
abstract setupBiometrics(): Promise<void>;
abstract enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void>;
abstract hasPersistentKey(userId: UserId): Promise<boolean>;
}

View File

@@ -51,6 +51,13 @@ export class MainBiometricsIPCListener {
return await this.biometricService.setShouldAutopromptNow(message.data as boolean);
case BiometricAction.GetShouldAutoprompt:
return await this.biometricService.getShouldAutopromptNow();
case BiometricAction.HasPersistentKey:
return await this.biometricService.hasPersistentKey(message.userId as UserId);
case BiometricAction.EnrollPersistent:
return await this.biometricService.enrollPersistent(
message.userId as UserId,
SymmetricCryptoKey.fromString(message.key as string),
);
default:
return;
}

View File

@@ -128,4 +128,12 @@ export class MainBiometricsService extends DesktopBiometricsService {
async canEnableBiometricUnlock(): Promise<boolean> {
return true;
}
async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
return await this.osBiometricsService.enrollPersistent(userId, key);
}
async hasPersistentKey(userId: UserId): Promise<boolean> {
return await this.osBiometricsService.hasPersistentKey(userId);
}
}

View File

@@ -68,4 +68,12 @@ export class RendererBiometricsService extends DesktopBiometricsService {
BiometricsStatus.ManualSetupNeeded,
].includes(biometricStatus);
}
async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
return await ipc.keyManagement.biometric.enrollPersistent(userId, key.toBase64());
}
async hasPersistentKey(userId: UserId): Promise<boolean> {
return await ipc.keyManagement.biometric.hasPersistentKey(userId);
}
}

View File

@@ -50,6 +50,17 @@ const biometric = {
action: BiometricAction.SetShouldAutoprompt,
data: should,
} satisfies BiometricMessage),
enrollPersistent: (userId: string, keyB64: string): Promise<void> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.EnrollPersistent,
userId: userId,
key: keyB64,
} satisfies BiometricMessage),
hasPersistentKey: (userId: string): Promise<boolean> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.HasPersistentKey,
userId: userId,
} satisfies BiometricMessage),
};
export default {

View File

@@ -1849,6 +1849,9 @@
"lockWithMasterPassOnRestart1": {
"message": "Lock with master password on restart"
},
"allowBiometricUnlockOnAppRestart": {
"message": "Allow biometric unlock on app restart"
},
"deleteAccount": {
"message": "Delete account"
},

View File

@@ -13,6 +13,9 @@ export enum BiometricAction {
GetShouldAutoprompt = "getShouldAutoprompt",
SetShouldAutoprompt = "setShouldAutoprompt",
EnrollPersistent = "enrollPersistent",
HasPersistentKey = "hasPersistentKey",
}
export type BiometricMessage =
@@ -22,7 +25,15 @@ export type BiometricMessage =
key: string;
}
| {
action: Exclude<BiometricAction, BiometricAction.SetKeyForUser>;
action: BiometricAction.EnrollPersistent;
userId: string;
key: string;
}
| {
action: Exclude<
BiometricAction,
BiometricAction.SetKeyForUser | BiometricAction.EnrollPersistent
>;
userId?: string;
data?: any;
};