mirror of
https://github.com/bitwarden/browser
synced 2026-01-01 16:13:27 +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:
@@ -81,6 +81,31 @@
|
||||
"additionalTouchIdSettings" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
*ngIf="
|
||||
supportsBiometric &&
|
||||
form.value.biometric &&
|
||||
isWindows &&
|
||||
(userHasMasterPassword || (form.value.pin && userHasPinSet)) &&
|
||||
isWindowsV2BiometricsEnabled
|
||||
"
|
||||
>
|
||||
<div class="checkbox form-group-child">
|
||||
<label for="requireMasterPasswordOnAppRestart">
|
||||
<input
|
||||
id="requireMasterPasswordOnAppRestart"
|
||||
type="checkbox"
|
||||
formControlName="requireMasterPasswordOnAppRestart"
|
||||
/>
|
||||
@if (pinEnabled$ | async) {
|
||||
{{ "requireMasterPasswordOrPinOnAppRestart" | i18n }}
|
||||
} @else {
|
||||
{{ "requireMasterPasswordOnAppRestart" | i18n }}
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
*ngIf="supportsBiometric && this.form.value.biometric && this.isMac"
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -73,6 +74,9 @@ describe("SettingsComponent", () => {
|
||||
const desktopAutotypeService = mock<DesktopAutotypeService>();
|
||||
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
const configService = mock<ConfigService>();
|
||||
const userVerificationService = mock<UserVerificationService>();
|
||||
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
@@ -92,6 +96,7 @@ describe("SettingsComponent", () => {
|
||||
};
|
||||
|
||||
i18nService.supportedTranslationLocales = [];
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
@@ -124,7 +129,7 @@ describe("SettingsComponent", () => {
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: StateService, useValue: mock<StateService>() },
|
||||
{ provide: ThemeStateService, useValue: themeStateService },
|
||||
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||
{ provide: UserVerificationService, useValue: userVerificationService },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||
{ provide: ValidationService, useValue: validationService },
|
||||
{ provide: MessagingService, useValue: messagingService },
|
||||
@@ -153,6 +158,7 @@ describe("SettingsComponent", () => {
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
desktopBiometricsService.hasPersistentKey.mockResolvedValue(false);
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
|
||||
of(VaultTimeoutStringType.OnLocked),
|
||||
);
|
||||
@@ -296,43 +302,81 @@ describe("SettingsComponent", () => {
|
||||
describe("windows desktop", () => {
|
||||
beforeEach(() => {
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
desktopBiometricsService.isWindowsV2BiometricsEnabled.mockResolvedValue(true);
|
||||
|
||||
// Recreate component to apply the correct device
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
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);
|
||||
test.each([true, false])(
|
||||
`correct message display for require MP/PIN on app restart when pin is set, windows desktop, and policy is %s`,
|
||||
async (policyEnabled) => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = policyEnabled;
|
||||
policyService.policiesByType$.mockReturnValue(of([policy]));
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='requirePasswordOnStart']"),
|
||||
);
|
||||
expect(requirePasswordOnStartLabelElement).toBeNull();
|
||||
const textNodes = checkRequireMasterPasswordOnAppRestartElement(fixture);
|
||||
|
||||
if (policyEnabled) {
|
||||
expect(textNodes).toContain("requireMasterPasswordOnAppRestart");
|
||||
} else {
|
||||
expect(textNodes).toContain("requireMasterPasswordOrPinOnAppRestart");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe("users without a master password", () => {
|
||||
beforeEach(() => {
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("displays require MP/PIN on app restart checkbox when pin is set", async () => {
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
checkRequireMasterPasswordOnAppRestartElement(fixture);
|
||||
});
|
||||
|
||||
it("does not display require MP/PIN on app restart checkbox when pin is not set", async () => {
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requireMasterPasswordOnAppRestartLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='requireMasterPasswordOnAppRestart']"),
|
||||
);
|
||||
expect(requireMasterPasswordOnAppRestartLabelElement).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
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']"),
|
||||
function checkRequireMasterPasswordOnAppRestartElement(
|
||||
fixture: ComponentFixture<SettingsComponent>,
|
||||
) {
|
||||
const requireMasterPasswordOnAppRestartLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='requireMasterPasswordOnAppRestart']"),
|
||||
);
|
||||
expect(requirePasswordOnStartLabelElement).toBeNull();
|
||||
});
|
||||
expect(requireMasterPasswordOnAppRestartLabelElement).not.toBeNull();
|
||||
expect(requireMasterPasswordOnAppRestartLabelElement.children).toHaveLength(1);
|
||||
expect(requireMasterPasswordOnAppRestartLabelElement.children[0].name).toBe("input");
|
||||
expect(requireMasterPasswordOnAppRestartLabelElement.children[0].attributes).toMatchObject({
|
||||
id: "requireMasterPasswordOnAppRestart",
|
||||
type: "checkbox",
|
||||
});
|
||||
const textNodes = requireMasterPasswordOnAppRestartLabelElement.childNodes
|
||||
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.nativeNode.wholeText?.trim());
|
||||
return textNodes;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -362,7 +406,7 @@ describe("SettingsComponent", () => {
|
||||
await component.updatePinHandler(true);
|
||||
|
||||
expect(component.form.controls.pin.value).toBe(false);
|
||||
expect(vaultTimeoutSettingsService.clear).not.toHaveBeenCalled();
|
||||
expect(pinServiceAbstraction.unsetPin).not.toHaveBeenCalled();
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
});
|
||||
|
||||
@@ -378,7 +422,7 @@ describe("SettingsComponent", () => {
|
||||
await component.updatePinHandler(true);
|
||||
|
||||
expect(component.form.controls.pin.value).toBe(dialogResult);
|
||||
expect(vaultTimeoutSettingsService.clear).not.toHaveBeenCalled();
|
||||
expect(pinServiceAbstraction.unsetPin).not.toHaveBeenCalled();
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
},
|
||||
);
|
||||
@@ -390,9 +434,147 @@ describe("SettingsComponent", () => {
|
||||
await component.updatePinHandler(false);
|
||||
|
||||
expect(component.form.controls.pin.value).toBe(false);
|
||||
expect(vaultTimeoutSettingsService.clear).not.toHaveBeenCalled();
|
||||
expect(pinServiceAbstraction.unsetPin).toHaveBeenCalled();
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
});
|
||||
|
||||
describe("when windows biometric v2 feature flag is enabled", () => {
|
||||
beforeEach(() => {
|
||||
keyService.userKey$ = jest.fn().mockReturnValue(of(mockUserKey));
|
||||
});
|
||||
|
||||
test.each([false, true])(
|
||||
"enrolls persistent biometric if needed, enrolled is %s",
|
||||
async (enrolled) => {
|
||||
desktopBiometricsService.hasPersistentKey.mockResolvedValue(enrolled);
|
||||
|
||||
await component.ngOnInit();
|
||||
component.isWindowsV2BiometricsEnabled = true;
|
||||
component.isWindows = true;
|
||||
component.form.value.requireMasterPasswordOnAppRestart = true;
|
||||
component.userHasMasterPassword = false;
|
||||
component.supportsBiometric = true;
|
||||
component.form.value.biometric = true;
|
||||
|
||||
await component.updatePinHandler(false);
|
||||
|
||||
expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(false);
|
||||
expect(component.form.controls.pin.value).toBe(false);
|
||||
expect(pinServiceAbstraction.unsetPin).toHaveBeenCalled();
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
|
||||
if (enrolled) {
|
||||
expect(desktopBiometricsService.enrollPersistent).not.toHaveBeenCalled();
|
||||
} else {
|
||||
expect(desktopBiometricsService.enrollPersistent).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockUserKey,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
{
|
||||
userHasMasterPassword: true,
|
||||
supportsBiometric: false,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: false,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: true,
|
||||
supportsBiometric: false,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: true,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: true,
|
||||
supportsBiometric: false,
|
||||
biometric: true,
|
||||
requireMasterPasswordOnAppRestart: false,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: true,
|
||||
supportsBiometric: false,
|
||||
biometric: true,
|
||||
requireMasterPasswordOnAppRestart: true,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: true,
|
||||
supportsBiometric: true,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: false,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: true,
|
||||
supportsBiometric: true,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: true,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: false,
|
||||
supportsBiometric: false,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: false,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: false,
|
||||
supportsBiometric: false,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: true,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: false,
|
||||
supportsBiometric: false,
|
||||
biometric: true,
|
||||
requireMasterPasswordOnAppRestart: false,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: false,
|
||||
supportsBiometric: false,
|
||||
biometric: true,
|
||||
requireMasterPasswordOnAppRestart: true,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: false,
|
||||
supportsBiometric: true,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: false,
|
||||
},
|
||||
{
|
||||
userHasMasterPassword: false,
|
||||
supportsBiometric: true,
|
||||
biometric: false,
|
||||
requireMasterPasswordOnAppRestart: true,
|
||||
},
|
||||
])(
|
||||
"does not enroll persistent biometric when conditions are not met: userHasMasterPassword=$userHasMasterPassword, supportsBiometric=$supportsBiometric, biometric=$biometric, requireMasterPasswordOnAppRestart=$requireMasterPasswordOnAppRestart",
|
||||
async ({
|
||||
userHasMasterPassword,
|
||||
supportsBiometric,
|
||||
biometric,
|
||||
requireMasterPasswordOnAppRestart,
|
||||
}) => {
|
||||
desktopBiometricsService.hasPersistentKey.mockResolvedValue(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
component.isWindowsV2BiometricsEnabled = true;
|
||||
component.isWindows = true;
|
||||
component.form.value.requireMasterPasswordOnAppRestart =
|
||||
requireMasterPasswordOnAppRestart;
|
||||
component.userHasMasterPassword = userHasMasterPassword;
|
||||
component.supportsBiometric = supportsBiometric;
|
||||
component.form.value.biometric = biometric;
|
||||
|
||||
await component.updatePinHandler(false);
|
||||
|
||||
expect(component.form.controls.pin.value).toBe(false);
|
||||
expect(pinServiceAbstraction.unsetPin).toHaveBeenCalled();
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
expect(desktopBiometricsService.enrollPersistent).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -474,22 +656,92 @@ describe("SettingsComponent", () => {
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
});
|
||||
|
||||
it("handles windows case", async () => {
|
||||
desktopBiometricsService.getBiometricsStatus.mockResolvedValue(BiometricsStatus.Available);
|
||||
desktopBiometricsService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.Available,
|
||||
);
|
||||
describe("windows test cases", () => {
|
||||
beforeEach(() => {
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
component.isWindows = true;
|
||||
component.isLinux = false;
|
||||
|
||||
component.isWindows = true;
|
||||
component.isLinux = false;
|
||||
await component.updateBiometricHandler(true);
|
||||
desktopBiometricsService.getBiometricsStatus.mockResolvedValue(
|
||||
BiometricsStatus.Available,
|
||||
);
|
||||
desktopBiometricsService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.Available,
|
||||
);
|
||||
});
|
||||
|
||||
expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith(true);
|
||||
expect(component.form.controls.autoPromptBiometrics.value).toBe(false);
|
||||
expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith(false);
|
||||
expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId);
|
||||
expect(component.form.controls.biometric.value).toBe(true);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
it("handles windows case", async () => {
|
||||
await component.updateBiometricHandler(true);
|
||||
|
||||
expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith(true);
|
||||
expect(component.form.controls.autoPromptBiometrics.value).toBe(false);
|
||||
expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith(false);
|
||||
expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId);
|
||||
expect(component.form.controls.biometric.value).toBe(true);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
});
|
||||
|
||||
describe("when windows v2 biometrics is enabled", () => {
|
||||
beforeEach(() => {
|
||||
component.isWindowsV2BiometricsEnabled = true;
|
||||
|
||||
keyService.userKey$ = jest.fn().mockReturnValue(of(mockUserKey));
|
||||
});
|
||||
|
||||
it("when the user doesn't have a master password or a PIN set, allows biometric unlock on app restart", async () => {
|
||||
component.userHasMasterPassword = false;
|
||||
component.userHasPinSet = false;
|
||||
desktopBiometricsService.hasPersistentKey.mockResolvedValue(false);
|
||||
|
||||
await component.updateBiometricHandler(true);
|
||||
|
||||
expect(keyService.userKey$).toHaveBeenCalledWith(mockUserId);
|
||||
expect(desktopBiometricsService.enrollPersistent).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockUserKey,
|
||||
);
|
||||
expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(false);
|
||||
|
||||
expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith(true);
|
||||
expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith(true);
|
||||
expect(component.form.controls.autoPromptBiometrics.value).toBe(false);
|
||||
expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith(false);
|
||||
expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId);
|
||||
expect(component.form.controls.biometric.value).toBe(true);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
});
|
||||
|
||||
test.each([
|
||||
[true, true],
|
||||
[true, false],
|
||||
[false, true],
|
||||
])(
|
||||
"when the userHasMasterPassword is %s and userHasPinSet is %s, require master password/PIN on app restart is the default setting",
|
||||
async (userHasMasterPassword, userHasPinSet) => {
|
||||
component.userHasMasterPassword = userHasMasterPassword;
|
||||
component.userHasPinSet = userHasPinSet;
|
||||
|
||||
await component.updateBiometricHandler(true);
|
||||
|
||||
expect(desktopBiometricsService.enrollPersistent).not.toHaveBeenCalled();
|
||||
expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(true);
|
||||
expect(desktopBiometricsService.deleteBiometricUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(
|
||||
desktopBiometricsService.setBiometricProtectedUnlockKeyForUser,
|
||||
).toHaveBeenCalledWith(mockUserId, mockUserKey);
|
||||
|
||||
expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith(true);
|
||||
expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith(true);
|
||||
expect(component.form.controls.autoPromptBiometrics.value).toBe(false);
|
||||
expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith(false);
|
||||
expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId);
|
||||
expect(component.form.controls.biometric.value).toBe(true);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("handles linux case", async () => {
|
||||
@@ -553,6 +805,57 @@ describe("SettingsComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateRequireMasterPasswordOnAppRestartHandler", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
keyService.userKey$ = jest.fn().mockReturnValue(of(mockUserKey));
|
||||
});
|
||||
|
||||
test.each([true, false])(`handles thrown errors when updated to %s`, async (update) => {
|
||||
const error = new Error("Test error");
|
||||
jest.spyOn(component, "updateRequireMasterPasswordOnAppRestart").mockRejectedValue(error);
|
||||
|
||||
await component.ngOnInit();
|
||||
await component.updateRequireMasterPasswordOnAppRestartHandler(update, mockUserId);
|
||||
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
expect(validationService.showError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
describe("when updating to true", () => {
|
||||
it("calls the biometrics service to clear and reset biometric key", async () => {
|
||||
await component.ngOnInit();
|
||||
await component.updateRequireMasterPasswordOnAppRestartHandler(true, mockUserId);
|
||||
|
||||
expect(keyService.userKey$).toHaveBeenCalledWith(mockUserId);
|
||||
expect(desktopBiometricsService.deleteBiometricUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(desktopBiometricsService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockUserKey,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when updating to false", () => {
|
||||
it("doesn't enroll persistent biometric if already enrolled", async () => {
|
||||
biometricStateService.hasPersistentKey.mockResolvedValue(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
await component.updateRequireMasterPasswordOnAppRestartHandler(false, mockUserId);
|
||||
|
||||
expect(keyService.userKey$).toHaveBeenCalledWith(mockUserId);
|
||||
expect(desktopBiometricsService.enrollPersistent).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockUserKey,
|
||||
);
|
||||
expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveVaultTimeout", () => {
|
||||
const DEFAULT_VAULT_TIMEOUT: VaultTimeout = 123;
|
||||
const DEFAULT_VAULT_TIMEOUT_ACTION = VaultTimeoutAction.Lock;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,6 +28,7 @@ import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype
|
||||
import { SshAgentService } from "../../autofill/services/ssh-agent.service";
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { VersionService } from "../../platform/services/version.service";
|
||||
import { BiometricMessageHandlerService } from "../../services/biometric-message-handler.service";
|
||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||
|
||||
@Injectable()
|
||||
@@ -53,6 +54,7 @@ export class InitService {
|
||||
private autofillService: DesktopAutofillService,
|
||||
private autotypeService: DesktopAutotypeService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private biometricMessageHandlerService: BiometricMessageHandlerService,
|
||||
private configService: ConfigService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
private readonly migrationRunner: MigrationRunner,
|
||||
@@ -95,6 +97,7 @@ export class InitService {
|
||||
const containerService = new ContainerService(this.keyService, this.encryptService);
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
||||
await this.biometricMessageHandlerService.init();
|
||||
await this.autofillService.init();
|
||||
await this.autotypeService.init();
|
||||
};
|
||||
|
||||
@@ -13,4 +13,9 @@ 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>;
|
||||
/* Enables the v2 biometrics re-write. This will stay enabled until the application is restarted. */
|
||||
abstract enableWindowsV2Biometrics(): Promise<void>;
|
||||
abstract isWindowsV2BiometricsEnabled(): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,17 @@ 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),
|
||||
);
|
||||
case BiometricAction.EnableWindowsV2:
|
||||
return await this.biometricService.enableWindowsV2Biometrics();
|
||||
case BiometricAction.IsWindowsV2Enabled:
|
||||
return await this.biometricService.isWindowsV2BiometricsEnabled();
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import {
|
||||
BiometricsService,
|
||||
BiometricsStatus,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
|
||||
import { MainBiometricsService } from "./main-biometrics.service";
|
||||
import { WindowsBiometricsSystem } from "./native-v2";
|
||||
import OsBiometricsServiceLinux from "./os-biometrics-linux.service";
|
||||
import OsBiometricsServiceMac from "./os-biometrics-mac.service";
|
||||
import OsBiometricsServiceWindows from "./os-biometrics-windows.service";
|
||||
@@ -28,6 +30,13 @@ jest.mock("@bitwarden/desktop-napi", () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("./native-v2", () => ({
|
||||
WindowsBiometricsSystem: jest.fn(),
|
||||
biometrics_v2: {
|
||||
initBiometricSystem: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const unlockKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
|
||||
describe("MainBiometricsService", function () {
|
||||
@@ -38,24 +47,6 @@ describe("MainBiometricsService", function () {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
it("Should call the platformspecific methods", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const mockService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = mockService;
|
||||
|
||||
await sut.authenticateBiometric();
|
||||
expect(mockService.authenticateBiometric).toBeCalled();
|
||||
});
|
||||
|
||||
describe("Should create a platform specific service", function () {
|
||||
it("Should create a biometrics service specific for Windows", () => {
|
||||
const sut = new MainBiometricsService(
|
||||
@@ -207,46 +198,6 @@ describe("MainBiometricsService", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupBiometrics", () => {
|
||||
it("should call the platform specific setup method", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
|
||||
await sut.setupBiometrics();
|
||||
|
||||
expect(osBiometricsService.runSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticateWithBiometrics", () => {
|
||||
it("should call the platform specific authenticate method", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
|
||||
await sut.authenticateWithBiometrics();
|
||||
|
||||
expect(osBiometricsService.authenticateBiometric).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlockWithBiometricsForUser", () => {
|
||||
let sut: MainBiometricsService;
|
||||
let osBiometricsService: MockProxy<OsBiometricService>;
|
||||
@@ -288,55 +239,6 @@ describe("MainBiometricsService", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setBiometricProtectedUnlockKeyForUser", () => {
|
||||
let sut: MainBiometricsService;
|
||||
let osBiometricsService: MockProxy<OsBiometricService>;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
});
|
||||
|
||||
it("should call the platform specific setBiometricKey method", async () => {
|
||||
const userId = "test" as UserId;
|
||||
|
||||
await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey);
|
||||
|
||||
expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(userId, unlockKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteBiometricUnlockKeyForUser", () => {
|
||||
it("should call the platform specific deleteBiometricKey method", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
|
||||
const userId = "test" as UserId;
|
||||
|
||||
await sut.deleteBiometricUnlockKeyForUser(userId);
|
||||
|
||||
expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setShouldAutopromptNow", () => {
|
||||
let sut: MainBiometricsService;
|
||||
|
||||
@@ -386,4 +288,138 @@ describe("MainBiometricsService", function () {
|
||||
expect(shouldAutoPrompt).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enableWindowsV2Biometrics", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("enables Windows V2 biometrics when platform is win32 and not already enabled", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
"win32",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
await sut.enableWindowsV2Biometrics();
|
||||
|
||||
expect(logService.info).toHaveBeenCalledWith(
|
||||
"[BiometricsMain] Loading native biometrics module v2 for windows",
|
||||
);
|
||||
expect(await sut.isWindowsV2BiometricsEnabled()).toBe(true);
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
expect(internalService).not.toBeNull();
|
||||
expect(internalService).toBeInstanceOf(WindowsBiometricsSystem);
|
||||
});
|
||||
|
||||
it("should not enable Windows V2 biometrics when platform is not win32", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
"darwin",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
await sut.enableWindowsV2Biometrics();
|
||||
|
||||
expect(logService.info).not.toHaveBeenCalled();
|
||||
expect(await sut.isWindowsV2BiometricsEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not enable Windows V2 biometrics when already enabled", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
"win32",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
// Enable it first
|
||||
await sut.enableWindowsV2Biometrics();
|
||||
|
||||
// Enable it again
|
||||
await sut.enableWindowsV2Biometrics();
|
||||
|
||||
expect(logService.info).toHaveBeenCalledWith(
|
||||
"[BiometricsMain] Loading native biometrics module v2 for windows",
|
||||
);
|
||||
expect(logService.info).toHaveBeenCalledTimes(1);
|
||||
expect(await sut.isWindowsV2BiometricsEnabled()).toBe(true);
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
expect(internalService).not.toBeNull();
|
||||
expect(internalService).toBeInstanceOf(WindowsBiometricsSystem);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pass through methods that call platform specific osBiometricsService methods", () => {
|
||||
const userId = newGuid() as UserId;
|
||||
let sut: MainBiometricsService;
|
||||
let osBiometricsService: MockProxy<OsBiometricService>;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
});
|
||||
|
||||
it("calls the platform specific setBiometricKey method", async () => {
|
||||
await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey);
|
||||
|
||||
expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(userId, unlockKey);
|
||||
});
|
||||
|
||||
it("calls the platform specific enrollPersistent method", async () => {
|
||||
await sut.enrollPersistent(userId, unlockKey);
|
||||
|
||||
expect(osBiometricsService.enrollPersistent).toHaveBeenCalledWith(userId, unlockKey);
|
||||
});
|
||||
|
||||
it("calls the platform specific hasPersistentKey method", async () => {
|
||||
await sut.hasPersistentKey(userId);
|
||||
|
||||
expect(osBiometricsService.hasPersistentKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("calls the platform specific deleteBiometricUnlockKeyForUser method", async () => {
|
||||
await sut.deleteBiometricUnlockKeyForUser(userId);
|
||||
|
||||
expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("calls the platform specific authenticateWithBiometrics method", async () => {
|
||||
await sut.authenticateWithBiometrics();
|
||||
|
||||
expect(osBiometricsService.authenticateBiometric).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls the platform specific authenticateBiometric method", async () => {
|
||||
await sut.authenticateBiometric();
|
||||
|
||||
expect(osBiometricsService.authenticateBiometric).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls the platform specific setupBiometrics method", async () => {
|
||||
await sut.setupBiometrics();
|
||||
|
||||
expect(osBiometricsService.runSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,17 +10,19 @@ import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-manageme
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
|
||||
import { DesktopBiometricsService } from "./desktop.biometrics.service";
|
||||
import { WindowsBiometricsSystem } from "./native-v2";
|
||||
import { OsBiometricService } from "./os-biometrics.service";
|
||||
|
||||
export class MainBiometricsService extends DesktopBiometricsService {
|
||||
private osBiometricsService: OsBiometricService;
|
||||
private shouldAutoPrompt = true;
|
||||
private windowsV2BiometricsEnabled = false;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
platform: NodeJS.Platform,
|
||||
private platform: NodeJS.Platform,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -144,4 +146,28 @@ 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);
|
||||
}
|
||||
|
||||
async enableWindowsV2Biometrics(): Promise<void> {
|
||||
if (this.platform === "win32" && !this.windowsV2BiometricsEnabled) {
|
||||
this.logService.info("[BiometricsMain] Loading native biometrics module v2 for windows");
|
||||
this.osBiometricsService = new WindowsBiometricsSystem(
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
this.logService,
|
||||
);
|
||||
this.windowsV2BiometricsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async isWindowsV2BiometricsEnabled(): Promise<boolean> {
|
||||
return this.windowsV2BiometricsEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as WindowsBiometricsSystem } from "./os-biometrics-windows.service";
|
||||
@@ -0,0 +1,126 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics_v2 } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
|
||||
import OsBiometricsServiceWindows from "./os-biometrics-windows.service";
|
||||
|
||||
jest.mock("@bitwarden/desktop-napi", () => ({
|
||||
biometrics_v2: {
|
||||
initBiometricSystem: jest.fn(() => "mockSystem"),
|
||||
provideKey: jest.fn(),
|
||||
enrollPersistent: jest.fn(),
|
||||
unenroll: jest.fn(),
|
||||
unlock: jest.fn(),
|
||||
authenticate: jest.fn(),
|
||||
authenticateAvailable: jest.fn(),
|
||||
unlockAvailable: jest.fn(),
|
||||
hasPersistent: jest.fn(),
|
||||
},
|
||||
passwords: {
|
||||
isAvailable: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockKey = new Uint8Array(64);
|
||||
|
||||
jest.mock("../../../utils", () => ({
|
||||
isFlatpak: jest.fn(() => false),
|
||||
isLinux: jest.fn(() => true),
|
||||
isSnapStore: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
describe("OsBiometricsServiceWindows", () => {
|
||||
const userId = "user-id" as UserId;
|
||||
|
||||
let service: OsBiometricsServiceWindows;
|
||||
let i18nService: I18nService;
|
||||
let windowMain: WindowMain;
|
||||
let logService: LogService;
|
||||
|
||||
beforeEach(() => {
|
||||
i18nService = mock<I18nService>();
|
||||
windowMain = mock<WindowMain>();
|
||||
logService = mock<LogService>();
|
||||
|
||||
windowMain.win.getNativeWindowHandle = jest.fn().mockReturnValue(Buffer.from([1, 2, 3, 4]));
|
||||
service = new OsBiometricsServiceWindows(i18nService, windowMain, logService);
|
||||
});
|
||||
|
||||
it("should enroll persistent biometric key", async () => {
|
||||
await service.enrollPersistent("user-id" as UserId, new SymmetricCryptoKey(mockKey));
|
||||
expect(biometrics_v2.enrollPersistent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set biometric key", async () => {
|
||||
await service.setBiometricKey(userId, new SymmetricCryptoKey(mockKey));
|
||||
expect(biometrics_v2.provideKey).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete biometric key", async () => {
|
||||
await service.deleteBiometricKey(userId);
|
||||
expect(biometrics_v2.unenroll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should get biometric key", async () => {
|
||||
(biometrics_v2.unlock as jest.Mock).mockResolvedValue(mockKey);
|
||||
const result = await service.getBiometricKey(userId);
|
||||
expect(result).toBeInstanceOf(SymmetricCryptoKey);
|
||||
});
|
||||
|
||||
it("should return null if no biometric key", async () => {
|
||||
const error = new Error("No key found");
|
||||
(biometrics_v2.unlock as jest.Mock).mockRejectedValue(error);
|
||||
const result = await service.getBiometricKey(userId);
|
||||
expect(result).toBeNull();
|
||||
expect(logService.warning).toHaveBeenCalledWith(
|
||||
`[OsBiometricsServiceWindows] Fetching the biometric key failed: ${error} returning null`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should authenticate biometric", async () => {
|
||||
(biometrics_v2.authenticate as jest.Mock).mockResolvedValue(true);
|
||||
const result = await service.authenticateBiometric();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should check if biometrics is supported", async () => {
|
||||
(biometrics_v2.authenticateAvailable as jest.Mock).mockResolvedValue(true);
|
||||
const result = await service.supportsBiometrics();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return needs setup false", async () => {
|
||||
const result = await service.needsSetup();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return auto setup false", async () => {
|
||||
const result = await service.canAutoSetup();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should get biometrics first unlock status for user", async () => {
|
||||
(biometrics_v2.unlockAvailable as jest.Mock).mockResolvedValue(true);
|
||||
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
expect(result).toBe(BiometricsStatus.Available);
|
||||
});
|
||||
|
||||
it("should return false for hasPersistentKey false", async () => {
|
||||
(biometrics_v2.hasPersistent as jest.Mock).mockResolvedValue(false);
|
||||
const result = await service.hasPersistentKey(userId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for hasPersistentKey true", async () => {
|
||||
(biometrics_v2.hasPersistent as jest.Mock).mockResolvedValue(true);
|
||||
const result = await service.hasPersistentKey(userId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics_v2 } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { WindowMain } from "../../../main/window.main";
|
||||
import { OsBiometricService } from "../os-biometrics.service";
|
||||
|
||||
export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
private biometricsSystem: biometrics_v2.BiometricLockSystem;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
) {
|
||||
this.biometricsSystem = biometrics_v2.initBiometricSystem();
|
||||
}
|
||||
|
||||
async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
await biometrics_v2.enrollPersistent(
|
||||
this.biometricsSystem,
|
||||
userId,
|
||||
Buffer.from(key.toEncoded().buffer),
|
||||
);
|
||||
}
|
||||
|
||||
async hasPersistentKey(userId: UserId): Promise<boolean> {
|
||||
return await biometrics_v2.hasPersistent(this.biometricsSystem, userId);
|
||||
}
|
||||
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return await biometrics_v2.authenticateAvailable(this.biometricsSystem);
|
||||
}
|
||||
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
try {
|
||||
const key = await biometrics_v2.unlock(
|
||||
this.biometricsSystem,
|
||||
userId,
|
||||
this.windowMain.win.getNativeWindowHandle(),
|
||||
);
|
||||
return key ? new SymmetricCryptoKey(Uint8Array.from(key)) : null;
|
||||
} catch (error) {
|
||||
this.logService.warning(
|
||||
`[OsBiometricsServiceWindows] Fetching the biometric key failed: ${error} returning null`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
await biometrics_v2.provideKey(
|
||||
this.biometricsSystem,
|
||||
userId,
|
||||
Buffer.from(key.toEncoded().buffer),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteBiometricKey(userId: UserId): Promise<void> {
|
||||
await biometrics_v2.unenroll(this.biometricsSystem, userId);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
const hwnd = this.windowMain.win.getNativeWindowHandle();
|
||||
return await biometrics_v2.authenticate(
|
||||
this.biometricsSystem,
|
||||
hwnd,
|
||||
this.i18nService.t("windowsHelloConsentMessage"),
|
||||
);
|
||||
}
|
||||
|
||||
async needsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async runSetup(): Promise<void> {}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
return (await biometrics_v2.hasPersistent(this.biometricsSystem, userId)) ||
|
||||
(await biometrics_v2.unlockAvailable(this.biometricsSystem, userId))
|
||||
? BiometricsStatus.Available
|
||||
: BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,12 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void> {}
|
||||
|
||||
async hasPersistentKey(userId: UserId): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
private _iv: string | null = null;
|
||||
// Use getKeyMaterial helper instead of direct access
|
||||
private _osKeyHalf: string | null = null;
|
||||
|
||||
@@ -20,6 +20,14 @@ export default class OsBiometricsServiceMac implements OsBiometricService {
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
return await passwords.setPassword(SERVICE, getLookupKeyForUser(userId), key.toBase64());
|
||||
}
|
||||
|
||||
async hasPersistentKey(userId: UserId): Promise<boolean> {
|
||||
return (await passwords.getPassword(SERVICE, getLookupKeyForUser(userId))) != null;
|
||||
}
|
||||
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return systemPreferences.canPromptTouchID();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
|
||||
async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void> {}
|
||||
|
||||
async hasPersistentKey(userId: UserId): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return await biometrics.available();
|
||||
}
|
||||
|
||||
@@ -25,4 +25,6 @@ export interface OsBiometricService {
|
||||
setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void>;
|
||||
deleteBiometricKey(userId: UserId): Promise<void>;
|
||||
getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus>;
|
||||
enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void>;
|
||||
hasPersistentKey(userId: UserId): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -68,4 +68,20 @@ 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);
|
||||
}
|
||||
|
||||
async enableWindowsV2Biometrics(): Promise<void> {
|
||||
return await ipc.keyManagement.biometric.enableWindowsV2Biometrics();
|
||||
}
|
||||
|
||||
async isWindowsV2BiometricsEnabled(): Promise<boolean> {
|
||||
return await ipc.keyManagement.biometric.isWindowsV2BiometricsEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,25 @@ 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),
|
||||
enableWindowsV2Biometrics: (): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.EnableWindowsV2,
|
||||
} satisfies BiometricMessage),
|
||||
isWindowsV2BiometricsEnabled: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.IsWindowsV2Enabled,
|
||||
} satisfies BiometricMessage),
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1852,6 +1852,12 @@
|
||||
"lockWithMasterPassOnRestart1": {
|
||||
"message": "Lock with master password on restart"
|
||||
},
|
||||
"requireMasterPasswordOrPinOnAppRestart": {
|
||||
"message": "Require master password or PIN on app restart"
|
||||
},
|
||||
"requireMasterPasswordOnAppRestart": {
|
||||
"message": "Require master password on app restart"
|
||||
},
|
||||
"deleteAccount": {
|
||||
"message": "Delete account"
|
||||
},
|
||||
|
||||
@@ -82,7 +82,12 @@ export class WindowMain {
|
||||
|
||||
ipcMain.on("window-hide", () => {
|
||||
if (this.win != null) {
|
||||
this.win.hide();
|
||||
if (isWindows()) {
|
||||
// On windows, to return focus we need minimize
|
||||
this.win.minimize();
|
||||
} else {
|
||||
this.win.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -13,13 +13,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, I18nMockService } from "@bitwarden/components";
|
||||
import {
|
||||
KeyService,
|
||||
BiometricsService,
|
||||
BiometricStateService,
|
||||
BiometricsCommands,
|
||||
} from "@bitwarden/key-management";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService, BiometricsService, BiometricsCommands } from "@bitwarden/key-management";
|
||||
import { ConfigService } from "@bitwarden/services/config.service";
|
||||
|
||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||
|
||||
@@ -47,15 +43,14 @@ describe("BiometricMessageHandlerService", () => {
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let desktopSettingsService: DesktopSettingsService;
|
||||
let biometricStateService: BiometricStateService;
|
||||
let biometricsService: MockProxy<BiometricsService>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let accountService: AccountService;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let ngZone: MockProxy<NgZone>;
|
||||
let i18nService: MockProxy<I18nMockService>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
@@ -64,14 +59,13 @@ describe("BiometricMessageHandlerService", () => {
|
||||
logService = mock<LogService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
desktopSettingsService = mock<DesktopSettingsService>();
|
||||
biometricStateService = mock<BiometricStateService>();
|
||||
configService = mock<ConfigService>();
|
||||
biometricsService = mock<BiometricsService>();
|
||||
dialogService = mock<DialogService>();
|
||||
|
||||
accountService = new FakeAccountService(accounts);
|
||||
authService = mock<AuthService>();
|
||||
ngZone = mock<NgZone>();
|
||||
i18nService = mock<I18nMockService>();
|
||||
|
||||
desktopSettingsService.browserIntegrationEnabled$ = of(false);
|
||||
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(false);
|
||||
@@ -94,7 +88,7 @@ describe("BiometricMessageHandlerService", () => {
|
||||
cryptoFunctionService.rsaEncrypt.mockResolvedValue(
|
||||
Utils.fromUtf8ToArray("encrypted") as CsprngArray,
|
||||
);
|
||||
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
service = new BiometricMessageHandlerService(
|
||||
cryptoFunctionService,
|
||||
keyService,
|
||||
@@ -102,13 +96,12 @@ describe("BiometricMessageHandlerService", () => {
|
||||
logService,
|
||||
messagingService,
|
||||
desktopSettingsService,
|
||||
biometricStateService,
|
||||
biometricsService,
|
||||
dialogService,
|
||||
accountService,
|
||||
authService,
|
||||
ngZone,
|
||||
i18nService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,13 +153,12 @@ describe("BiometricMessageHandlerService", () => {
|
||||
logService,
|
||||
messagingService,
|
||||
desktopSettingsService,
|
||||
biometricStateService,
|
||||
biometricsService,
|
||||
dialogService,
|
||||
accountService,
|
||||
authService,
|
||||
ngZone,
|
||||
i18nService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -511,4 +503,19 @@ describe("BiometricMessageHandlerService", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
it("enables Windows v2 biometrics when feature flag enabled", async () => {
|
||||
configService.getFeatureFlag.mockReturnValue(true);
|
||||
|
||||
await service.init();
|
||||
expect(biometricsService.enableWindowsV2Biometrics).toHaveBeenCalled();
|
||||
});
|
||||
it("does not enable Windows v2 biometrics when feature flag disabled", async () => {
|
||||
configService.getFeatureFlag.mockReturnValue(false);
|
||||
|
||||
await service.init();
|
||||
expect(biometricsService.enableWindowsV2Biometrics).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,25 +4,21 @@ import { combineLatest, concatMap, firstValueFrom } from "rxjs";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
BiometricStateService,
|
||||
BiometricsCommands,
|
||||
BiometricsService,
|
||||
BiometricsStatus,
|
||||
KeyService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { BiometricsCommands, BiometricsStatus, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
|
||||
import { DesktopBiometricsService } from "../key-management/biometrics/desktop.biometrics.service";
|
||||
import { LegacyMessage, LegacyMessageWrapper } from "../models/native-messaging";
|
||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||
|
||||
@@ -82,13 +78,12 @@ export class BiometricMessageHandlerService {
|
||||
private logService: LogService,
|
||||
private messagingService: MessagingService,
|
||||
private desktopSettingService: DesktopSettingsService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private biometricsService: BiometricsService,
|
||||
private biometricsService: DesktopBiometricsService,
|
||||
private dialogService: DialogService,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private ngZone: NgZone,
|
||||
private i18nService: I18nService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.desktopSettingService.browserIntegrationEnabled$,
|
||||
@@ -119,6 +114,19 @@ export class BiometricMessageHandlerService {
|
||||
|
||||
private connectedApps: ConnectedApps = new ConnectedApps();
|
||||
|
||||
async init() {
|
||||
this.logService.debug(
|
||||
"[BiometricMessageHandlerService] Initializing biometric message handler",
|
||||
);
|
||||
|
||||
const windowsV2Enabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.WindowsBiometricsV2,
|
||||
);
|
||||
if (windowsV2Enabled) {
|
||||
await this.biometricsService.enableWindowsV2Biometrics();
|
||||
}
|
||||
}
|
||||
|
||||
async handleMessage(msg: LegacyMessageWrapper) {
|
||||
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ export enum BiometricAction {
|
||||
|
||||
GetShouldAutoprompt = "getShouldAutoprompt",
|
||||
SetShouldAutoprompt = "setShouldAutoprompt",
|
||||
|
||||
EnrollPersistent = "enrollPersistent",
|
||||
HasPersistentKey = "hasPersistentKey",
|
||||
|
||||
EnableWindowsV2 = "enableWindowsV2",
|
||||
IsWindowsV2Enabled = "isWindowsV2Enabled",
|
||||
}
|
||||
|
||||
export type BiometricMessage =
|
||||
@@ -22,7 +28,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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user