From 2ca34b46dbfdddcbd2f34d0aadb48d80ba65d122 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 7 Feb 2024 10:39:54 -0500 Subject: [PATCH] [PM-5537] Persist require password on startup through logout (#7825) * Persist require password on startup through logout * Test new methods --- .../src/app/accounts/settings.component.ts | 10 ++-- .../src/app/services/services.module.ts | 11 +--- apps/desktop/src/auth/lock.component.spec.ts | 7 +-- apps/desktop/src/auth/lock.component.ts | 8 ++- .../services/electron-crypto.service.spec.ts | 57 ++++-------------- .../services/electron-crypto.service.ts | 60 +++++++------------ .../biometric-state.service.spec.ts | 52 +++++++++++++--- .../biometrics/biometric-state.service.ts | 50 +++++++++++++--- .../biometrics/biometric.state.spec.ts | 14 ++++- .../platform/biometrics/biometric.state.ts | 13 ++++ 10 files changed, 161 insertions(+), 121 deletions(-) diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 3b4bc82405..caa122dc90 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -12,6 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -22,7 +23,6 @@ import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; import { flagEnabled } from "../../platform/flags"; -import { ElectronCryptoService } from "../../platform/services/electron-crypto.service"; import { ElectronStateService } from "../../platform/services/electron-state.service.abstraction"; @Component({ selector: "app-settings", @@ -113,7 +113,7 @@ export class SettingsComponent implements OnInit { private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private stateService: ElectronStateService, private messagingService: MessagingService, - private cryptoService: ElectronCryptoService, + private cryptoService: CryptoService, private modalService: ModalService, private themingService: AbstractThemingService, private settingsService: SettingsService, @@ -457,7 +457,7 @@ export class SettingsComponent implements OnInit { this.form.controls.requirePasswordOnStart.setValue(true); this.form.controls.autoPromptBiometrics.setValue(false); await this.stateService.setDisableAutoBiometricsPrompt(true); - await this.cryptoService.setBiometricClientKeyHalf(); + await this.biometricStateService.setRequirePasswordOnStart(true); await this.stateService.setDismissedBiometricRequirePasswordOnStart(); } await this.cryptoService.refreshAdditionalKeys(); @@ -491,9 +491,9 @@ export class SettingsComponent implements OnInit { this.form.controls.autoPromptBiometrics.setValue(false); await this.updateAutoPromptBiometrics(); - await this.cryptoService.setBiometricClientKeyHalf(); + await this.biometricStateService.setRequirePasswordOnStart(true); } else { - await this.cryptoService.removeBiometricClientKeyHalf(); + await this.biometricStateService.setRequirePasswordOnStart(false); } await this.stateService.setDismissedBiometricRequirePasswordOnStart(); await this.cryptoService.refreshAdditionalKeys(); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 71c936ca59..3b991bd7af 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -48,10 +48,7 @@ import { DialogService } from "@bitwarden/components"; import { LoginGuard } from "../../auth/guards/login.guard"; import { Account } from "../../models/account"; -import { - DefaultElectronCryptoService, - ElectronCryptoService, -} from "../../platform/services/electron-crypto.service"; +import { ElectronCryptoService } from "../../platform/services/electron-crypto.service"; import { ElectronLogService } from "../../platform/services/electron-log.service"; import { ElectronPlatformUtilsService } from "../../platform/services/electron-platform-utils.service"; import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service"; @@ -182,11 +179,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); }, { provide: CryptoServiceAbstraction, - useExisting: ElectronCryptoService, - }, - { - provide: ElectronCryptoService, - useClass: DefaultElectronCryptoService, + useClass: ElectronCryptoService, deps: [ CryptoFunctionServiceAbstraction, EncryptService, diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 6b2d6c01f6..f428bda50e 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -25,7 +25,6 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { DialogService } from "@bitwarden/components"; -import { ElectronCryptoService } from "../platform/services/electron-crypto.service"; import { ElectronStateService } from "../platform/services/electron-state.service.abstraction"; import { LockComponent } from "./lock.component"; @@ -80,11 +79,7 @@ describe("LockComponent", () => { }, { provide: CryptoService, - useExisting: ElectronCryptoService, - }, - { - provide: ElectronCryptoService, - useValue: mock(), + useValue: mock(), }, { provide: VaultTimeoutService, diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 2c0e093f6a..6af535eda4 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -13,15 +13,16 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { DialogService } from "@bitwarden/components"; -import { ElectronCryptoService } from "../platform/services/electron-crypto.service"; import { ElectronStateService } from "../platform/services/electron-state.service.abstraction"; const BroadcasterSubscriptionId = "LockComponent"; @@ -41,7 +42,7 @@ export class LockComponent extends BaseLockComponent { i18nService: I18nService, platformUtilsService: PlatformUtilsService, messagingService: MessagingService, - protected override cryptoService: ElectronCryptoService, + cryptoService: CryptoService, vaultTimeoutService: VaultTimeoutService, vaultTimeoutSettingsService: VaultTimeoutSettingsService, environmentService: EnvironmentService, @@ -58,6 +59,7 @@ export class LockComponent extends BaseLockComponent { deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, + private biometricStateService: BiometricStateService, ) { super( router, @@ -175,8 +177,8 @@ export class LockComponent extends BaseLockComponent { type: "warning", }); + await this.biometricStateService.setRequirePasswordOnStart(response); if (response) { - await this.cryptoService.setBiometricClientKeyHalf(); await this.stateService.setDisableAutoBiometricsPrompt(true); } this.supportsBiometric = await this.canUseBiometric(); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 50c1f2101a..8dbc9294f4 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -6,9 +6,8 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { makeEncString, makeStaticByteArray } from "@bitwarden/common/spec"; +import { makeEncString } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -18,11 +17,11 @@ import { mockAccountServiceWith, } from "../../../../../libs/common/spec/fake-account-service"; -import { DefaultElectronCryptoService } from "./electron-crypto.service"; +import { ElectronCryptoService } from "./electron-crypto.service"; import { ElectronStateService } from "./electron-state.service.abstraction"; describe("electronCryptoService", () => { - let sut: DefaultElectronCryptoService; + let sut: ElectronCryptoService; const cryptoFunctionService = mock(); const encryptService = mock(); @@ -39,7 +38,7 @@ describe("electronCryptoService", () => { accountService = mockAccountServiceWith("userId" as UserId); stateProvider = new FakeStateProvider(accountService); - sut = new DefaultElectronCryptoService( + sut = new ElectronCryptoService( cryptoFunctionService, encryptService, platformUtilService, @@ -55,44 +54,6 @@ describe("electronCryptoService", () => { jest.resetAllMocks(); }); - describe("setBiometricClientKeyHalf", () => { - const userKey = new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as UserKey; - const keyBytes = makeStaticByteArray(32, 2) as CsprngArray; - const encKeyHalf = makeEncString(Utils.fromBufferToUtf8(keyBytes)); - - beforeEach(() => { - sut.getUserKey = jest.fn().mockResolvedValue(userKey); - cryptoFunctionService.randomBytes.mockResolvedValue(keyBytes); - encryptService.encrypt.mockResolvedValue(encKeyHalf); - }); - - it("sets a biometric client key half for the currently active user", async () => { - await sut.setBiometricClientKeyHalf(); - - expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(encKeyHalf); - }); - - it("should create the key from csprng bytes", async () => { - await sut.setBiometricClientKeyHalf(); - - expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32); - }); - - it("should encrypt the key half with the user key", async () => { - await sut.setBiometricClientKeyHalf(); - - expect(encryptService.encrypt).toHaveBeenCalledWith(expect.any(String), userKey); - }); - }); - - describe("removeBiometricClientKeyHalf", () => { - it("removes the biometric client key half for the currently active user", async () => { - await sut.removeBiometricClientKeyHalf(); - - expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(null); - }); - }); - describe("setUserKey", () => { let mockUserKey: UserKey; @@ -102,15 +63,23 @@ describe("electronCryptoService", () => { }); describe("Biometric Key refresh", () => { + const encClientKeyHalf = makeEncString(); + const decClientKeyHalf = "decrypted client key half"; + + beforeEach(() => { + encClientKeyHalf.decrypt = jest.fn().mockResolvedValue(decClientKeyHalf); + }); + it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => { stateService.getBiometricUnlock.mockResolvedValue(true); platformUtilService.supportsSecureStorage.mockReturnValue(true); biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true); + biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(encClientKeyHalf); await sut.setUserKey(mockUserKey, mockUserId); expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith( - expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: null }), + expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: decClientKeyHalf }), { userId: mockUserId, }, diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 9fa64e231b..ab9974c955 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,5 +1,3 @@ -import { firstValueFrom } from "rxjs"; - import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -18,18 +16,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { ElectronStateService } from "./electron-state.service.abstraction"; -export abstract class ElectronCryptoService extends CryptoService { - /** - * Creates and sets a new biometric client key half for the currently active user. - */ - abstract setBiometricClientKeyHalf(): Promise; - /** - * Removes the biometric client key half for the currently active user. - */ - abstract removeBiometricClientKeyHalf(): Promise; -} - -export class DefaultElectronCryptoService extends ElectronCryptoService { +export class ElectronCryptoService extends CryptoService { constructor( cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -72,19 +59,6 @@ export class DefaultElectronCryptoService extends ElectronCryptoService { await super.clearStoredUserKey(keySuffix, userId); } - async setBiometricClientKeyHalf(): Promise { - const userKey = await this.getUserKey(); - const keyBytes = await this.cryptoFunctionService.randomBytes(32); - const biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString; - const encKey = await this.encryptService.encrypt(biometricKey, userKey); - - await this.biometricStateService.setEncryptedClientKeyHalf(encKey); - } - - async removeBiometricClientKeyHalf(): Promise { - await this.biometricStateService.setEncryptedClientKeyHalf(null); - } - protected override async storeAdditionalKeys(key: UserKey, userId?: UserId) { await super.storeAdditionalKeys(key, userId); @@ -112,7 +86,7 @@ export class DefaultElectronCryptoService extends ElectronCryptoService { protected async storeBiometricKey(key: UserKey, userId?: UserId): Promise { // May resolve to null, in which case no client key have is required - const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId); + const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(key, userId); await this.stateService.setUserKeyBiometric( { key: key.keyB64, clientEncKeyHalf }, { userId: userId }, @@ -132,17 +106,29 @@ export class DefaultElectronCryptoService extends ElectronCryptoService { await super.clearAllStoredUserKeys(userId); } - private async getBiometricEncryptionClientKeyHalf(userId?: UserId): Promise { - const encryptedKeyHalfPromise = - userId == null - ? firstValueFrom(this.biometricStateService.encryptedClientKeyHalf$) - : this.biometricStateService.getEncryptedClientKeyHalf(userId); - const encryptedKeyHalf = await encryptedKeyHalfPromise; - if (encryptedKeyHalf == null) { + private async getBiometricEncryptionClientKeyHalf( + userKey: UserKey, + userId: UserId, + ): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { return null; } - const userKey = await this.getUserKey(); - return (await this.encryptService.decryptToUtf8(encryptedKeyHalf, userKey)) as CsprngString; + + // Retrieve existing key half if it exists + let biometricKey = await this.biometricStateService + .getEncryptedClientKeyHalf(userId) + .then((result) => result?.decrypt(null /* user encrypted */, userKey)) + .then((result) => result as CsprngString); + if (biometricKey == null && userKey != null) { + // Set a key half if it doesn't exist + const keyBytes = await this.cryptoFunctionService.randomBytes(32); + biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString; + const encKey = await this.encryptService.encrypt(biometricKey, userKey); + await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); + } + + return biometricKey; } // --LEGACY METHODS-- diff --git a/libs/common/src/platform/biometrics/biometric-state.service.spec.ts b/libs/common/src/platform/biometrics/biometric-state.service.spec.ts index 6c33d1be33..5752f69271 100644 --- a/libs/common/src/platform/biometrics/biometric-state.service.spec.ts +++ b/libs/common/src/platform/biometrics/biometric-state.service.spec.ts @@ -2,11 +2,13 @@ import { firstValueFrom } from "rxjs"; import { makeEncString } from "../../../spec"; import { mockAccountServiceWith } from "../../../spec/fake-account-service"; +import { FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { UserId } from "../../types/guid"; +import { EncryptedString } from "../models/domain/enc-string"; import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service"; -import { ENCRYPTED_CLIENT_KEY_HALF } from "./biometric.state"; +import { ENCRYPTED_CLIENT_KEY_HALF, REQUIRE_PASSWORD_ON_START } from "./biometric.state"; describe("BiometricStateService", () => { let sut: BiometricStateService; @@ -27,13 +29,14 @@ describe("BiometricStateService", () => { }); describe("requirePasswordOnStart$", () => { - it("should be false when encryptedClientKeyHalf is undefined", async () => { - stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF).nextState(undefined); - expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(false); - }); + it("should track the requirePasswordOnStart state", async () => { + const state = stateProvider.activeUser.getFake(REQUIRE_PASSWORD_ON_START); + state.nextState(undefined); + + expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(false); + + state.nextState(true); - it("should be true when encryptedClientKeyHalf is defined", async () => { - stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF).nextState(encryptedClientKeyHalf); expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(true); }); }); @@ -58,4 +61,39 @@ describe("BiometricStateService", () => { expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf); }); }); + + describe("setRequirePasswordOnStart", () => { + it("should update the requirePasswordOnStart$", async () => { + await sut.setRequirePasswordOnStart(true); + + expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(true); + }); + + it("should remove the encryptedClientKeyHalf if the value is false", async () => { + await sut.setEncryptedClientKeyHalf(encClientKeyHalf, userId); + await sut.setRequirePasswordOnStart(false); + + const keyHalfState = stateProvider.getUser( + userId, + ENCRYPTED_CLIENT_KEY_HALF, + ) as FakeSingleUserState; + expect(await firstValueFrom(keyHalfState.state$)).toBe(null); + expect(keyHalfState.nextMock).toHaveBeenCalledWith(null); + }); + + it("should not remove the encryptedClientKeyHalf if the value is true", async () => { + await sut.setEncryptedClientKeyHalf(encClientKeyHalf); + await sut.setRequirePasswordOnStart(true); + + expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf); + }); + }); + + describe("getRequirePasswordOnStart", () => { + it("should return the requirePasswordOnStart value", async () => { + stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START.key, true); + + expect(await sut.getRequirePasswordOnStart(userId)).toBe(true); + }); + }); }); diff --git a/libs/common/src/platform/biometrics/biometric-state.service.ts b/libs/common/src/platform/biometrics/biometric-state.service.ts index 011e5cdc59..0007a5f210 100644 --- a/libs/common/src/platform/biometrics/biometric-state.service.ts +++ b/libs/common/src/platform/biometrics/biometric-state.service.ts @@ -4,7 +4,7 @@ import { UserId } from "../../types/guid"; import { EncryptedString, EncString } from "../models/domain/enc-string"; import { ActiveUserState, StateProvider } from "../state"; -import { ENCRYPTED_CLIENT_KEY_HALF } from "./biometric.state"; +import { ENCRYPTED_CLIENT_KEY_HALF, REQUIRE_PASSWORD_ON_START } from "./biometric.state"; export abstract class BiometricStateService { /** @@ -21,27 +21,60 @@ export abstract class BiometricStateService { */ requirePasswordOnStart$: Observable; - abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString): Promise; + /** + * 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), ); - this.requirePasswordOnStart$ = this.encryptedClientKeyHalf$.pipe(map((keyHalf) => !!keyHalf)); } - async setEncryptedClientKeyHalf(encryptedKeyHalf: EncString): Promise { - await this.encryptedClientKeyHalfState.update(() => encryptedKeyHalf?.encryptedString ?? null); + 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 { @@ -49,10 +82,9 @@ export class DefaultBiometricStateService implements BiometricStateService { } async getRequirePasswordOnStart(userId: UserId): Promise { - if (userId == null) { - return false; - } - return !!(await this.getEncryptedClientKeyHalf(userId)); + return !!(await firstValueFrom( + this.stateProvider.getUser(userId, REQUIRE_PASSWORD_ON_START).state$, + )); } async getEncryptedClientKeyHalf(userId: UserId): Promise { diff --git a/libs/common/src/platform/biometrics/biometric.state.spec.ts b/libs/common/src/platform/biometrics/biometric.state.spec.ts index 09c8875814..e36b5f434f 100644 --- a/libs/common/src/platform/biometrics/biometric.state.spec.ts +++ b/libs/common/src/platform/biometrics/biometric.state.spec.ts @@ -1,4 +1,16 @@ -import { ENCRYPTED_CLIENT_KEY_HALF } from "./biometric.state"; +import { ENCRYPTED_CLIENT_KEY_HALF, REQUIRE_PASSWORD_ON_START } from "./biometric.state"; + +describe("require password on start", () => { + const sut = REQUIRE_PASSWORD_ON_START; + + it("should deserialize require password on start state", () => { + const requirePasswordOnStart = "requirePasswordOnStart"; + + const result = sut.deserializer(JSON.parse(JSON.stringify(requirePasswordOnStart))); + + expect(result).toEqual(requirePasswordOnStart); + }); +}); describe("encrypted client key half", () => { const sut = ENCRYPTED_CLIENT_KEY_HALF; diff --git a/libs/common/src/platform/biometrics/biometric.state.ts b/libs/common/src/platform/biometrics/biometric.state.ts index 90268d2110..2c231b9e76 100644 --- a/libs/common/src/platform/biometrics/biometric.state.ts +++ b/libs/common/src/platform/biometrics/biometric.state.ts @@ -1,6 +1,19 @@ import { EncryptedString } from "../models/domain/enc-string"; import { KeyDefinition, BIOMETRIC_SETTINGS_DISK } from "../state"; +/** + * Boolean indicating the user has elected to require a password to use their biometric key upon starting the application. + * + * A true setting controls whether {@link ENCRYPTED_CLIENT_KEY_HALF} is set. + */ +export const REQUIRE_PASSWORD_ON_START = new KeyDefinition( + BIOMETRIC_SETTINGS_DISK, + "requirePasswordOnStart", + { + deserializer: (value) => value, + }, +); + /** * 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.