1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +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

@@ -7,14 +7,17 @@ import {
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { KdfConfig, KeyService } from "@bitwarden/key-management";
import {
BiometricsService,
BiometricsStatus,
KdfConfig,
KeyService,
} from "@bitwarden/key-management";
import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { HashPurpose } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
@@ -36,10 +39,9 @@ describe("UserVerificationService", () => {
const userVerificationApiService = mock<UserVerificationApiServiceAbstraction>();
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
const pinService = mock<PinServiceAbstraction>();
const logService = mock<LogService>();
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
const platformUtilsService = mock<PlatformUtilsService>();
const kdfConfigService = mock<KdfConfigService>();
const biometricsService = mock<BiometricsService>();
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
@@ -56,10 +58,8 @@ describe("UserVerificationService", () => {
userVerificationApiService,
userDecryptionOptionsService,
pinService,
logService,
vaultTimeoutSettingsService,
platformUtilsService,
kdfConfigService,
biometricsService,
);
});
@@ -113,26 +113,15 @@ describe("UserVerificationService", () => {
);
test.each([
[true, true, true, true],
[true, true, true, false],
[true, true, false, false],
[false, true, false, true],
[false, false, false, false],
[false, false, true, false],
[false, false, false, true],
[true, BiometricsStatus.Available],
[false, BiometricsStatus.DesktopDisconnected],
[false, BiometricsStatus.HardwareUnavailable],
])(
"returns %s for biometrics availability when isBiometricLockSet is %s, hasUserKeyStored is %s, and supportsSecureStorage is %s",
async (
expectedReturn: boolean,
isBiometricsLockSet: boolean,
isBiometricsUserKeyStored: boolean,
platformSupportSecureStorage: boolean,
) => {
async (expectedReturn: boolean, biometricsStatus: BiometricsStatus) => {
setMasterPasswordAvailability(false);
setPinAvailability("DISABLED");
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet);
keyService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored);
platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage);
biometricsService.getBiometricsStatus.mockResolvedValue(biometricsStatus);
const result = await sut.getAvailableVerificationOptions("client");

View File

@@ -3,17 +3,17 @@
import { firstValueFrom, map } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import {
BiometricsService,
BiometricsStatus,
KdfConfigService,
KeyService,
} from "@bitwarden/key-management";
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { HashPurpose } from "../../../platform/enums";
import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum";
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
import { AccountService } from "../../abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
@@ -47,10 +47,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private userVerificationApiService: UserVerificationApiServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private pinService: PinServiceAbstraction,
private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private kdfConfigService: KdfConfigService,
private biometricsService: BiometricsService,
) {}
async getAvailableVerificationOptions(
@@ -58,17 +56,13 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
): Promise<UserVerificationOptions> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (verificationType === "client") {
const [
userHasMasterPassword,
isPinDecryptionAvailable,
biometricsLockSet,
biometricsUserKeyStored,
] = await Promise.all([
this.hasMasterPasswordAndMasterKeyHash(userId),
this.pinService.isPinDecryptionAvailable(userId),
this.vaultTimeoutSettingsService.isBiometricLockSet(userId),
this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric, userId),
]);
const [userHasMasterPassword, isPinDecryptionAvailable, biometricsStatus] = await Promise.all(
[
this.hasMasterPasswordAndMasterKeyHash(userId),
this.pinService.isPinDecryptionAvailable(userId),
this.biometricsService.getBiometricsStatus(),
],
);
// note: we do not need to check this.platformUtilsService.supportsBiometric() because
// we can just use the logic below which works for both desktop & the browser extension.
@@ -77,9 +71,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
client: {
masterPassword: userHasMasterPassword,
pin: isPinDecryptionAvailable,
biometrics:
biometricsLockSet &&
(biometricsUserKeyStored || !this.platformUtilsService.supportsSecureStorage()),
biometrics: biometricsStatus === BiometricsStatus.Available,
},
server: {
masterPassword: false,
@@ -253,17 +245,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
}
private async verifyUserByBiometrics(): Promise<boolean> {
let userKey: UserKey;
// Biometrics crashes and doesn't return a value if the user cancels the prompt
try {
userKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
} catch (e) {
this.logService.error(`Biometrics User Verification failed: ${e.message}`);
// So, any failures should be treated as a failed verification
return false;
}
return userKey != null;
return this.biometricsService.authenticateWithBiometrics();
}
async requestOTP() {