1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-08 03:23:50 +00:00

[PM-18054] Chrome extension biometric unlock not functioning correctly with Windows Hello. (#14953)

* Chrome extension biometric unlock not functioning correctly with Windows Hello.

When unlocking via Windows Hello prompt, the popup have to be in the foreground. If it is not, even for short amount of time (few seconds), if later prompt confirmed, it won't return success when returning signed os key half.

* unit test coverage

* unit test coverage

* exclude test files from build

* use electron `setAlwaysOnTop` instead of toggle

* remove Windows os key half created with derive_key_material biometric function, that prompted Windows Hello.

Moves Windows hello prompt into getBiometricKey.
Witness key no longer needed.

* windows crate formatting

* remove biometric on app start for windows

* failing os biometrics windows unit tests

* cleanup of os biometrics windows unit tests

* increased coverage of os biometrics windows unit tests

* open Windows Hello prompt in the currently focused window, instead of always desktop app

* conflict resolution after merge, typescript lint issues, increased test coverage.

* backwards compatibility when require password on start was disabled

* biometric unlock cancellation and error handling

* biometric settings simplifications
This commit is contained in:
Maciej Zieniuk
2025-07-21 19:35:31 +02:00
committed by GitHub
parent 8b5e6adc37
commit 167fa9a7ab
11 changed files with 481 additions and 603 deletions

View File

@@ -126,13 +126,13 @@
{{ biometricText | i18n }}
</label>
</div>
<small class="help-block" *ngIf="this.form.value.biometric && !this.isLinux">{{
additionalBiometricSettingsText | i18n
<small class="help-block" *ngIf="this.form.value.biometric && this.isMac">{{
"additionalTouchIdSettings" | i18n
}}</small>
</div>
<div
class="form-group"
*ngIf="supportsBiometric && this.form.value.biometric && !this.isLinux"
*ngIf="supportsBiometric && this.form.value.biometric && this.isMac"
>
<div class="checkbox form-group-child">
<label for="autoPromptBiometrics">
@@ -142,7 +142,7 @@
formControlName="autoPromptBiometrics"
(change)="updateAutoPromptBiometrics()"
/>
{{ autoPromptBiometricsText | i18n }}
{{ "autoPromptTouchId" | i18n }}
</label>
</div>
</div>
@@ -152,7 +152,7 @@
supportsBiometric &&
this.form.value.biometric &&
(userHasMasterPassword || (this.form.value.pin && userHasPinSet)) &&
this.isWindows
false
"
>
<div class="checkbox form-group-child">
@@ -170,9 +170,6 @@
}
</label>
</div>
<small class="help-block form-group-child" *ngIf="isWindows">{{
"recommendedForSecurity" | i18n
}}</small>
</div>
</ng-container>
</div>

View File

@@ -271,74 +271,46 @@ describe("SettingsComponent", () => {
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(true);
});
it("require password or pin on app start message when RemoveUnlockWithPin policy is disabled and pin set and windows desktop", async () => {
const policy = new Policy();
policy.type = PolicyType.RemoveUnlockWithPin;
policy.enabled = false;
policyService.policiesByType$.mockReturnValue(of([policy]));
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
i18nService.t.mockImplementation((id: string) => {
if (id === "requirePasswordOnStart") {
return "Require password or pin on app start";
} else if (id === "requirePasswordWithoutPinOnStart") {
return "Require password on app start";
}
return "";
describe("windows desktop", () => {
beforeEach(() => {
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
// Recreate component to apply the correct device
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
});
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
await component.ngOnInit();
fixture.detectChanges();
it("require password or pin on app start not visible when RemoveUnlockWithPin policy is disabled and pin set and windows desktop", async () => {
const policy = new Policy();
policy.type = PolicyType.RemoveUnlockWithPin;
policy.enabled = false;
policyService.policiesByType$.mockReturnValue(of([policy]));
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
By.css("label[for='requirePasswordOnStart']"),
);
expect(requirePasswordOnStartLabelElement).not.toBeNull();
expect(requirePasswordOnStartLabelElement.children).toHaveLength(1);
expect(requirePasswordOnStartLabelElement.children[0].name).toBe("input");
expect(requirePasswordOnStartLabelElement.children[0].attributes).toMatchObject({
id: "requirePasswordOnStart",
type: "checkbox",
await component.ngOnInit();
fixture.detectChanges();
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
By.css("label[for='requirePasswordOnStart']"),
);
expect(requirePasswordOnStartLabelElement).toBeNull();
});
const textNodes = requirePasswordOnStartLabelElement.childNodes
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
.map((node) => node.nativeNode.wholeText?.trim());
expect(textNodes).toContain("Require password or pin on app start");
});
it("require password on app start message when RemoveUnlockWithPin policy is enabled and pin set and windows desktop", async () => {
const policy = new Policy();
policy.type = PolicyType.RemoveUnlockWithPin;
policy.enabled = true;
policyService.policiesByType$.mockReturnValue(of([policy]));
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
i18nService.t.mockImplementation((id: string) => {
if (id === "requirePasswordOnStart") {
return "Require password or pin on app start";
} else if (id === "requirePasswordWithoutPinOnStart") {
return "Require password on app start";
}
return "";
it("require password on app start not visible when RemoveUnlockWithPin policy is enabled and pin set and windows desktop", async () => {
const policy = new Policy();
policy.type = PolicyType.RemoveUnlockWithPin;
policy.enabled = true;
policyService.policiesByType$.mockReturnValue(of([policy]));
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
await component.ngOnInit();
fixture.detectChanges();
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
By.css("label[for='requirePasswordOnStart']"),
);
expect(requirePasswordOnStartLabelElement).toBeNull();
});
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
await component.ngOnInit();
fixture.detectChanges();
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
By.css("label[for='requirePasswordOnStart']"),
);
expect(requirePasswordOnStartLabelElement).not.toBeNull();
expect(requirePasswordOnStartLabelElement.children).toHaveLength(1);
expect(requirePasswordOnStartLabelElement.children[0].name).toBe("input");
expect(requirePasswordOnStartLabelElement.children[0].attributes).toMatchObject({
id: "requirePasswordOnStart",
type: "checkbox",
});
const textNodes = requirePasswordOnStartLabelElement.childNodes
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
.map((node) => node.nativeNode.wholeText?.trim());
expect(textNodes).toContain("Require password on app start");
});
});

View File

@@ -78,6 +78,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
showOpenAtLoginOption = false;
isWindows: boolean;
isLinux: boolean;
isMac: boolean;
enableTrayText: string;
enableTrayDescText: string;
@@ -170,31 +171,33 @@ export class SettingsComponent implements OnInit, OnDestroy {
private configService: ConfigService,
private validationService: ValidationService,
) {
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
this.isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
// Workaround to avoid ghosting trays https://github.com/electron/electron/issues/17622
this.requireEnableTray = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
const trayKey = isMac ? "enableMenuBar" : "enableTray";
const trayKey = this.isMac ? "enableMenuBar" : "enableTray";
this.enableTrayText = this.i18nService.t(trayKey);
this.enableTrayDescText = this.i18nService.t(trayKey + "Desc");
const minToTrayKey = isMac ? "enableMinToMenuBar" : "enableMinToTray";
const minToTrayKey = this.isMac ? "enableMinToMenuBar" : "enableMinToTray";
this.enableMinToTrayText = this.i18nService.t(minToTrayKey);
this.enableMinToTrayDescText = this.i18nService.t(minToTrayKey + "Desc");
const closeToTrayKey = isMac ? "enableCloseToMenuBar" : "enableCloseToTray";
const closeToTrayKey = this.isMac ? "enableCloseToMenuBar" : "enableCloseToTray";
this.enableCloseToTrayText = this.i18nService.t(closeToTrayKey);
this.enableCloseToTrayDescText = this.i18nService.t(closeToTrayKey + "Desc");
const startToTrayKey = isMac ? "startToMenuBar" : "startToTray";
const startToTrayKey = this.isMac ? "startToMenuBar" : "startToTray";
this.startToTrayText = this.i18nService.t(startToTrayKey);
this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc");
this.showOpenAtLoginOption = !ipc.platform.isWindowsStore;
// DuckDuckGo browser is only for macos initially
this.showDuckDuckGoIntegrationOption = isMac;
this.showDuckDuckGoIntegrationOption = this.isMac;
const localeOptions: any[] = [];
this.i18nService.supportedTranslationLocales.forEach((locale) => {
@@ -239,7 +242,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
async ngOnInit() {
this.vaultTimeoutOptions = await this.generateVaultTimeoutOptions();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
this.isLinux = (await this.platformUtilsService.getDevice()) === DeviceType.LinuxDesktop;
// Autotype is for Windows initially
const isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
@@ -250,8 +252,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
this.isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
this.currentUserEmail = activeAccount.email;
this.currentUserId = activeAccount.id;
@@ -911,28 +911,4 @@ export class SettingsComponent implements OnInit, OnDestroy {
throw new Error("Unsupported platform");
}
}
get autoPromptBiometricsText() {
switch (this.platformUtilsService.getDevice()) {
case DeviceType.MacOsDesktop:
return "autoPromptTouchId";
case DeviceType.WindowsDesktop:
return "autoPromptWindowsHello";
case DeviceType.LinuxDesktop:
return "autoPromptPolkit";
default:
throw new Error("Unsupported platform");
}
}
get additionalBiometricSettingsText() {
switch (this.platformUtilsService.getDevice()) {
case DeviceType.MacOsDesktop:
return "additionalTouchIdSettings";
case DeviceType.WindowsDesktop:
return "additionalWindowsHelloSettings";
default:
throw new Error("Unsupported platform");
}
}
}