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.stateService,
this.autofillSettingsService, this.autofillSettingsService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
this.biometricStateService,
); );
// Other fields // Other fields

View File

@ -84,10 +84,6 @@ export class NativeMessagingBackground {
private authService: AuthService, private authService: AuthService,
private biometricStateService: BiometricStateService, 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) { if (chrome?.permissions?.onAdded) {
// Reload extension to activate nativeMessaging // Reload extension to activate nativeMessaging
chrome.permissions.onAdded.addListener((permissions) => { chrome.permissions.onAdded.addListener((permissions) => {
@ -100,9 +96,7 @@ export class NativeMessagingBackground {
async connect() { async connect() {
this.appId = await this.appIdService.getAppId(); 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. await this.biometricStateService.setFingerprintValidated(false);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBiometricFingerprintValidated(false);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.port = BrowserApi.connectNative("com.8bit.bitwarden"); this.port = BrowserApi.connectNative("com.8bit.bitwarden");
@ -148,9 +142,7 @@ export class NativeMessagingBackground {
if (this.validatingFingerprint) { if (this.validatingFingerprint) {
this.validatingFingerprint = false; 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. await this.biometricStateService.setFingerprintValidated(true);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBiometricFingerprintValidated(true);
} }
this.sharedSecret = new SymmetricCryptoKey(decrypted); this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.secureSetupResolve(); this.secureSetupResolve();

View File

@ -415,7 +415,7 @@ export class SettingsComponent implements OnInit {
]); ]);
} else { } else {
await this.biometricStateService.setBiometricUnlockEnabled(false); 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, StateServiceAbstraction,
AutofillSettingsServiceAbstraction, AutofillSettingsServiceAbstraction,
VaultTimeoutSettingsService, VaultTimeoutSettingsService,
BiometricStateService,
], ],
}, },
{ {

View File

@ -12,6 +12,7 @@ import {
BIOMETRIC_UNLOCK_ENABLED, BIOMETRIC_UNLOCK_ENABLED,
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
ENCRYPTED_CLIENT_KEY_HALF, ENCRYPTED_CLIENT_KEY_HALF,
FINGERPRINT_VALIDATED,
PROMPT_AUTOMATICALLY, PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED, PROMPT_CANCELLED,
REQUIRE_PASSWORD_ON_START, 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", () => { describe("setEncryptedClientKeyHalf", () => {
it("updates encryptedClientKeyHalf$", async () => { it("updates encryptedClientKeyHalf$", async () => {
await sut.setEncryptedClientKeyHalf(encClientKeyHalf); await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
@ -207,4 +221,20 @@ describe("BiometricStateService", () => {
expect(await sut.getBiometricUnlockEnabled(userId)).toBe(false); 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 { UserId } from "../../types/guid";
import { EncryptedString, EncString } from "../models/domain/enc-string"; import { EncryptedString, EncString } from "../models/domain/enc-string";
import { ActiveUserState, StateProvider } from "../state"; import { ActiveUserState, GlobalState, StateProvider } from "../state";
import { import {
BIOMETRIC_UNLOCK_ENABLED, BIOMETRIC_UNLOCK_ENABLED,
@ -11,6 +11,7 @@ import {
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
PROMPT_AUTOMATICALLY, PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED, PROMPT_CANCELLED,
FINGERPRINT_VALIDATED,
} from "./biometric.state"; } from "./biometric.state";
export abstract class BiometricStateService { export abstract class BiometricStateService {
@ -49,6 +50,10 @@ export abstract class BiometricStateService {
* tracks the currently active user * tracks the currently active user
*/ */
promptAutomatically$: Observable<boolean>; 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. * 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. * @param prompt Whether or not to prompt for biometrics on application start.
*/ */
abstract setPromptAutomatically(prompt: boolean): Promise<void>; 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>; abstract logout(userId: UserId): Promise<void>;
} }
@ -99,12 +109,14 @@ export class DefaultBiometricStateService implements BiometricStateService {
private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>; private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>;
private promptCancelledState: ActiveUserState<boolean>; private promptCancelledState: ActiveUserState<boolean>;
private promptAutomaticallyState: ActiveUserState<boolean>; private promptAutomaticallyState: ActiveUserState<boolean>;
private fingerprintValidatedState: GlobalState<boolean>;
biometricUnlockEnabled$: Observable<boolean>; biometricUnlockEnabled$: Observable<boolean>;
encryptedClientKeyHalf$: Observable<EncString | undefined>; encryptedClientKeyHalf$: Observable<EncString | undefined>;
requirePasswordOnStart$: Observable<boolean>; requirePasswordOnStart$: Observable<boolean>;
dismissedRequirePasswordOnStartCallout$: Observable<boolean>; dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
promptCancelled$: Observable<boolean>; promptCancelled$: Observable<boolean>;
promptAutomatically$: Observable<boolean>; promptAutomatically$: Observable<boolean>;
fingerprintValidated$: Observable<boolean>;
constructor(private stateProvider: StateProvider) { constructor(private stateProvider: StateProvider) {
this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED); 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.promptCancelled$ = this.promptCancelledState.state$.pipe(map(Boolean));
this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY); this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY);
this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map(Boolean)); 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> { async setBiometricUnlockEnabled(enabled: boolean): Promise<void> {
@ -207,6 +222,10 @@ export class DefaultBiometricStateService implements BiometricStateService {
async setPromptAutomatically(prompt: boolean): Promise<void> { async setPromptAutomatically(prompt: boolean): Promise<void> {
await this.promptAutomaticallyState.update(() => prompt); await this.promptAutomaticallyState.update(() => prompt);
} }
async setFingerprintValidated(validated: boolean): Promise<void> {
await this.fingerprintValidatedState.update(() => validated);
}
} }
function encryptedClientKeyHalfToEncString( function encryptedClientKeyHalfToEncString(

View File

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

View File

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