mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-10741] Refactor biometrics interface & add dynamic status (#10973)
This commit is contained in:
@@ -279,12 +279,13 @@ import {
|
||||
ImportServiceAbstraction,
|
||||
} from "@bitwarden/importer/core";
|
||||
import {
|
||||
KeyService as KeyServiceAbstraction,
|
||||
DefaultKeyService as KeyService,
|
||||
KeyService,
|
||||
DefaultKeyService,
|
||||
BiometricStateService,
|
||||
DefaultBiometricStateService,
|
||||
KdfConfigService,
|
||||
BiometricsService,
|
||||
DefaultKdfConfigService,
|
||||
KdfConfigService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
DefaultUserAsymmetricKeysRegenerationService,
|
||||
UserAsymmetricKeysRegenerationApiService,
|
||||
@@ -416,7 +417,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
ApiServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
@@ -428,7 +429,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
ApiServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
@@ -471,7 +472,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: CipherServiceAbstraction,
|
||||
useFactory: (
|
||||
keyService: KeyServiceAbstraction,
|
||||
keyService: KeyService,
|
||||
domainSettingsService: DomainSettingsService,
|
||||
apiService: ApiServiceAbstraction,
|
||||
i18nService: I18nServiceAbstraction,
|
||||
@@ -501,7 +502,7 @@ const safeProviders: SafeProvider[] = [
|
||||
accountService,
|
||||
),
|
||||
deps: [
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
DomainSettingsService,
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
@@ -520,7 +521,7 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: InternalFolderService,
|
||||
useClass: FolderService,
|
||||
deps: [
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
@@ -565,7 +566,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: CollectionService,
|
||||
useClass: DefaultCollectionService,
|
||||
deps: [KeyServiceAbstraction, EncryptService, I18nServiceAbstraction, StateProvider],
|
||||
deps: [KeyService, EncryptService, I18nServiceAbstraction, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ENV_ADDITIONAL_REGIONS,
|
||||
@@ -610,8 +611,8 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [CryptoFunctionServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyServiceAbstraction,
|
||||
useClass: KeyService,
|
||||
provide: KeyService,
|
||||
useClass: DefaultKeyService,
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
@@ -636,7 +637,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useFactory: legacyPasswordGenerationServiceFactory,
|
||||
deps: [
|
||||
EncryptService,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
PolicyServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
@@ -645,7 +646,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: GeneratorHistoryService,
|
||||
useClass: LocalGeneratorHistoryService,
|
||||
deps: [EncryptService, KeyServiceAbstraction, StateProvider],
|
||||
deps: [EncryptService, KeyService, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
@@ -653,7 +654,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
PolicyServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
@@ -693,7 +694,7 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: InternalSendService,
|
||||
useClass: SendService,
|
||||
deps: [
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
I18nServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
SendStateProviderAbstraction,
|
||||
@@ -720,7 +721,7 @@ const safeProviders: SafeProvider[] = [
|
||||
DomainSettingsService,
|
||||
InternalFolderService,
|
||||
CipherServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
CollectionService,
|
||||
MessagingServiceAbstraction,
|
||||
InternalPolicyService,
|
||||
@@ -753,7 +754,7 @@ const safeProviders: SafeProvider[] = [
|
||||
AccountServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
TokenServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
BiometricStateService,
|
||||
@@ -780,6 +781,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateEventRunnerService,
|
||||
TaskSchedulerService,
|
||||
LogService,
|
||||
BiometricsService,
|
||||
LOCKED_CALLBACK,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
@@ -826,7 +828,7 @@ const safeProviders: SafeProvider[] = [
|
||||
ImportApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CollectionService,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
PinServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
@@ -839,7 +841,7 @@ const safeProviders: SafeProvider[] = [
|
||||
FolderServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
KdfConfigService,
|
||||
@@ -853,7 +855,7 @@ const safeProviders: SafeProvider[] = [
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
CollectionService,
|
||||
@@ -960,7 +962,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
ApiServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
LogService,
|
||||
@@ -974,17 +976,15 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: UserVerificationServiceAbstraction,
|
||||
useClass: UserVerificationService,
|
||||
deps: [
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
AccountServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
UserVerificationApiServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
LogService,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
KdfConfigService,
|
||||
BiometricsService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1007,7 +1007,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
OrganizationApiServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
OrganizationUserApiService,
|
||||
I18nServiceAbstraction,
|
||||
@@ -1117,7 +1117,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
KeyGenerationServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
AppIdServiceAbstraction,
|
||||
DevicesApiServiceAbstraction,
|
||||
@@ -1137,7 +1137,7 @@ const safeProviders: SafeProvider[] = [
|
||||
AppIdServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
ApiServiceAbstraction,
|
||||
StateProvider,
|
||||
@@ -1231,7 +1231,7 @@ const safeProviders: SafeProvider[] = [
|
||||
ApiServiceAbstraction,
|
||||
BillingApiServiceAbstraction,
|
||||
ConfigService,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
@@ -1291,7 +1291,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: UserAutoUnlockKeyService,
|
||||
useClass: UserAutoUnlockKeyService,
|
||||
deps: [KeyServiceAbstraction],
|
||||
deps: [KeyService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ErrorHandler,
|
||||
@@ -1335,7 +1335,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultSetPasswordJitService,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigService,
|
||||
@@ -1363,7 +1363,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: RegistrationFinishServiceAbstraction,
|
||||
useClass: DefaultRegistrationFinishService,
|
||||
deps: [KeyServiceAbstraction, AccountApiServiceAbstraction],
|
||||
deps: [KeyService, AccountApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ViewCacheService,
|
||||
@@ -1390,7 +1390,7 @@ const safeProviders: SafeProvider[] = [
|
||||
PlatformUtilsServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
KdfConfigService,
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1418,7 +1418,7 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: UserAsymmetricKeysRegenerationService,
|
||||
useClass: DefaultUserAsymmetricKeysRegenerationService,
|
||||
deps: [
|
||||
KeyServiceAbstraction,
|
||||
KeyService,
|
||||
CipherServiceAbstraction,
|
||||
UserAsymmetricKeysRegenerationApiService,
|
||||
LogService,
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map, timeout } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -24,6 +25,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private accountService: AccountService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async startProcessReload(authService: AuthService): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum KeySuffixOptions {
|
||||
Auto = "auto",
|
||||
Biometric = "biometric",
|
||||
Pin = "pin",
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
@@ -41,6 +42,7 @@ describe("VaultTimeoutService", () => {
|
||||
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
|
||||
let taskSchedulerService: MockProxy<TaskSchedulerService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let biometricsService: MockProxy<BiometricsService>;
|
||||
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
|
||||
let loggedOutCallback: jest.Mock<Promise<void>, [logoutReason: LogoutReason, userId?: string]>;
|
||||
|
||||
@@ -66,6 +68,7 @@ describe("VaultTimeoutService", () => {
|
||||
stateEventRunnerService = mock();
|
||||
taskSchedulerService = mock<TaskSchedulerService>();
|
||||
logService = mock<LogService>();
|
||||
biometricsService = mock<BiometricsService>();
|
||||
|
||||
lockedCallback = jest.fn();
|
||||
loggedOutCallback = jest.fn();
|
||||
@@ -93,6 +96,7 @@ describe("VaultTimeoutService", () => {
|
||||
stateEventRunnerService,
|
||||
taskSchedulerService,
|
||||
logService,
|
||||
biometricsService,
|
||||
lockedCallback,
|
||||
loggedOutCallback,
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
@@ -41,6 +42,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private taskSchedulerService: TaskSchedulerService,
|
||||
protected logService: LogService,
|
||||
private biometricService: BiometricsService,
|
||||
private lockedCallback: (userId?: string) => Promise<void> = null,
|
||||
private loggedOutCallback: (
|
||||
logoutReason: LogoutReason,
|
||||
@@ -98,6 +100,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
}
|
||||
|
||||
async lock(userId?: UserId): Promise<void> {
|
||||
await this.biometricService.setShouldAutopromptNow(false);
|
||||
|
||||
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
|
||||
if (!authed) {
|
||||
return;
|
||||
|
||||
@@ -3,8 +3,4 @@
|
||||
*/
|
||||
|
||||
export { LockComponent } from "./lock/components/lock.component";
|
||||
export {
|
||||
LockComponentService,
|
||||
BiometricsDisableReason,
|
||||
UnlockOptions,
|
||||
} from "./lock/services/lock-component.service";
|
||||
export { LockComponentService, UnlockOptions } from "./lock/services/lock-component.service";
|
||||
|
||||
@@ -86,12 +86,13 @@
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
@@ -156,12 +157,13 @@
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,16 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
firstValueFrom,
|
||||
interval,
|
||||
mergeMap,
|
||||
Subject,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
@@ -27,7 +36,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -42,6 +50,8 @@ import {
|
||||
import {
|
||||
KeyService,
|
||||
BiometricStateService,
|
||||
BiometricsService,
|
||||
BiometricsStatus,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
@@ -115,9 +125,6 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
private deferFocus: boolean = null;
|
||||
private biometricAsked = false;
|
||||
|
||||
// Browser extension properties:
|
||||
private isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||
|
||||
defaultUnlockOptionSetForUser = false;
|
||||
|
||||
unlockingViaBiometrics = false;
|
||||
@@ -144,6 +151,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
|
||||
|
||||
private biometricService: BiometricsService,
|
||||
|
||||
private lockComponentService: LockComponentService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
|
||||
@@ -157,14 +166,31 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
// Listen for active account changes
|
||||
this.listenForActiveAccountChanges();
|
||||
|
||||
this.listenForUnlockOptionsChanges();
|
||||
|
||||
// Identify client
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
|
||||
if (this.clientType === "desktop") {
|
||||
await this.desktopOnInit();
|
||||
} else if (this.clientType === ClientType.Browser) {
|
||||
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
|
||||
}
|
||||
}
|
||||
|
||||
private listenForUnlockOptionsChanges() {
|
||||
interval(1000)
|
||||
.pipe(
|
||||
mergeMap(async () => {
|
||||
this.unlockOptions = await firstValueFrom(
|
||||
this.lockComponentService.getAvailableUnlockOptions$(this.activeAccount.id),
|
||||
);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
// Base component methods
|
||||
private listenForActiveUnlockOptionChanges() {
|
||||
this.activeUnlockOption$
|
||||
@@ -234,7 +260,6 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.unlockOptions = null;
|
||||
this.activeUnlockOption = null;
|
||||
this.formGroup = null; // new form group will be created based on new active unlock option
|
||||
this.isInitialLockScreen = true;
|
||||
|
||||
// Desktop properties:
|
||||
this.biometricAsked = false;
|
||||
@@ -276,8 +301,9 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
if (
|
||||
this.unlockOptions.biometrics.enabled &&
|
||||
autoPromptBiometrics &&
|
||||
this.isInitialLockScreen // only autoprompt biometrics on initial lock screen
|
||||
(await this.biometricService.getShouldAutopromptNow())
|
||||
) {
|
||||
await this.biometricService.setShouldAutopromptNow(false);
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
}
|
||||
@@ -316,8 +342,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
|
||||
try {
|
||||
await this.biometricStateService.setUserPromptCancelled();
|
||||
const userKey = await this.keyService.getUserKeyFromStorage(
|
||||
KeySuffixOptions.Biometric,
|
||||
const userKey = await this.biometricService.unlockWithBiometricsForUser(
|
||||
this.activeAccount.id,
|
||||
);
|
||||
|
||||
@@ -587,6 +612,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
async desktopOnInit() {
|
||||
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
|
||||
|
||||
// TODO: move this into a WindowService and subscribe to messages via MessageListener service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
@@ -617,6 +644,10 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.biometricService.getShouldAutopromptNow())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevent the biometric prompt from showing if the user has already cancelled it
|
||||
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
|
||||
return;
|
||||
@@ -650,4 +681,47 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
get biometricsAvailable(): boolean {
|
||||
return this.unlockOptions.biometrics.enabled;
|
||||
}
|
||||
|
||||
get showBiometrics(): boolean {
|
||||
return (
|
||||
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.PlatformUnsupported &&
|
||||
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.NotEnabledLocally
|
||||
);
|
||||
}
|
||||
|
||||
get biometricUnavailabilityReason(): string {
|
||||
switch (this.unlockOptions.biometrics.biometricsStatus) {
|
||||
case BiometricsStatus.Available:
|
||||
return "";
|
||||
case BiometricsStatus.UnlockNeeded:
|
||||
return this.i18nService.t("biometricsStatusHelptextUnlockNeeded");
|
||||
case BiometricsStatus.HardwareUnavailable:
|
||||
return this.i18nService.t("biometricsStatusHelptextHardwareUnavailable");
|
||||
case BiometricsStatus.AutoSetupNeeded:
|
||||
return this.i18nService.t("biometricsStatusHelptextAutoSetupNeeded");
|
||||
case BiometricsStatus.ManualSetupNeeded:
|
||||
return this.i18nService.t("biometricsStatusHelptextManualSetupNeeded");
|
||||
case BiometricsStatus.NotEnabledInConnectedDesktopApp:
|
||||
return this.i18nService.t(
|
||||
"biometricsStatusHelptextNotEnabledInDesktop",
|
||||
this.activeAccount.email,
|
||||
);
|
||||
case BiometricsStatus.NotEnabledLocally:
|
||||
return this.i18nService.t(
|
||||
"biometricsStatusHelptextNotEnabledInDesktop",
|
||||
this.activeAccount.email,
|
||||
);
|
||||
case BiometricsStatus.DesktopDisconnected:
|
||||
return this.i18nService.t("biometricsStatusHelptextDesktopDisconnected");
|
||||
default:
|
||||
return (
|
||||
this.i18nService.t("biometricsStatusHelptextUnavailableReasonUnknown") +
|
||||
this.unlockOptions.biometrics.biometricsStatus
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export enum BiometricsDisableReason {
|
||||
NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem",
|
||||
EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable",
|
||||
SystemBiometricsUnavailable = "SystemBiometricsUnavailable",
|
||||
}
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics"
|
||||
export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption];
|
||||
@@ -26,7 +21,7 @@ export type UnlockOptions = {
|
||||
};
|
||||
biometrics: {
|
||||
enabled: boolean;
|
||||
disableReason: BiometricsDisableReason | null;
|
||||
biometricsStatus: BiometricsStatus;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { BiometricsStatus } from "./biometrics-status";
|
||||
|
||||
/**
|
||||
* The biometrics service is used to provide access to the status of and access to biometric functionality on the platforms.
|
||||
*/
|
||||
export abstract class BiometricsService {
|
||||
supportsBiometric() {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
/**
|
||||
* Check if the platform supports biometric authentication.
|
||||
* Performs a biometric prompt, without unlocking any keys
|
||||
* @returns true if the biometric prompt was successful, false otherwise
|
||||
*/
|
||||
abstract supportsBiometric(): Promise<boolean>;
|
||||
abstract authenticateWithBiometrics(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Checks whether biometric unlock is currently available at the moment (e.g. if the laptop lid is shut, biometric unlock may not be available)
|
||||
* Gets the status of biometrics for the platform system states.
|
||||
* @returns the status of biometrics
|
||||
*/
|
||||
abstract isBiometricUnlockAvailable(): Promise<boolean>;
|
||||
abstract getBiometricsStatus(): Promise<BiometricsStatus>;
|
||||
|
||||
/**
|
||||
* Performs biometric authentication
|
||||
* Retrieves a userkey for the provided user, as present in the biometrics system.
|
||||
* THIS NEEDS TO BE VERIFIED FOR RECENCY AND VALIDITY
|
||||
* @param userId the user to unlock
|
||||
* @returns the user key
|
||||
*/
|
||||
abstract authenticateBiometric(): Promise<boolean>;
|
||||
abstract unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null>;
|
||||
|
||||
/**
|
||||
* Determine whether biometrics support requires going through a setup process.
|
||||
* This is currently only needed on Linux.
|
||||
*
|
||||
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
|
||||
* Gets the status of biometrics for a current user. This includes system states (hardware unavailable) but also user specific states (needs unlock with master-password).
|
||||
* @param userId the user to check the biometrics status for
|
||||
* @returns the status of biometrics for the user
|
||||
*/
|
||||
abstract biometricsNeedsSetup(): Promise<boolean>;
|
||||
/**
|
||||
* Determine whether biometrics support can be automatically setup, or requires user interaction.
|
||||
* Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak.
|
||||
*
|
||||
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
|
||||
*/
|
||||
abstract biometricsSupportsAutoSetup(): Promise<boolean>;
|
||||
/**
|
||||
* Start automatic biometric setup, which places the required configuration files / changes the required settings.
|
||||
*/
|
||||
abstract biometricsSetup(): Promise<void>;
|
||||
abstract getBiometricsStatusForUser(userId: UserId): Promise<BiometricsStatus>;
|
||||
|
||||
abstract getShouldAutopromptNow(): Promise<boolean>;
|
||||
abstract setShouldAutopromptNow(value: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
14
libs/key-management/src/biometrics/biometrics-commands.ts
Normal file
14
libs/key-management/src/biometrics/biometrics-commands.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export enum BiometricsCommands {
|
||||
/** Perform biometric authentication for the system's user. Does not require setup, and does not return cryptographic material, only yes or no. */
|
||||
AuthenticateWithBiometrics = "authenticateWithBiometrics",
|
||||
/** Get biometric status of the system, and can be used before biometrics is set up. Only returns data about the biometrics system, not about availability of cryptographic material */
|
||||
GetBiometricsStatus = "getBiometricsStatus",
|
||||
/** Perform biometric authentication for the system's user for the given bitwarden account's credentials. This returns cryptographic material that can be used to unlock the vault. */
|
||||
UnlockWithBiometricsForUser = "unlockWithBiometricsForUser",
|
||||
/** Get biometric status for a specific user account. This includes both information about availability of cryptographic material (is the user configured for biometric unlock? is a masterpassword unlock needed? But also information about the biometric system's availability in a single status) */
|
||||
GetBiometricsStatusForUser = "getBiometricsStatusForUser",
|
||||
|
||||
// legacy
|
||||
Unlock = "biometricUnlock",
|
||||
IsAvailable = "biometricUnlockAvailable",
|
||||
}
|
||||
22
libs/key-management/src/biometrics/biometrics-status.ts
Normal file
22
libs/key-management/src/biometrics/biometrics-status.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export enum BiometricsStatus {
|
||||
/** For the biometrics interface, this means that biometric unlock is available and can be used. Querying for the user specifically, this means that biometric can be used for to unlock this user */
|
||||
Available,
|
||||
/** Biometrics cannot be used, because the userkey needs to first be unlocked by the user's password, because unlock needs some volatile data that is not available on app-start */
|
||||
UnlockNeeded,
|
||||
/** Biometric hardware is not available (i.e laptop folded shut, sensor unplugged) */
|
||||
HardwareUnavailable,
|
||||
/** Only relevant for linux, this means that polkit policies need to be set up and that can happen automatically */
|
||||
AutoSetupNeeded,
|
||||
/** Only relevant for linux, this means that polkit policies need to be set up but that needs to be done manually */
|
||||
ManualSetupNeeded,
|
||||
/** Biometrics is not implemented for this platform (i.e web) */
|
||||
PlatformUnsupported,
|
||||
/** Browser extension cannot connect to the desktop app to use biometrics */
|
||||
DesktopDisconnected,
|
||||
/** Biometrics is not enabled in the desktop app/extension (current app) */
|
||||
NotEnabledLocally,
|
||||
/** Only on browser extension; Biometrics is not enabled in the desktop app */
|
||||
NotEnabledInConnectedDesktopApp,
|
||||
/** Browser extension does not have the permission to talk to the desktop app */
|
||||
NativeMessagingPermissionMissing,
|
||||
}
|
||||
@@ -2,6 +2,8 @@ export {
|
||||
BiometricStateService,
|
||||
DefaultBiometricStateService,
|
||||
} from "./biometrics/biometric-state.service";
|
||||
export { BiometricsStatus } from "./biometrics/biometrics-status";
|
||||
export { BiometricsCommands } from "./biometrics/biometrics-commands";
|
||||
export { BiometricsService } from "./biometrics/biometric.service";
|
||||
export * from "./biometrics/biometric.state";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user