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 } 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; /** * 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; } export class DefaultBiometricStateService implements BiometricStateService { private requirePasswordOnStartState: ActiveUserState; private encryptedClientKeyHalfState: ActiveUserState; encryptedClientKeyHalf$: Observable; requirePasswordOnStart$: 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), ); } 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); } } function encryptedClientKeyHalfToEncString( encryptedKeyHalf: EncryptedString | undefined, ): EncString { return encryptedKeyHalf == null ? null : new EncString(encryptedKeyHalf); }