Move fingerprint validated to biometric state povider (#8058)

This commit is contained in:
Matt Gibson 2024-03-07 19:41:56 -06:00 committed by GitHub
parent eedd6f0881
commit f83dcf2b24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 74 additions and 15 deletions

View File

@ -814,6 +814,7 @@ export default class MainBackground {
this.stateService,
this.autofillSettingsService,
this.vaultTimeoutSettingsService,
this.biometricStateService,
);
// Other fields

View File

@ -84,10 +84,6 @@ export class NativeMessagingBackground {
private authService: AuthService,
private biometricStateService: BiometricStateService,
) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBiometricFingerprintValidated(false);
if (chrome?.permissions?.onAdded) {
// Reload extension to activate nativeMessaging
chrome.permissions.onAdded.addListener((permissions) => {
@ -100,9 +96,7 @@ export class NativeMessagingBackground {
async connect() {
this.appId = await this.appIdService.getAppId();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBiometricFingerprintValidated(false);
await this.biometricStateService.setFingerprintValidated(false);
return new Promise<void>((resolve, reject) => {
this.port = BrowserApi.connectNative("com.8bit.bitwarden");
@ -148,9 +142,7 @@ export class NativeMessagingBackground {
if (this.validatingFingerprint) {
this.validatingFingerprint = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBiometricFingerprintValidated(true);
await this.biometricStateService.setFingerprintValidated(true);
}
this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.secureSetupResolve();

View File

@ -415,7 +415,7 @@ export class SettingsComponent implements OnInit {
]);
} else {
await this.biometricStateService.setBiometricUnlockEnabled(false);
await this.stateService.setBiometricFingerprintValidated(false);
await this.biometricStateService.setFingerprintValidated(false);
}
}

View File

@ -126,6 +126,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
StateServiceAbstraction,
AutofillSettingsServiceAbstraction,
VaultTimeoutSettingsService,
BiometricStateService,
],
},
{

View File

@ -12,6 +12,7 @@ import {
BIOMETRIC_UNLOCK_ENABLED,
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
ENCRYPTED_CLIENT_KEY_HALF,
FINGERPRINT_VALIDATED,
PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED,
REQUIRE_PASSWORD_ON_START,
@ -67,6 +68,19 @@ describe("BiometricStateService", () => {
});
});
describe("fingerprintValidated$", () => {
it("emits when the fingerprint validated state changes", async () => {
const state = stateProvider.global.getFake(FINGERPRINT_VALIDATED);
state.stateSubject.next(undefined);
expect(await firstValueFrom(sut.fingerprintValidated$)).toBe(false);
state.stateSubject.next(true);
expect(await firstValueFrom(sut.fingerprintValidated$)).toEqual(true);
});
});
describe("setEncryptedClientKeyHalf", () => {
it("updates encryptedClientKeyHalf$", async () => {
await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
@ -207,4 +221,20 @@ describe("BiometricStateService", () => {
expect(await sut.getBiometricUnlockEnabled(userId)).toBe(false);
});
});
describe("setFingerprintValidated", () => {
it("updates fingerprintValidated$", async () => {
await sut.setFingerprintValidated(true);
expect(await firstValueFrom(sut.fingerprintValidated$)).toBe(true);
});
it("updates state", async () => {
await sut.setFingerprintValidated(true);
expect(stateProvider.global.getFake(FINGERPRINT_VALIDATED).nextMock).toHaveBeenCalledWith(
true,
);
});
});
});

View File

@ -2,7 +2,7 @@ import { Observable, firstValueFrom, map } from "rxjs";
import { UserId } from "../../types/guid";
import { EncryptedString, EncString } from "../models/domain/enc-string";
import { ActiveUserState, StateProvider } from "../state";
import { ActiveUserState, GlobalState, StateProvider } from "../state";
import {
BIOMETRIC_UNLOCK_ENABLED,
@ -11,6 +11,7 @@ import {
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED,
FINGERPRINT_VALIDATED,
} from "./biometric.state";
export abstract class BiometricStateService {
@ -49,6 +50,10 @@ export abstract class BiometricStateService {
* tracks the currently active user
*/
promptAutomatically$: Observable<boolean>;
/**
* Whether or not IPC fingerprint has been validated by the user this session.
*/
fingerprintValidated$: Observable<boolean>;
/**
* Updates the require password on start state for the currently active user.
@ -88,6 +93,11 @@ export abstract class BiometricStateService {
* @param prompt Whether or not to prompt for biometrics on application start.
*/
abstract setPromptAutomatically(prompt: boolean): Promise<void>;
/**
* Updates whether or not IPC has been validated by the user this session
* @param validated the value to save
*/
abstract setFingerprintValidated(validated: boolean): Promise<void>;
abstract logout(userId: UserId): Promise<void>;
}
@ -99,12 +109,14 @@ export class DefaultBiometricStateService implements BiometricStateService {
private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>;
private promptCancelledState: ActiveUserState<boolean>;
private promptAutomaticallyState: ActiveUserState<boolean>;
private fingerprintValidatedState: GlobalState<boolean>;
biometricUnlockEnabled$: Observable<boolean>;
encryptedClientKeyHalf$: Observable<EncString | undefined>;
requirePasswordOnStart$: Observable<boolean>;
dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
promptCancelled$: Observable<boolean>;
promptAutomatically$: Observable<boolean>;
fingerprintValidated$: Observable<boolean>;
constructor(private stateProvider: StateProvider) {
this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED);
@ -130,6 +142,9 @@ export class DefaultBiometricStateService implements BiometricStateService {
this.promptCancelled$ = this.promptCancelledState.state$.pipe(map(Boolean));
this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY);
this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map(Boolean));
this.fingerprintValidatedState = this.stateProvider.getGlobal(FINGERPRINT_VALIDATED);
this.fingerprintValidated$ = this.fingerprintValidatedState.state$.pipe(map(Boolean));
}
async setBiometricUnlockEnabled(enabled: boolean): Promise<void> {
@ -207,6 +222,10 @@ export class DefaultBiometricStateService implements BiometricStateService {
async setPromptAutomatically(prompt: boolean): Promise<void> {
await this.promptAutomaticallyState.update(() => prompt);
}
async setFingerprintValidated(validated: boolean): Promise<void> {
await this.fingerprintValidatedState.update(() => validated);
}
}
function encryptedClientKeyHalfToEncString(

View File

@ -5,6 +5,7 @@ import {
BIOMETRIC_UNLOCK_ENABLED,
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
ENCRYPTED_CLIENT_KEY_HALF,
FINGERPRINT_VALIDATED,
PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED,
REQUIRE_PASSWORD_ON_START,
@ -16,7 +17,8 @@ describe.each([
[PROMPT_CANCELLED, true],
[PROMPT_AUTOMATICALLY, true],
[REQUIRE_PASSWORD_ON_START, true],
[BIOMETRIC_UNLOCK_ENABLED, "test"],
[BIOMETRIC_UNLOCK_ENABLED, true],
[FINGERPRINT_VALIDATED, true],
])(
"deserializes state %s",
(

View File

@ -74,3 +74,14 @@ export const PROMPT_AUTOMATICALLY = new KeyDefinition<boolean>(
deserializer: (obj) => obj,
},
);
/**
* Stores whether or not IPC handshake has been validated this session.
*/
export const FINGERPRINT_VALIDATED = new KeyDefinition<boolean>(
BIOMETRIC_SETTINGS_DISK,
"fingerprintValidated",
{
deserializer: (obj) => obj,
},
);

View File

@ -9,6 +9,7 @@ import { MessagingService } from "../abstractions/messaging.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service";
import { BiometricStateService } from "../biometrics/biometric-state.service";
import { Utils } from "../misc/utils";
export class SystemService implements SystemServiceAbstraction {
@ -23,6 +24,7 @@ export class SystemService implements SystemServiceAbstraction {
private stateService: StateService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private biometricStateService: BiometricStateService,
) {}
async startProcessReload(authService: AuthService): Promise<void> {
@ -54,8 +56,9 @@ export class SystemService implements SystemServiceAbstraction {
}
private async executeProcessReload() {
const biometricLockedFingerprintValidated =
await this.stateService.getBiometricFingerprintValidated();
const biometricLockedFingerprintValidated = await firstValueFrom(
this.biometricStateService.fingerprintValidated$,
);
if (!biometricLockedFingerprintValidated) {
clearInterval(this.reloadInterval);
this.reloadInterval = null;