mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-5537] Biometric State Service (#7761)
* Create state for biometric client key halves * Move enc string util to central utils * Provide biometric state through service * Use biometric state to track client key half * Create migration for client key half * Ensure client key half is removed on logout * Remove account data for client key half * Remove unnecessary key definition likes * Remove moved state from account * Fix null-conditional operator failure * Simplify migration * Remove lame test * Fix test type * Add migrator * Prefer userKey when legacy not needed * Fix tests
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { makeEncString } from "../../../spec";
|
||||
import { mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service";
|
||||
import { ENCRYPTED_CLIENT_KEY_HALF } from "./biometric.state";
|
||||
|
||||
describe("BiometricStateService", () => {
|
||||
let sut: BiometricStateService;
|
||||
const userId = "userId" as UserId;
|
||||
const encClientKeyHalf = makeEncString();
|
||||
const encryptedClientKeyHalf = encClientKeyHalf.encryptedString;
|
||||
const accountService = mockAccountServiceWith(userId);
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
sut = new DefaultBiometricStateService(stateProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
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 be true when encryptedClientKeyHalf is defined", async () => {
|
||||
stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF).nextState(encryptedClientKeyHalf);
|
||||
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptedClientKeyHalf$", () => {
|
||||
it("should track the encryptedClientKeyHalf state", async () => {
|
||||
const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF);
|
||||
state.nextState(undefined);
|
||||
|
||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toBe(null);
|
||||
|
||||
state.nextState(encryptedClientKeyHalf);
|
||||
|
||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setEncryptedClientKeyHalf", () => {
|
||||
it("should update the encryptedClientKeyHalf$", async () => {
|
||||
await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
|
||||
|
||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
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 } 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<EncString | undefined>;
|
||||
/**
|
||||
* whether or not a password is required on first unlock after opening the application
|
||||
*
|
||||
* tracks the currently active user
|
||||
*/
|
||||
requirePasswordOnStart$: Observable<boolean>;
|
||||
|
||||
abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString): Promise<void>;
|
||||
abstract getEncryptedClientKeyHalf(userId: UserId): Promise<EncString>;
|
||||
abstract getRequirePasswordOnStart(userId: UserId): Promise<boolean>;
|
||||
abstract removeEncryptedClientKeyHalf(userId: UserId): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultBiometricStateService implements BiometricStateService {
|
||||
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString | undefined>;
|
||||
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
||||
requirePasswordOnStart$: Observable<boolean>;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
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<void> {
|
||||
await this.encryptedClientKeyHalfState.update(() => encryptedKeyHalf?.encryptedString ?? null);
|
||||
}
|
||||
|
||||
async removeEncryptedClientKeyHalf(userId: UserId): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF).update(() => null);
|
||||
}
|
||||
|
||||
async getRequirePasswordOnStart(userId: UserId): Promise<boolean> {
|
||||
if (userId == null) {
|
||||
return false;
|
||||
}
|
||||
return !!(await this.getEncryptedClientKeyHalf(userId));
|
||||
}
|
||||
|
||||
async getEncryptedClientKeyHalf(userId: UserId): Promise<EncString> {
|
||||
return await firstValueFrom(
|
||||
this.stateProvider
|
||||
.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF)
|
||||
.state$.pipe(map(encryptedClientKeyHalfToEncString)),
|
||||
);
|
||||
}
|
||||
|
||||
async logout(userId: UserId): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF).update(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
function encryptedClientKeyHalfToEncString(
|
||||
encryptedKeyHalf: EncryptedString | undefined,
|
||||
): EncString {
|
||||
return encryptedKeyHalf == null ? null : new EncString(encryptedKeyHalf);
|
||||
}
|
||||
13
libs/common/src/platform/biometrics/biometric.state.spec.ts
Normal file
13
libs/common/src/platform/biometrics/biometric.state.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ENCRYPTED_CLIENT_KEY_HALF } from "./biometric.state";
|
||||
|
||||
describe("encrypted client key half", () => {
|
||||
const sut = ENCRYPTED_CLIENT_KEY_HALF;
|
||||
|
||||
it("should deserialize encrypted client key half state", () => {
|
||||
const encryptedClientKeyHalf = "encryptedClientKeyHalf";
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(encryptedClientKeyHalf)));
|
||||
|
||||
expect(result).toEqual(encryptedClientKeyHalf);
|
||||
});
|
||||
});
|
||||
17
libs/common/src/platform/biometrics/biometric.state.ts
Normal file
17
libs/common/src/platform/biometrics/biometric.state.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { EncryptedString } from "../models/domain/enc-string";
|
||||
import { KeyDefinition, BIOMETRIC_SETTINGS_DISK } from "../state";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* For operating systems without application-level key storage, this key half is concatenated with a signature
|
||||
* provided by the OS and used to encrypt the biometric key prior to storage.
|
||||
*/
|
||||
export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition<EncryptedString>(
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
"clientKeyHalf",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
},
|
||||
);
|
||||
@@ -1,22 +1,12 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { ProviderEncryptedOrganizationKey } from "../../../admin-console/models/domain/encrypted-organization-key";
|
||||
import { makeEncString, makeStaticByteArray } from "../../../../spec";
|
||||
import { OrgKey } from "../../../types/key";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { EncString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ORGANIZATION_KEYS } from "./org-keys.state";
|
||||
|
||||
function makeEncString(data?: string) {
|
||||
data ??= Utils.newGuid();
|
||||
return new EncString(EncryptionType.AesCbc256_HmacSha256_B64, data, "test", "test");
|
||||
}
|
||||
ProviderEncryptedOrganizationKey;
|
||||
|
||||
describe("encrypted org keys", () => {
|
||||
const sut = USER_ENCRYPTED_ORGANIZATION_KEYS;
|
||||
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { makeEncString, makeStaticByteArray } from "../../../../spec";
|
||||
import { ProviderId } from "../../../types/guid";
|
||||
import { ProviderKey, UserPrivateKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
||||
import { EncryptedString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../crypto.service";
|
||||
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./provider-keys.state";
|
||||
|
||||
function makeEncString(data?: string) {
|
||||
data ??= Utils.newGuid();
|
||||
return new EncString(EncryptionType.AesCbc256_HmacSha256_B64, data, "test", "test");
|
||||
}
|
||||
|
||||
describe("encrypted provider keys", () => {
|
||||
const sut = USER_ENCRYPTED_PROVIDER_KEYS;
|
||||
|
||||
|
||||
@@ -22,14 +22,16 @@ export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
|
||||
|
||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||
|
||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||
|
||||
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
||||
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
||||
|
||||
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
|
||||
|
||||
// Admin Console
|
||||
export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk");
|
||||
export const POLICIES_DISK = new StateDefinition("policies", "disk");
|
||||
export const POLICIES_MEMORY = new StateDefinition("policies", "memory");
|
||||
export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user