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 { ENCRYPTED_CLIENT_KEY_HALF, REQUIRE_PASSWORD_ON_START, DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, PROMPT_AUTOMATICALLY, PROMPT_CANCELLED, } from "./biometric.state"; export abstract class BiometricStateService { /** * If the user has elected to require a password on first unlock of an application instance, this key will store the * encrypted client key half used to unlock the vault. * * Tracks the currently active user */ encryptedClientKeyHalf$: Observable; /** * whether or not a password is required on first unlock after opening the application * * tracks the currently active user */ requirePasswordOnStart$: Observable; /** * Indicates the user has been warned about the security implications of using biometrics and, depending on the OS, * * tracks the currently active user. */ dismissedRequirePasswordOnStartCallout$: Observable; /** * Whether the user has cancelled the biometric prompt. * * tracks the currently active user */ promptCancelled$: Observable; /** * Whether the user has elected to automatically prompt for biometrics. * * tracks the currently active user */ promptAutomatically$: Observable; /** * Updates the require password on start state for the currently active user. * * If false, the encrypted client key half will be removed. * @param value whether or not a password is required on first unlock after opening the application */ abstract setRequirePasswordOnStart(value: boolean): Promise; abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString, userId?: UserId): Promise; abstract getEncryptedClientKeyHalf(userId: UserId): Promise; abstract getRequirePasswordOnStart(userId: UserId): Promise; abstract removeEncryptedClientKeyHalf(userId: UserId): Promise; /** * Updates the active user's state to reflect that they've been warned about requiring password on start. */ abstract setDismissedRequirePasswordOnStartCallout(): Promise; /** * Updates the active user's state to reflect that they've cancelled the biometric prompt this lock. */ abstract setPromptCancelled(): Promise; /** * Resets the active user's state to reflect that they haven't cancelled the biometric prompt this lock. */ abstract resetPromptCancelled(): Promise; /** * Updates the currently active user's setting for auto prompting for biometrics on application start and lock * @param prompt Whether or not to prompt for biometrics on application start. */ abstract setPromptAutomatically(prompt: boolean): Promise; abstract logout(userId: UserId): Promise; } export class DefaultBiometricStateService implements BiometricStateService { private requirePasswordOnStartState: ActiveUserState; private encryptedClientKeyHalfState: ActiveUserState; private dismissedRequirePasswordOnStartCalloutState: ActiveUserState; private promptCancelledState: ActiveUserState; private promptAutomaticallyState: ActiveUserState; encryptedClientKeyHalf$: Observable; requirePasswordOnStart$: Observable; dismissedRequirePasswordOnStartCallout$: Observable; promptCancelled$: Observable; promptAutomatically$: Observable; constructor(private stateProvider: StateProvider) { this.requirePasswordOnStartState = this.stateProvider.getActive(REQUIRE_PASSWORD_ON_START); this.requirePasswordOnStart$ = this.requirePasswordOnStartState.state$.pipe( map((value) => !!value), ); this.encryptedClientKeyHalfState = this.stateProvider.getActive(ENCRYPTED_CLIENT_KEY_HALF); this.encryptedClientKeyHalf$ = this.encryptedClientKeyHalfState.state$.pipe( map(encryptedClientKeyHalfToEncString), ); this.dismissedRequirePasswordOnStartCalloutState = this.stateProvider.getActive( DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, ); this.dismissedRequirePasswordOnStartCallout$ = this.dismissedRequirePasswordOnStartCalloutState.state$.pipe(map((v) => !!v)); this.promptCancelledState = this.stateProvider.getActive(PROMPT_CANCELLED); this.promptCancelled$ = this.promptCancelledState.state$.pipe(map((v) => !!v)); this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY); this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map((v) => !!v)); } async setRequirePasswordOnStart(value: boolean): Promise { let currentActiveId: UserId; await this.requirePasswordOnStartState.update( (_, [userId]) => { currentActiveId = userId; return value; }, { combineLatestWith: this.requirePasswordOnStartState.combinedState$, }, ); if (!value) { await this.removeEncryptedClientKeyHalf(currentActiveId); } } async setEncryptedClientKeyHalf(encryptedKeyHalf: EncString, userId?: UserId): Promise { const value = encryptedKeyHalf?.encryptedString ?? null; if (userId) { await this.stateProvider.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF).update(() => value); } else { await this.encryptedClientKeyHalfState.update(() => value); } } async removeEncryptedClientKeyHalf(userId: UserId): Promise { await this.stateProvider.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF).update(() => null); } async getRequirePasswordOnStart(userId: UserId): Promise { return !!(await firstValueFrom( this.stateProvider.getUser(userId, REQUIRE_PASSWORD_ON_START).state$, )); } async getEncryptedClientKeyHalf(userId: UserId): Promise { return await firstValueFrom( this.stateProvider .getUser(userId, ENCRYPTED_CLIENT_KEY_HALF) .state$.pipe(map(encryptedClientKeyHalfToEncString)), ); } async logout(userId: UserId): Promise { await this.stateProvider.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF).update(() => null); await this.stateProvider.getUser(userId, PROMPT_CANCELLED).update(() => null); // Persist auto prompt setting through logout // Persist dismissed require password on start callout through logout } async setDismissedRequirePasswordOnStartCallout(): Promise { await this.dismissedRequirePasswordOnStartCalloutState.update(() => true); } async setPromptCancelled(): Promise { await this.promptCancelledState.update(() => true); } async resetPromptCancelled(): Promise { await this.promptCancelledState.update(() => null); } async setPromptAutomatically(prompt: boolean): Promise { await this.promptAutomaticallyState.update(() => prompt); } } function encryptedClientKeyHalfToEncString( encryptedKeyHalf: EncryptedString | undefined, ): EncString { return encryptedKeyHalf == null ? null : new EncString(encryptedKeyHalf); }