mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-5537] Persist require password on startup through logout (#7825)
* Persist require password on startup through logout * Test new methods
This commit is contained in:
@@ -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<EncryptedString>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<boolean>;
|
||||
|
||||
abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString): Promise<void>;
|
||||
/**
|
||||
* 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<void>;
|
||||
abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString, userId?: UserId): 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 requirePasswordOnStartState: ActiveUserState<boolean>;
|
||||
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString | undefined>;
|
||||
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
||||
requirePasswordOnStart$: Observable<boolean>;
|
||||
|
||||
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<void> {
|
||||
await this.encryptedClientKeyHalfState.update(() => encryptedKeyHalf?.encryptedString ?? null);
|
||||
async setRequirePasswordOnStart(value: boolean): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
@@ -49,10 +82,9 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
}
|
||||
|
||||
async getRequirePasswordOnStart(userId: UserId): Promise<boolean> {
|
||||
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<EncString> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<boolean>(
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user