mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 18:53:29 +00:00
[PM-14445] TS strict for Key Management Biometrics (#13039)
* PM-14445: TS strict for Key Management Biometrics * formatting * callbacks not null expectations * state nullability expectations updates * unit tests fix * secure channel naming, explicit null check on messageId * revert null for getUser, getGlobal in state.provider.ts * revert null for getUser, getGlobal in state.provider.ts
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { makeEncString, trackEmissions } from "../../../common/spec";
|
||||
import {
|
||||
makeEncString,
|
||||
trackEmissions,
|
||||
FakeStateProvider,
|
||||
FakeGlobalState,
|
||||
FakeSingleUserState,
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../common/spec/fake-account-service";
|
||||
import { FakeGlobalState, FakeSingleUserState } from "../../../common/spec/fake-state";
|
||||
import { FakeStateProvider } from "../../../common/spec/fake-state-provider";
|
||||
} from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service";
|
||||
import {
|
||||
@@ -51,7 +52,7 @@ describe("BiometricStateService", () => {
|
||||
|
||||
it("emits false when the require password on start state is undefined", async () => {
|
||||
const state = stateProvider.activeUser.getFake(REQUIRE_PASSWORD_ON_START);
|
||||
state.nextState(undefined);
|
||||
state.nextState(undefined as unknown as boolean);
|
||||
|
||||
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(false);
|
||||
});
|
||||
@@ -60,14 +61,14 @@ describe("BiometricStateService", () => {
|
||||
describe("encryptedClientKeyHalf$", () => {
|
||||
it("emits when the encryptedClientKeyHalf state changes", async () => {
|
||||
const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF);
|
||||
state.nextState(encryptedClientKeyHalf);
|
||||
state.nextState(encryptedClientKeyHalf as unknown as EncryptedString);
|
||||
|
||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf);
|
||||
});
|
||||
|
||||
it("emits false when the encryptedClientKeyHalf state is undefined", async () => {
|
||||
const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF);
|
||||
state.nextState(undefined);
|
||||
state.nextState(undefined as unknown as EncryptedString);
|
||||
|
||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toBe(null);
|
||||
});
|
||||
@@ -76,7 +77,7 @@ describe("BiometricStateService", () => {
|
||||
describe("fingerprintValidated$", () => {
|
||||
it("emits when the fingerprint validated state changes", async () => {
|
||||
const state = stateProvider.global.getFake(FINGERPRINT_VALIDATED);
|
||||
state.stateSubject.next(undefined);
|
||||
state.stateSubject.next(undefined as unknown as boolean);
|
||||
|
||||
expect(await firstValueFrom(sut.fingerprintValidated$)).toBe(false);
|
||||
|
||||
@@ -172,7 +173,7 @@ describe("BiometricStateService", () => {
|
||||
});
|
||||
|
||||
it("throws when called with no active user", async () => {
|
||||
await accountService.switchAccount(null);
|
||||
await accountService.switchAccount(null as unknown as UserId);
|
||||
await expect(sut.setUserPromptCancelled()).rejects.toThrow(
|
||||
"Cannot update biometric prompt cancelled state without an active user",
|
||||
);
|
||||
@@ -261,7 +262,7 @@ describe("BiometricStateService", () => {
|
||||
|
||||
it("emits false when biometricUnlockEnabled state is undefined", async () => {
|
||||
const state = stateProvider.activeUser.getFake(BIOMETRIC_UNLOCK_ENABLED);
|
||||
state.nextState(undefined);
|
||||
state.nextState(undefined as unknown as boolean);
|
||||
|
||||
expect(await firstValueFrom(sut.biometricUnlockEnabled$)).toBe(false);
|
||||
});
|
||||
@@ -291,7 +292,9 @@ describe("BiometricStateService", () => {
|
||||
});
|
||||
|
||||
it("returns false when the state is not set", async () => {
|
||||
stateProvider.singleUser.getFake(userId, BIOMETRIC_UNLOCK_ENABLED).nextState(undefined);
|
||||
stateProvider.singleUser
|
||||
.getFake(userId, BIOMETRIC_UNLOCK_ENABLED)
|
||||
.nextState(undefined as unknown as boolean);
|
||||
|
||||
expect(await sut.getBiometricUnlockEnabled(userId)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable, firstValueFrom, map, combineLatest } from "rxjs";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ActiveUserState, GlobalState, StateProvider } from "../../../common/src/platform/state";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { UserId } from "../../../common/src/types/guid";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { ActiveUserState, GlobalState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
BIOMETRIC_UNLOCK_ENABLED,
|
||||
@@ -34,7 +26,7 @@ export abstract class BiometricStateService {
|
||||
*
|
||||
* Tracks the currently active user
|
||||
*/
|
||||
abstract encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
||||
abstract encryptedClientKeyHalf$: Observable<EncString | null>;
|
||||
/**
|
||||
* whether or not a password is required on first unlock after opening the application
|
||||
*
|
||||
@@ -71,42 +63,54 @@ export abstract class BiometricStateService {
|
||||
* @param value whether or not a password is required on first unlock after opening the application
|
||||
*/
|
||||
abstract setRequirePasswordOnStart(value: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates the biometric unlock enabled state for the currently active user.
|
||||
* @param enabled whether or not to store a biometric key to unlock the vault
|
||||
*/
|
||||
abstract setBiometricUnlockEnabled(enabled: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets the biometric unlock enabled state for the given user.
|
||||
* @param userId user Id to check
|
||||
*/
|
||||
abstract getBiometricUnlockEnabled(userId: UserId): Promise<boolean>;
|
||||
|
||||
abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString, userId?: UserId): Promise<void>;
|
||||
abstract getEncryptedClientKeyHalf(userId: UserId): Promise<EncString>;
|
||||
|
||||
abstract getEncryptedClientKeyHalf(userId: UserId): Promise<EncString | null>;
|
||||
|
||||
abstract getRequirePasswordOnStart(userId: UserId): Promise<boolean>;
|
||||
|
||||
abstract removeEncryptedClientKeyHalf(userId: UserId): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates the active user's state to reflect that they've been warned about requiring password on start.
|
||||
*/
|
||||
abstract setDismissedRequirePasswordOnStartCallout(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates the active user's state to reflect that they've cancelled the biometric prompt.
|
||||
*/
|
||||
abstract setUserPromptCancelled(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Resets the given user's state to reflect that they haven't cancelled the biometric prompt.
|
||||
* @param userId the user to reset the prompt cancelled state for. If not provided, the currently active user will be used.
|
||||
*/
|
||||
abstract resetUserPromptCancelled(userId?: UserId): Promise<void>;
|
||||
|
||||
/**
|
||||
* Resets all user's state to reflect that they haven't cancelled the biometric prompt.
|
||||
*/
|
||||
abstract resetAllPromptCancelled(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 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<void>;
|
||||
|
||||
/**
|
||||
* Updates whether or not IPC has been validated by the user this session
|
||||
* @param validated the value to save
|
||||
@@ -115,7 +119,7 @@ export abstract class BiometricStateService {
|
||||
|
||||
abstract updateLastProcessReload(): Promise<void>;
|
||||
|
||||
abstract getLastProcessReload(): Promise<Date>;
|
||||
abstract getLastProcessReload(): Promise<Date | null>;
|
||||
|
||||
abstract logout(userId: UserId): Promise<void>;
|
||||
}
|
||||
@@ -123,20 +127,20 @@ export abstract class BiometricStateService {
|
||||
export class DefaultBiometricStateService implements BiometricStateService {
|
||||
private biometricUnlockEnabledState: ActiveUserState<boolean>;
|
||||
private requirePasswordOnStartState: ActiveUserState<boolean>;
|
||||
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString | undefined>;
|
||||
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString>;
|
||||
private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>;
|
||||
private promptCancelledState: GlobalState<Record<UserId, boolean>>;
|
||||
private promptAutomaticallyState: ActiveUserState<boolean>;
|
||||
private fingerprintValidatedState: GlobalState<boolean>;
|
||||
private lastProcessReloadState: GlobalState<Date>;
|
||||
biometricUnlockEnabled$: Observable<boolean>;
|
||||
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
||||
encryptedClientKeyHalf$: Observable<EncString | null>;
|
||||
requirePasswordOnStart$: Observable<boolean>;
|
||||
dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
|
||||
promptCancelled$: Observable<boolean>;
|
||||
promptAutomatically$: Observable<boolean>;
|
||||
fingerprintValidated$: Observable<boolean>;
|
||||
lastProcessReload$: Observable<Date>;
|
||||
lastProcessReload$: Observable<Date | null>;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED);
|
||||
@@ -164,7 +168,7 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
this.promptCancelledState.state$,
|
||||
]).pipe(
|
||||
map(([userId, record]) => {
|
||||
return record?.[userId] ?? false;
|
||||
return userId != null ? (record?.[userId] ?? false) : false;
|
||||
}),
|
||||
);
|
||||
this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY);
|
||||
@@ -188,7 +192,7 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
}
|
||||
|
||||
async setRequirePasswordOnStart(value: boolean): Promise<void> {
|
||||
let currentActiveId: UserId;
|
||||
let currentActiveId: UserId | undefined = undefined;
|
||||
await this.requirePasswordOnStartState.update(
|
||||
(_, [userId]) => {
|
||||
currentActiveId = userId;
|
||||
@@ -198,7 +202,7 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
combineLatestWith: this.requirePasswordOnStartState.combinedState$,
|
||||
},
|
||||
);
|
||||
if (!value) {
|
||||
if (!value && currentActiveId) {
|
||||
await this.removeEncryptedClientKeyHalf(currentActiveId);
|
||||
}
|
||||
}
|
||||
@@ -222,7 +226,7 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
));
|
||||
}
|
||||
|
||||
async getEncryptedClientKeyHalf(userId: UserId): Promise<EncString> {
|
||||
async getEncryptedClientKeyHalf(userId: UserId): Promise<EncString | null> {
|
||||
return await firstValueFrom(
|
||||
this.stateProvider
|
||||
.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF)
|
||||
@@ -244,7 +248,9 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
async resetUserPromptCancelled(userId: UserId): Promise<void> {
|
||||
await this.stateProvider.getGlobal(PROMPT_CANCELLED).update(
|
||||
(data, activeUserId) => {
|
||||
delete data[userId ?? activeUserId];
|
||||
if (data != null) {
|
||||
delete data[userId ?? activeUserId];
|
||||
}
|
||||
return data;
|
||||
},
|
||||
{
|
||||
@@ -257,8 +263,10 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
async setUserPromptCancelled(): Promise<void> {
|
||||
await this.promptCancelledState.update(
|
||||
(record, userId) => {
|
||||
record ??= {};
|
||||
record[userId] = true;
|
||||
if (userId != null) {
|
||||
record ??= {};
|
||||
record[userId] = true;
|
||||
}
|
||||
return record;
|
||||
},
|
||||
{
|
||||
@@ -291,13 +299,13 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
await this.lastProcessReloadState.update(() => new Date());
|
||||
}
|
||||
|
||||
async getLastProcessReload(): Promise<Date> {
|
||||
async getLastProcessReload(): Promise<Date | null> {
|
||||
return await firstValueFrom(this.lastProcessReload$);
|
||||
}
|
||||
}
|
||||
|
||||
function encryptedClientKeyHalfToEncString(
|
||||
encryptedKeyHalf: EncryptedString | undefined,
|
||||
): EncString {
|
||||
encryptedKeyHalf: EncryptedString | null | undefined,
|
||||
): EncString | null {
|
||||
return encryptedKeyHalf == null ? null : new EncString(encryptedKeyHalf);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { KeyDefinition, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
|
||||
import {
|
||||
@@ -21,12 +20,7 @@ describe.each([
|
||||
[FINGERPRINT_VALIDATED, true],
|
||||
])(
|
||||
"deserializes state %s",
|
||||
(
|
||||
...args:
|
||||
| [UserKeyDefinition<EncryptedString>, EncryptedString]
|
||||
| [UserKeyDefinition<boolean>, boolean]
|
||||
| [KeyDefinition<boolean>, boolean]
|
||||
) => {
|
||||
(...args: [UserKeyDefinition<unknown> | KeyDefinition<unknown>, unknown]) => {
|
||||
function testDeserialization<T>(
|
||||
keyDefinition: UserKeyDefinition<T> | KeyDefinition<T>,
|
||||
state: T,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EncryptedString } from "../../../common/src/platform/models/domain/enc-string";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import {
|
||||
KeyDefinition,
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
UserKeyDefinition,
|
||||
} from "../../../common/src/platform/state";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { UserId } from "../../../common/src/types/guid";
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
/**
|
||||
* Indicates whether the user elected to store a biometric key to unlock their vault.
|
||||
|
||||
Reference in New Issue
Block a user