1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-28 14:13:22 +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:
Matt Gibson
2024-02-07 10:39:54 -05:00
committed by GitHub
parent 0eb9e760aa
commit 2ca34b46db
10 changed files with 161 additions and 121 deletions

View File

@@ -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<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
@@ -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,
},

View File

@@ -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<void>;
/**
* Removes the biometric client key half for the currently active user.
*/
abstract removeBiometricClientKeyHalf(): Promise<void>;
}
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<void> {
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<void> {
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<void> {
// 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<CsprngString | null> {
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<CsprngString | null> {
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--