mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +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:
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ElectronCryptoService>(),
|
||||
useValue: mock<CryptoService>(),
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutService,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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--
|
||||
|
||||
Reference in New Issue
Block a user