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:
@@ -9,7 +9,10 @@ module.exports = {
|
||||
...sharedConfig,
|
||||
preset: "jest-preset-angular",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
moduleNameMapper: pathsToModuleNameMapper(
|
||||
{ "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||
{
|
||||
prefix: "<rootDir>/",
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
@@ -12,16 +12,17 @@ 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";
|
||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
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",
|
||||
@@ -112,12 +113,13 @@ export class SettingsComponent implements OnInit {
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private stateService: ElectronStateService,
|
||||
private messagingService: MessagingService,
|
||||
private cryptoService: CryptoService,
|
||||
private cryptoService: ElectronCryptoService,
|
||||
private modalService: ModalService,
|
||||
private themingService: AbstractThemingService,
|
||||
private settingsService: SettingsService,
|
||||
private dialogService: DialogService,
|
||||
private userVerificationService: UserVerificationServiceAbstraction,
|
||||
private biometricStateService: BiometricStateService,
|
||||
) {
|
||||
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
|
||||
|
||||
@@ -242,8 +244,9 @@ export class SettingsComponent implements OnInit {
|
||||
pin: this.userHasPinSet,
|
||||
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||
autoPromptBiometrics: !(await this.stateService.getDisableAutoBiometricsPrompt()),
|
||||
requirePasswordOnStart:
|
||||
(await this.stateService.getBiometricRequirePasswordOnStart()) ?? false,
|
||||
requirePasswordOnStart: await firstValueFrom(
|
||||
this.biometricStateService.requirePasswordOnStart$,
|
||||
),
|
||||
approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false,
|
||||
clearClipboard: await this.stateService.getClearClipboard(),
|
||||
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
|
||||
@@ -454,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.stateService.setBiometricRequirePasswordOnStart(true);
|
||||
await this.cryptoService.setBiometricClientKeyHalf();
|
||||
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
||||
}
|
||||
await this.cryptoService.refreshAdditionalKeys();
|
||||
@@ -488,10 +491,9 @@ export class SettingsComponent implements OnInit {
|
||||
this.form.controls.autoPromptBiometrics.setValue(false);
|
||||
await this.updateAutoPromptBiometrics();
|
||||
|
||||
await this.stateService.setBiometricRequirePasswordOnStart(true);
|
||||
await this.cryptoService.setBiometricClientKeyHalf();
|
||||
} else {
|
||||
await this.stateService.setBiometricRequirePasswordOnStart(false);
|
||||
await this.stateService.setBiometricEncryptionClientKeyHalf(null);
|
||||
await this.cryptoService.removeBiometricClientKeyHalf();
|
||||
}
|
||||
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
||||
await this.cryptoService.refreshAdditionalKeys();
|
||||
|
||||
@@ -34,6 +34,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
@@ -47,7 +48,10 @@ import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { LoginGuard } from "../../auth/guards/login.guard";
|
||||
import { Account } from "../../models/account";
|
||||
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
|
||||
import {
|
||||
DefaultElectronCryptoService,
|
||||
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";
|
||||
@@ -178,7 +182,11 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
||||
},
|
||||
{
|
||||
provide: CryptoServiceAbstraction,
|
||||
useClass: ElectronCryptoService,
|
||||
useExisting: ElectronCryptoService,
|
||||
},
|
||||
{
|
||||
provide: ElectronCryptoService,
|
||||
useClass: DefaultElectronCryptoService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
EncryptService,
|
||||
@@ -187,6 +195,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
||||
StateServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
BiometricStateService,
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -21,9 +21,11 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
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";
|
||||
|
||||
import { LockComponent } from "./lock.component";
|
||||
@@ -78,7 +80,11 @@ describe("LockComponent", () => {
|
||||
},
|
||||
{
|
||||
provide: CryptoService,
|
||||
useValue: mock<CryptoService>(),
|
||||
useExisting: ElectronCryptoService,
|
||||
},
|
||||
{
|
||||
provide: ElectronCryptoService,
|
||||
useValue: mock<ElectronCryptoService>(),
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutService,
|
||||
@@ -140,6 +146,10 @@ describe("LockComponent", () => {
|
||||
provide: PinCryptoServiceAbstraction,
|
||||
useValue: mock<PinCryptoServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: BiometricStateService,
|
||||
useValue: mock<BiometricStateService>(),
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -13,7 +13,6 @@ 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";
|
||||
@@ -22,6 +21,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
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 +41,7 @@ export class LockComponent extends BaseLockComponent {
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
cryptoService: CryptoService,
|
||||
protected override cryptoService: ElectronCryptoService,
|
||||
vaultTimeoutService: VaultTimeoutService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
environmentService: EnvironmentService,
|
||||
@@ -175,8 +175,8 @@ export class LockComponent extends BaseLockComponent {
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
await this.stateService.setBiometricRequirePasswordOnStart(response);
|
||||
if (response) {
|
||||
await this.cryptoService.setBiometricClientKeyHalf();
|
||||
await this.stateService.setDisableAutoBiometricsPrompt(true);
|
||||
}
|
||||
this.supportsBiometric = await this.canUseBiometric();
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as path from "path";
|
||||
import { app } from "electron";
|
||||
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||
@@ -159,6 +160,8 @@ export class Main {
|
||||
this.updaterMain,
|
||||
);
|
||||
|
||||
const biometricStateService = new DefaultBiometricStateService(stateProvider);
|
||||
|
||||
this.biometricsService = new BiometricsService(
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
@@ -166,6 +169,7 @@ export class Main {
|
||||
this.logService,
|
||||
this.messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
);
|
||||
|
||||
this.desktopCredentialStorageListener = new DesktopCredentialStorageListener(
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
Account as BaseAccount,
|
||||
AccountSettings as BaseAccountSettings,
|
||||
AccountKeys as BaseAccountKeys,
|
||||
} from "@bitwarden/common/platform/models/domain/account";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
|
||||
export class AccountSettings extends BaseAccountSettings {
|
||||
vaultTimeout = -1; // On Restart
|
||||
requirePasswordOnStart?: boolean;
|
||||
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||
}
|
||||
|
||||
export class AccountKeys extends BaseAccountKeys {
|
||||
biometricEncryptionClientKeyHalf?: Jsonify<EncString>;
|
||||
}
|
||||
|
||||
export class Account extends BaseAccount {
|
||||
settings?: AccountSettings = new AccountSettings();
|
||||
keys?: AccountKeys = new AccountKeys();
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
super(init);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { WindowMain } from "../../../main/window.main";
|
||||
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
|
||||
@@ -25,8 +27,10 @@ describe("biometrics tests", function () {
|
||||
const stateService = mock<ElectronStateService>();
|
||||
const logService = mock<LogService>();
|
||||
const messagingService = mock<MessagingService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
|
||||
it("Should call the platformspecific methods", async () => {
|
||||
const userId = "userId-1" as UserId;
|
||||
const sut = new BiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
@@ -34,6 +38,7 @@ describe("biometrics tests", function () {
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
);
|
||||
|
||||
const mockService = mock<OsBiometricService>();
|
||||
@@ -46,7 +51,7 @@ describe("biometrics tests", function () {
|
||||
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
|
||||
expect(mockService.init).toBeCalled();
|
||||
|
||||
await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
|
||||
await sut.canAuthBiometric({ service: "test", key: "test", userId });
|
||||
expect(mockService.osSupportsBiometric).toBeCalled();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
@@ -64,6 +69,7 @@ describe("biometrics tests", function () {
|
||||
logService,
|
||||
messagingService,
|
||||
"win32",
|
||||
biometricStateService,
|
||||
);
|
||||
|
||||
const internalService = (sut as any).platformSpecificService;
|
||||
@@ -79,6 +85,7 @@ describe("biometrics tests", function () {
|
||||
logService,
|
||||
messagingService,
|
||||
"darwin",
|
||||
biometricStateService,
|
||||
);
|
||||
const internalService = (sut as any).platformSpecificService;
|
||||
expect(internalService).not.toBeNull();
|
||||
@@ -89,6 +96,7 @@ describe("biometrics tests", function () {
|
||||
describe("can auth biometric", () => {
|
||||
let sut: BiometricsService;
|
||||
let innerService: MockProxy<OsBiometricService>;
|
||||
const userId = "userId-1" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new BiometricsService(
|
||||
@@ -98,6 +106,7 @@ describe("biometrics tests", function () {
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
);
|
||||
|
||||
innerService = mock();
|
||||
@@ -108,9 +117,9 @@ describe("biometrics tests", function () {
|
||||
});
|
||||
|
||||
it("should return false if client key half is required and not provided", async () => {
|
||||
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(true);
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
||||
|
||||
const result = await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
|
||||
const result = await sut.canAuthBiometric({ service: "test", key: "test", userId });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
@@ -121,18 +130,18 @@ describe("biometrics tests", function () {
|
||||
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
|
||||
expect(innerService.init).toBeCalled();
|
||||
|
||||
await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
|
||||
await sut.canAuthBiometric({ service: "test", key: "test", userId });
|
||||
expect(innerService.osSupportsBiometric).toBeCalled();
|
||||
});
|
||||
|
||||
it("should call osSupportBiometric if client key half is not required", async () => {
|
||||
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(false);
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(false);
|
||||
innerService.osSupportsBiometric.mockResolvedValue(true);
|
||||
|
||||
const result = await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
|
||||
const result = await sut.canAuthBiometric({ service: "test", key: "test", userId });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(innerService.osSupportsBiometric).toBeCalled();
|
||||
expect(innerService.osSupportsBiometric).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { WindowMain } from "../../../main/window.main";
|
||||
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
|
||||
@@ -18,6 +20,7 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
||||
private logService: LogService,
|
||||
private messagingService: MessagingService,
|
||||
private platform: NodeJS.Platform,
|
||||
private biometricStateService: BiometricStateService,
|
||||
) {
|
||||
this.loadPlatformSpecificService(this.platform);
|
||||
}
|
||||
@@ -70,11 +73,9 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
||||
}: {
|
||||
service: string;
|
||||
key: string;
|
||||
userId: string;
|
||||
userId: UserId;
|
||||
}): Promise<boolean> {
|
||||
const requireClientKeyHalf = await this.stateService.getBiometricRequirePasswordOnStart({
|
||||
userId,
|
||||
});
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.getClientKeyHalf(service, key);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
return clientKeyHalfSatisfied && (await this.osSupportsBiometric());
|
||||
@@ -171,7 +172,13 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
||||
}
|
||||
|
||||
private async enforceClientKeyHalf(service: string, storageKey: string): Promise<void> {
|
||||
const requireClientKeyHalf = await this.stateService.getBiometricRequirePasswordOnStart();
|
||||
// The first half of the storageKey is the userId, separated by `_`
|
||||
// We need to extract from the service because the active user isn't properly synced to the main process,
|
||||
// So we can't use the observables on `biometricStateService`
|
||||
const [userId] = storageKey.split("_");
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(
|
||||
userId as UserId,
|
||||
);
|
||||
const clientKeyHalfB64 = this.getClientKeyHalf(service, storageKey);
|
||||
|
||||
if (requireClientKeyHalf && !clientKeyHalfB64) {
|
||||
|
||||
@@ -5,7 +5,10 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
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 { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -15,11 +18,11 @@ import {
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../../libs/common/spec/fake-account-service";
|
||||
|
||||
import { ElectronCryptoService } from "./electron-crypto.service";
|
||||
import { DefaultElectronCryptoService } from "./electron-crypto.service";
|
||||
import { ElectronStateService } from "./electron-state.service.abstraction";
|
||||
|
||||
describe("electronCryptoService", () => {
|
||||
let electronCryptoService: ElectronCryptoService;
|
||||
let sut: DefaultElectronCryptoService;
|
||||
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
@@ -28,6 +31,7 @@ describe("electronCryptoService", () => {
|
||||
const stateService = mock<ElectronStateService>();
|
||||
let accountService: FakeAccountService;
|
||||
let stateProvider: FakeStateProvider;
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
|
||||
const mockUserId = "mock user id" as UserId;
|
||||
|
||||
@@ -35,7 +39,7 @@ describe("electronCryptoService", () => {
|
||||
accountService = mockAccountServiceWith("userId" as UserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
electronCryptoService = new ElectronCryptoService(
|
||||
sut = new DefaultElectronCryptoService(
|
||||
cryptoFunctionService,
|
||||
encryptService,
|
||||
platformUtilService,
|
||||
@@ -43,6 +47,7 @@ describe("electronCryptoService", () => {
|
||||
stateService,
|
||||
accountService,
|
||||
stateProvider,
|
||||
biometricStateService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -50,8 +55,42 @@ describe("electronCryptoService", () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(electronCryptoService).not.toBeFalsy();
|
||||
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", () => {
|
||||
@@ -66,9 +105,9 @@ describe("electronCryptoService", () => {
|
||||
it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
|
||||
stateService.getBiometricUnlock.mockResolvedValue(true);
|
||||
platformUtilService.supportsSecureStorage.mockReturnValue(true);
|
||||
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(false);
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
||||
|
||||
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
|
||||
await sut.setUserKey(mockUserKey, mockUserId);
|
||||
|
||||
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: null }),
|
||||
@@ -82,7 +121,7 @@ describe("electronCryptoService", () => {
|
||||
stateService.getBiometricUnlock.mockResolvedValue(true);
|
||||
platformUtilService.supportsSecureStorage.mockReturnValue(false);
|
||||
|
||||
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
|
||||
await sut.setUserKey(mockUserKey, mockUserId);
|
||||
|
||||
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(null, {
|
||||
userId: mockUserId,
|
||||
@@ -90,7 +129,7 @@ describe("electronCryptoService", () => {
|
||||
});
|
||||
|
||||
it("clears the old deprecated Biometric key whenever a User Key is set", async () => {
|
||||
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
|
||||
await sut.setUserKey(mockUserKey, mockUserId);
|
||||
|
||||
expect(stateService.setCryptoMasterKeyBiometric).toHaveBeenCalledWith(null, {
|
||||
userId: mockUserId,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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";
|
||||
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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
@@ -15,7 +18,18 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { ElectronStateService } from "./electron-state.service.abstraction";
|
||||
|
||||
export class ElectronCryptoService extends CryptoService {
|
||||
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 {
|
||||
constructor(
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
encryptService: EncryptService,
|
||||
@@ -24,6 +38,7 @@ export class ElectronCryptoService extends CryptoService {
|
||||
protected override stateService: ElectronStateService,
|
||||
accountService: AccountService,
|
||||
stateProvider: StateProvider,
|
||||
private biometricStateService: BiometricStateService,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
@@ -47,17 +62,27 @@ export class ElectronCryptoService extends CryptoService {
|
||||
|
||||
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.stateService.setUserKeyBiometric(null, { userId: userId });
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
|
||||
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
||||
await this.biometricStateService.removeEncryptedClientKeyHalf(userId);
|
||||
await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
|
||||
return;
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
super.clearStoredUserKey(keySuffix, userId);
|
||||
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) {
|
||||
@@ -86,10 +111,8 @@ export class ElectronCryptoService extends CryptoService {
|
||||
}
|
||||
|
||||
protected async storeBiometricKey(key: UserKey, userId?: UserId): Promise<void> {
|
||||
let clientEncKeyHalf: CsprngString = null;
|
||||
if (await this.stateService.getBiometricRequirePasswordOnStart({ userId })) {
|
||||
clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
|
||||
}
|
||||
// May resolve to null, in which case no client key have is required
|
||||
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
|
||||
await this.stateService.setUserKeyBiometric(
|
||||
{ key: key.keyB64, clientEncKeyHalf },
|
||||
{ userId: userId },
|
||||
@@ -105,30 +128,21 @@ export class ElectronCryptoService extends CryptoService {
|
||||
}
|
||||
|
||||
protected override async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
super.clearAllStoredUserKeys(userId);
|
||||
await this.clearStoredUserKey(KeySuffixOptions.Biometric, userId);
|
||||
await super.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
|
||||
private async getBiometricEncryptionClientKeyHalf(userId?: UserId): Promise<CsprngString | null> {
|
||||
try {
|
||||
let biometricKey = await this.stateService
|
||||
.getBiometricEncryptionClientKeyHalf({ userId })
|
||||
.then((result) => result?.decrypt(null /* user encrypted */))
|
||||
.then((result) => result as CsprngString);
|
||||
const userKey = await this.getUserKeyWithLegacySupport();
|
||||
if (biometricKey == null && userKey != null) {
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
|
||||
const encKey = await this.encryptService.encrypt(biometricKey, userKey);
|
||||
await this.stateService.setBiometricEncryptionClientKeyHalf(encKey);
|
||||
}
|
||||
|
||||
return biometricKey;
|
||||
} catch {
|
||||
const encryptedKeyHalfPromise =
|
||||
userId == null
|
||||
? firstValueFrom(this.biometricStateService.encryptedClientKeyHalf$)
|
||||
: this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
const encryptedKeyHalf = await encryptedKeyHalfPromise;
|
||||
if (encryptedKeyHalf == null) {
|
||||
return null;
|
||||
}
|
||||
const userKey = await this.getUserKey();
|
||||
return (await this.encryptService.decryptToUtf8(encryptedKeyHalf, userKey)) as CsprngString;
|
||||
}
|
||||
|
||||
// --LEGACY METHODS--
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
|
||||
import { Account } from "../../models/account";
|
||||
|
||||
export abstract class ElectronStateService extends StateService<Account> {
|
||||
getBiometricEncryptionClientKeyHalf: (options?: StorageOptions) => Promise<EncString>;
|
||||
setBiometricEncryptionClientKeyHalf: (
|
||||
value: EncString,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<void>;
|
||||
getBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricRequirePasswordOnStart: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
@@ -24,49 +23,6 @@ export class ElectronStateService
|
||||
await super.addAccount(account);
|
||||
}
|
||||
|
||||
async getBiometricEncryptionClientKeyHalf(options?: StorageOptions): Promise<EncString> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
const key = account?.keys?.biometricEncryptionClientKeyHalf;
|
||||
return key == null ? null : new EncString(key);
|
||||
}
|
||||
|
||||
async setBiometricEncryptionClientKeyHalf(
|
||||
value: EncString,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.keys.biometricEncryptionClientKeyHalf = value?.encryptedString;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getBiometricRequirePasswordOnStart(options?: StorageOptions): Promise<boolean> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
return account?.settings?.requirePasswordOnStart;
|
||||
}
|
||||
|
||||
async setBiometricRequirePasswordOnStart(
|
||||
value: boolean,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.settings.requirePasswordOnStart = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getDismissedBiometricRequirePasswordOnStart(options?: StorageOptions): Promise<boolean> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
|
||||
Reference in New Issue
Block a user