mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-28 04:08:47 +02: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:
parent
99f18c9666
commit
414ee2563f
@ -9,7 +9,10 @@ module.exports = {
|
|||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
preset: "jest-preset-angular",
|
preset: "jest-preset-angular",
|
||||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(
|
||||||
prefix: "<rootDir>/",
|
{ "@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 { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
||||||
import { flagEnabled } from "../../platform/flags";
|
import { flagEnabled } from "../../platform/flags";
|
||||||
|
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
|
||||||
import { ElectronStateService } from "../../platform/services/electron-state.service.abstraction";
|
import { ElectronStateService } from "../../platform/services/electron-state.service.abstraction";
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-settings",
|
selector: "app-settings",
|
||||||
@ -112,12 +113,13 @@ export class SettingsComponent implements OnInit {
|
|||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
private stateService: ElectronStateService,
|
private stateService: ElectronStateService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: ElectronCryptoService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private userVerificationService: UserVerificationServiceAbstraction,
|
private userVerificationService: UserVerificationServiceAbstraction,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
|
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
|
||||||
|
|
||||||
@ -242,8 +244,9 @@ export class SettingsComponent implements OnInit {
|
|||||||
pin: this.userHasPinSet,
|
pin: this.userHasPinSet,
|
||||||
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||||
autoPromptBiometrics: !(await this.stateService.getDisableAutoBiometricsPrompt()),
|
autoPromptBiometrics: !(await this.stateService.getDisableAutoBiometricsPrompt()),
|
||||||
requirePasswordOnStart:
|
requirePasswordOnStart: await firstValueFrom(
|
||||||
(await this.stateService.getBiometricRequirePasswordOnStart()) ?? false,
|
this.biometricStateService.requirePasswordOnStart$,
|
||||||
|
),
|
||||||
approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false,
|
approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false,
|
||||||
clearClipboard: await this.stateService.getClearClipboard(),
|
clearClipboard: await this.stateService.getClearClipboard(),
|
||||||
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
|
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
|
||||||
@ -454,7 +457,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
this.form.controls.requirePasswordOnStart.setValue(true);
|
this.form.controls.requirePasswordOnStart.setValue(true);
|
||||||
this.form.controls.autoPromptBiometrics.setValue(false);
|
this.form.controls.autoPromptBiometrics.setValue(false);
|
||||||
await this.stateService.setDisableAutoBiometricsPrompt(true);
|
await this.stateService.setDisableAutoBiometricsPrompt(true);
|
||||||
await this.stateService.setBiometricRequirePasswordOnStart(true);
|
await this.cryptoService.setBiometricClientKeyHalf();
|
||||||
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
||||||
}
|
}
|
||||||
await this.cryptoService.refreshAdditionalKeys();
|
await this.cryptoService.refreshAdditionalKeys();
|
||||||
@ -488,10 +491,9 @@ export class SettingsComponent implements OnInit {
|
|||||||
this.form.controls.autoPromptBiometrics.setValue(false);
|
this.form.controls.autoPromptBiometrics.setValue(false);
|
||||||
await this.updateAutoPromptBiometrics();
|
await this.updateAutoPromptBiometrics();
|
||||||
|
|
||||||
await this.stateService.setBiometricRequirePasswordOnStart(true);
|
await this.cryptoService.setBiometricClientKeyHalf();
|
||||||
} else {
|
} else {
|
||||||
await this.stateService.setBiometricRequirePasswordOnStart(false);
|
await this.cryptoService.removeBiometricClientKeyHalf();
|
||||||
await this.stateService.setBiometricEncryptionClientKeyHalf(null);
|
|
||||||
}
|
}
|
||||||
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
||||||
await this.cryptoService.refreshAdditionalKeys();
|
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 { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.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 { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
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 { LoginGuard } from "../../auth/guards/login.guard";
|
||||||
import { Account } from "../../models/account";
|
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 { ElectronLogService } from "../../platform/services/electron-log.service";
|
||||||
import { ElectronPlatformUtilsService } from "../../platform/services/electron-platform-utils.service";
|
import { ElectronPlatformUtilsService } from "../../platform/services/electron-platform-utils.service";
|
||||||
import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service";
|
import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service";
|
||||||
@ -178,7 +182,11 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: CryptoServiceAbstraction,
|
provide: CryptoServiceAbstraction,
|
||||||
useClass: ElectronCryptoService,
|
useExisting: ElectronCryptoService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ElectronCryptoService,
|
||||||
|
useClass: DefaultElectronCryptoService,
|
||||||
deps: [
|
deps: [
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
@ -187,6 +195,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
StateProvider,
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ElectronCryptoService } from "../platform/services/electron-crypto.service";
|
||||||
import { ElectronStateService } from "../platform/services/electron-state.service.abstraction";
|
import { ElectronStateService } from "../platform/services/electron-state.service.abstraction";
|
||||||
|
|
||||||
import { LockComponent } from "./lock.component";
|
import { LockComponent } from "./lock.component";
|
||||||
@ -78,7 +80,11 @@ describe("LockComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: CryptoService,
|
provide: CryptoService,
|
||||||
useValue: mock<CryptoService>(),
|
useExisting: ElectronCryptoService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ElectronCryptoService,
|
||||||
|
useValue: mock<ElectronCryptoService>(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: VaultTimeoutService,
|
provide: VaultTimeoutService,
|
||||||
@ -140,6 +146,10 @@ describe("LockComponent", () => {
|
|||||||
provide: PinCryptoServiceAbstraction,
|
provide: PinCryptoServiceAbstraction,
|
||||||
useValue: mock<PinCryptoServiceAbstraction>(),
|
useValue: mock<PinCryptoServiceAbstraction>(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: BiometricStateService,
|
||||||
|
useValue: mock<BiometricStateService>(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).compileComponents();
|
}).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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ElectronCryptoService } from "../platform/services/electron-crypto.service";
|
||||||
import { ElectronStateService } from "../platform/services/electron-state.service.abstraction";
|
import { ElectronStateService } from "../platform/services/electron-state.service.abstraction";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "LockComponent";
|
const BroadcasterSubscriptionId = "LockComponent";
|
||||||
@ -41,7 +41,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
messagingService: MessagingService,
|
messagingService: MessagingService,
|
||||||
cryptoService: CryptoService,
|
protected override cryptoService: ElectronCryptoService,
|
||||||
vaultTimeoutService: VaultTimeoutService,
|
vaultTimeoutService: VaultTimeoutService,
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
@ -175,8 +175,8 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
type: "warning",
|
type: "warning",
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.stateService.setBiometricRequirePasswordOnStart(response);
|
|
||||||
if (response) {
|
if (response) {
|
||||||
|
await this.cryptoService.setBiometricClientKeyHalf();
|
||||||
await this.stateService.setDisableAutoBiometricsPrompt(true);
|
await this.stateService.setDisableAutoBiometricsPrompt(true);
|
||||||
}
|
}
|
||||||
this.supportsBiometric = await this.canUseBiometric();
|
this.supportsBiometric = await this.canUseBiometric();
|
||||||
|
@ -3,6 +3,7 @@ import * as path from "path";
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
|
||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
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 { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
@ -159,6 +160,8 @@ export class Main {
|
|||||||
this.updaterMain,
|
this.updaterMain,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const biometricStateService = new DefaultBiometricStateService(stateProvider);
|
||||||
|
|
||||||
this.biometricsService = new BiometricsService(
|
this.biometricsService = new BiometricsService(
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.windowMain,
|
this.windowMain,
|
||||||
@ -166,6 +169,7 @@ export class Main {
|
|||||||
this.logService,
|
this.logService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
process.platform,
|
process.platform,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.desktopCredentialStorageListener = new DesktopCredentialStorageListener(
|
this.desktopCredentialStorageListener = new DesktopCredentialStorageListener(
|
||||||
|
@ -1,25 +1,15 @@
|
|||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Account as BaseAccount,
|
Account as BaseAccount,
|
||||||
AccountSettings as BaseAccountSettings,
|
AccountSettings as BaseAccountSettings,
|
||||||
AccountKeys as BaseAccountKeys,
|
|
||||||
} from "@bitwarden/common/platform/models/domain/account";
|
} from "@bitwarden/common/platform/models/domain/account";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
||||||
|
|
||||||
export class AccountSettings extends BaseAccountSettings {
|
export class AccountSettings extends BaseAccountSettings {
|
||||||
vaultTimeout = -1; // On Restart
|
vaultTimeout = -1; // On Restart
|
||||||
requirePasswordOnStart?: boolean;
|
|
||||||
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountKeys extends BaseAccountKeys {
|
|
||||||
biometricEncryptionClientKeyHalf?: Jsonify<EncString>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Account extends BaseAccount {
|
export class Account extends BaseAccount {
|
||||||
settings?: AccountSettings = new AccountSettings();
|
settings?: AccountSettings = new AccountSettings();
|
||||||
keys?: AccountKeys = new AccountKeys();
|
|
||||||
|
|
||||||
constructor(init: Partial<Account>) {
|
constructor(init: Partial<Account>) {
|
||||||
super(init);
|
super(init);
|
||||||
|
@ -3,6 +3,8 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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 { WindowMain } from "../../../main/window.main";
|
||||||
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
|
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
|
||||||
@ -25,8 +27,10 @@ describe("biometrics tests", function () {
|
|||||||
const stateService = mock<ElectronStateService>();
|
const stateService = mock<ElectronStateService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const messagingService = mock<MessagingService>();
|
const messagingService = mock<MessagingService>();
|
||||||
|
const biometricStateService = mock<BiometricStateService>();
|
||||||
|
|
||||||
it("Should call the platformspecific methods", async () => {
|
it("Should call the platformspecific methods", async () => {
|
||||||
|
const userId = "userId-1" as UserId;
|
||||||
const sut = new BiometricsService(
|
const sut = new BiometricsService(
|
||||||
i18nService,
|
i18nService,
|
||||||
windowMain,
|
windowMain,
|
||||||
@ -34,6 +38,7 @@ describe("biometrics tests", function () {
|
|||||||
logService,
|
logService,
|
||||||
messagingService,
|
messagingService,
|
||||||
process.platform,
|
process.platform,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockService = mock<OsBiometricService>();
|
const mockService = mock<OsBiometricService>();
|
||||||
@ -46,7 +51,7 @@ describe("biometrics tests", function () {
|
|||||||
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
|
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
|
||||||
expect(mockService.init).toBeCalled();
|
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();
|
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.
|
// 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,
|
logService,
|
||||||
messagingService,
|
messagingService,
|
||||||
"win32",
|
"win32",
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const internalService = (sut as any).platformSpecificService;
|
const internalService = (sut as any).platformSpecificService;
|
||||||
@ -79,6 +85,7 @@ describe("biometrics tests", function () {
|
|||||||
logService,
|
logService,
|
||||||
messagingService,
|
messagingService,
|
||||||
"darwin",
|
"darwin",
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
const internalService = (sut as any).platformSpecificService;
|
const internalService = (sut as any).platformSpecificService;
|
||||||
expect(internalService).not.toBeNull();
|
expect(internalService).not.toBeNull();
|
||||||
@ -89,6 +96,7 @@ describe("biometrics tests", function () {
|
|||||||
describe("can auth biometric", () => {
|
describe("can auth biometric", () => {
|
||||||
let sut: BiometricsService;
|
let sut: BiometricsService;
|
||||||
let innerService: MockProxy<OsBiometricService>;
|
let innerService: MockProxy<OsBiometricService>;
|
||||||
|
const userId = "userId-1" as UserId;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sut = new BiometricsService(
|
sut = new BiometricsService(
|
||||||
@ -98,6 +106,7 @@ describe("biometrics tests", function () {
|
|||||||
logService,
|
logService,
|
||||||
messagingService,
|
messagingService,
|
||||||
process.platform,
|
process.platform,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
innerService = mock();
|
innerService = mock();
|
||||||
@ -108,9 +117,9 @@ describe("biometrics tests", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if client key half is required and not provided", async () => {
|
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);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
@ -121,18 +130,18 @@ describe("biometrics tests", function () {
|
|||||||
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
|
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
|
||||||
expect(innerService.init).toBeCalled();
|
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();
|
expect(innerService.osSupportsBiometric).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call osSupportBiometric if client key half is not required", async () => {
|
it("should call osSupportBiometric if client key half is not required", async () => {
|
||||||
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(false);
|
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(false);
|
||||||
innerService.osSupportsBiometric.mockResolvedValue(true);
|
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(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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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 { WindowMain } from "../../../main/window.main";
|
||||||
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
|
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
|
||||||
@ -18,6 +20,7 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private platform: NodeJS.Platform,
|
private platform: NodeJS.Platform,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
this.loadPlatformSpecificService(this.platform);
|
this.loadPlatformSpecificService(this.platform);
|
||||||
}
|
}
|
||||||
@ -70,11 +73,9 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
|||||||
}: {
|
}: {
|
||||||
service: string;
|
service: string;
|
||||||
key: string;
|
key: string;
|
||||||
userId: string;
|
userId: UserId;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const requireClientKeyHalf = await this.stateService.getBiometricRequirePasswordOnStart({
|
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||||
userId,
|
|
||||||
});
|
|
||||||
const clientKeyHalfB64 = this.getClientKeyHalf(service, key);
|
const clientKeyHalfB64 = this.getClientKeyHalf(service, key);
|
||||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||||
return clientKeyHalfSatisfied && (await this.osSupportsBiometric());
|
return clientKeyHalfSatisfied && (await this.osSupportsBiometric());
|
||||||
@ -171,7 +172,13 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async enforceClientKeyHalf(service: string, storageKey: string): Promise<void> {
|
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);
|
const clientKeyHalfB64 = this.getClientKeyHalf(service, storageKey);
|
||||||
|
|
||||||
if (requireClientKeyHalf && !clientKeyHalfB64) {
|
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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { 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 { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
@ -15,11 +18,11 @@ import {
|
|||||||
mockAccountServiceWith,
|
mockAccountServiceWith,
|
||||||
} from "../../../../../libs/common/spec/fake-account-service";
|
} 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";
|
import { ElectronStateService } from "./electron-state.service.abstraction";
|
||||||
|
|
||||||
describe("electronCryptoService", () => {
|
describe("electronCryptoService", () => {
|
||||||
let electronCryptoService: ElectronCryptoService;
|
let sut: DefaultElectronCryptoService;
|
||||||
|
|
||||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
@ -28,6 +31,7 @@ describe("electronCryptoService", () => {
|
|||||||
const stateService = mock<ElectronStateService>();
|
const stateService = mock<ElectronStateService>();
|
||||||
let accountService: FakeAccountService;
|
let accountService: FakeAccountService;
|
||||||
let stateProvider: FakeStateProvider;
|
let stateProvider: FakeStateProvider;
|
||||||
|
const biometricStateService = mock<BiometricStateService>();
|
||||||
|
|
||||||
const mockUserId = "mock user id" as UserId;
|
const mockUserId = "mock user id" as UserId;
|
||||||
|
|
||||||
@ -35,7 +39,7 @@ describe("electronCryptoService", () => {
|
|||||||
accountService = mockAccountServiceWith("userId" as UserId);
|
accountService = mockAccountServiceWith("userId" as UserId);
|
||||||
stateProvider = new FakeStateProvider(accountService);
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
electronCryptoService = new ElectronCryptoService(
|
sut = new DefaultElectronCryptoService(
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
encryptService,
|
encryptService,
|
||||||
platformUtilService,
|
platformUtilService,
|
||||||
@ -43,6 +47,7 @@ describe("electronCryptoService", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
accountService,
|
accountService,
|
||||||
stateProvider,
|
stateProvider,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,8 +55,42 @@ describe("electronCryptoService", () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("instantiates", () => {
|
describe("setBiometricClientKeyHalf", () => {
|
||||||
expect(electronCryptoService).not.toBeFalsy();
|
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", () => {
|
describe("setUserKey", () => {
|
||||||
@ -66,9 +105,9 @@ describe("electronCryptoService", () => {
|
|||||||
it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
|
it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
|
||||||
stateService.getBiometricUnlock.mockResolvedValue(true);
|
stateService.getBiometricUnlock.mockResolvedValue(true);
|
||||||
platformUtilService.supportsSecureStorage.mockReturnValue(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(stateService.setUserKeyBiometric).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: null }),
|
expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: null }),
|
||||||
@ -82,7 +121,7 @@ describe("electronCryptoService", () => {
|
|||||||
stateService.getBiometricUnlock.mockResolvedValue(true);
|
stateService.getBiometricUnlock.mockResolvedValue(true);
|
||||||
platformUtilService.supportsSecureStorage.mockReturnValue(false);
|
platformUtilService.supportsSecureStorage.mockReturnValue(false);
|
||||||
|
|
||||||
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
|
await sut.setUserKey(mockUserKey, mockUserId);
|
||||||
|
|
||||||
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(null, {
|
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(null, {
|
||||||
userId: mockUserId,
|
userId: mockUserId,
|
||||||
@ -90,7 +129,7 @@ describe("electronCryptoService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("clears the old deprecated Biometric key whenever a User Key is set", async () => {
|
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, {
|
expect(stateService.setCryptoMasterKeyBiometric).toHaveBeenCalledWith(null, {
|
||||||
userId: mockUserId,
|
userId: mockUserId,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
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";
|
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(
|
constructor(
|
||||||
cryptoFunctionService: CryptoFunctionService,
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
encryptService: EncryptService,
|
encryptService: EncryptService,
|
||||||
@ -24,6 +38,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
protected override stateService: ElectronStateService,
|
protected override stateService: ElectronStateService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
@ -47,17 +62,27 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
|
|
||||||
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
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.
|
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.biometricStateService.removeEncryptedClientKeyHalf(userId);
|
||||||
this.stateService.setUserKeyBiometric(null, { userId: userId });
|
await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// 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) {
|
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> {
|
protected async storeBiometricKey(key: UserKey, userId?: UserId): Promise<void> {
|
||||||
let clientEncKeyHalf: CsprngString = null;
|
// May resolve to null, in which case no client key have is required
|
||||||
if (await this.stateService.getBiometricRequirePasswordOnStart({ userId })) {
|
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
|
||||||
clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
|
|
||||||
}
|
|
||||||
await this.stateService.setUserKeyBiometric(
|
await this.stateService.setUserKeyBiometric(
|
||||||
{ key: key.keyB64, clientEncKeyHalf },
|
{ key: key.keyB64, clientEncKeyHalf },
|
||||||
{ userId: userId },
|
{ userId: userId },
|
||||||
@ -105,30 +128,21 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
protected override async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
await this.clearStoredUserKey(KeySuffixOptions.Biometric, userId);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await super.clearAllStoredUserKeys(userId);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
super.clearAllStoredUserKeys(userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBiometricEncryptionClientKeyHalf(userId?: UserId): Promise<CsprngString | null> {
|
private async getBiometricEncryptionClientKeyHalf(userId?: UserId): Promise<CsprngString | null> {
|
||||||
try {
|
const encryptedKeyHalfPromise =
|
||||||
let biometricKey = await this.stateService
|
userId == null
|
||||||
.getBiometricEncryptionClientKeyHalf({ userId })
|
? firstValueFrom(this.biometricStateService.encryptedClientKeyHalf$)
|
||||||
.then((result) => result?.decrypt(null /* user encrypted */))
|
: this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||||
.then((result) => result as CsprngString);
|
const encryptedKeyHalf = await encryptedKeyHalfPromise;
|
||||||
const userKey = await this.getUserKeyWithLegacySupport();
|
if (encryptedKeyHalf == null) {
|
||||||
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 {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const userKey = await this.getUserKey();
|
||||||
|
return (await this.encryptService.decryptToUtf8(encryptedKeyHalf, userKey)) as CsprngString;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --LEGACY METHODS--
|
// --LEGACY METHODS--
|
||||||
|
@ -1,17 +1,9 @@
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
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 { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
|
|
||||||
export abstract class ElectronStateService extends StateService<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>;
|
getDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<void>;
|
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 { 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 { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
@ -24,49 +23,6 @@ export class ElectronStateService
|
|||||||
await super.addAccount(account);
|
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> {
|
async getDismissedBiometricRequirePasswordOnStart(options?: StorageOptions): Promise<boolean> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||||
|
@ -95,6 +95,10 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
|
|||||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import {
|
||||||
|
BiometricStateService,
|
||||||
|
DefaultBiometricStateService,
|
||||||
|
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags";
|
import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||||
@ -876,6 +880,11 @@ import { ModalService } from "./modal.service";
|
|||||||
OrganizationApiServiceAbstraction,
|
OrganizationApiServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: BiometricStateService,
|
||||||
|
useClass: DefaultBiometricStateService,
|
||||||
|
deps: [StateProvider],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
@ -3,6 +3,9 @@ import { Observable } from "rxjs";
|
|||||||
|
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
|
||||||
|
import { EncryptionType } from "../src/platform/enums";
|
||||||
|
import { Utils } from "../src/platform/misc/utils";
|
||||||
|
|
||||||
function newGuid() {
|
function newGuid() {
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||||
const r = (Math.random() * 16) | 0;
|
const r = (Math.random() * 16) | 0;
|
||||||
@ -29,6 +32,11 @@ export function mockEnc(s: string): MockProxy<EncString> {
|
|||||||
return mocked;
|
return mocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeEncString(data?: string) {
|
||||||
|
data ??= Utils.newGuid();
|
||||||
|
return new EncString(EncryptionType.AesCbc256_HmacSha256_B64, data, "test", "test");
|
||||||
|
}
|
||||||
|
|
||||||
export function makeStaticByteArray(length: number, start = 0) {
|
export function makeStaticByteArray(length: number, start = 0) {
|
||||||
const arr = new Uint8Array(length);
|
const arr = new Uint8Array(length);
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
|
@ -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 { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { makeStaticByteArray } from "../../../../spec";
|
import { makeEncString, makeStaticByteArray } from "../../../../spec";
|
||||||
import { ProviderEncryptedOrganizationKey } from "../../../admin-console/models/domain/encrypted-organization-key";
|
|
||||||
import { OrgKey } from "../../../types/key";
|
import { OrgKey } from "../../../types/key";
|
||||||
import { CryptoService } from "../../abstractions/crypto.service";
|
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 { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ORGANIZATION_KEYS } from "./org-keys.state";
|
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", () => {
|
describe("encrypted org keys", () => {
|
||||||
const sut = USER_ENCRYPTED_ORGANIZATION_KEYS;
|
const sut = USER_ENCRYPTED_ORGANIZATION_KEYS;
|
||||||
|
|
||||||
|
@ -1,22 +1,15 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { makeStaticByteArray } from "../../../../spec";
|
import { makeEncString, makeStaticByteArray } from "../../../../spec";
|
||||||
import { ProviderId } from "../../../types/guid";
|
import { ProviderId } from "../../../types/guid";
|
||||||
import { ProviderKey, UserPrivateKey } from "../../../types/key";
|
import { ProviderKey, UserPrivateKey } from "../../../types/key";
|
||||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||||
import { EncryptionType } from "../../enums";
|
import { EncryptedString } from "../../models/domain/enc-string";
|
||||||
import { Utils } from "../../misc/utils";
|
|
||||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
|
||||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||||
import { CryptoService } from "../crypto.service";
|
import { CryptoService } from "../crypto.service";
|
||||||
|
|
||||||
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./provider-keys.state";
|
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", () => {
|
describe("encrypted provider keys", () => {
|
||||||
const sut = USER_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 BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
|
||||||
|
|
||||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||||
|
|
||||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||||
|
|
||||||
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
||||||
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
||||||
|
|
||||||
|
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
|
||||||
|
|
||||||
// Admin Console
|
// Admin Console
|
||||||
export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk");
|
export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk");
|
||||||
export const POLICIES_DISK = new StateDefinition("policies", "disk");
|
export const POLICIES_DISK = new StateDefinition("policies", "disk");
|
||||||
export const POLICIES_MEMORY = new StateDefinition("policies", "memory");
|
export const POLICIES_MEMORY = new StateDefinition("policies", "memory");
|
||||||
export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
||||||
//
|
|
||||||
|
@ -9,6 +9,7 @@ import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-t
|
|||||||
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
||||||
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
|
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
|
||||||
import { ProviderKeyMigrator } from "./migrations/13-move-provider-keys-to-state-providers";
|
import { ProviderKeyMigrator } from "./migrations/13-move-provider-keys-to-state-providers";
|
||||||
|
import { MoveBiometricClientKeyHalfToStateProviders } from "./migrations/14-move-biometric-client-key-half-state-to-providers";
|
||||||
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
||||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
@ -19,7 +20,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 2;
|
export const MIN_VERSION = 2;
|
||||||
export const CURRENT_VERSION = 13;
|
export const CURRENT_VERSION = 14;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export async function migrate(
|
export async function migrate(
|
||||||
@ -36,7 +37,6 @@ export async function migrate(
|
|||||||
await storageService.save("stateVersion", CURRENT_VERSION);
|
await storageService.save("stateVersion", CURRENT_VERSION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await MigrationBuilder.create()
|
await MigrationBuilder.create()
|
||||||
.with(MinVersionMigrator)
|
.with(MinVersionMigrator)
|
||||||
.with(FixPremiumMigrator, 2, 3)
|
.with(FixPremiumMigrator, 2, 3)
|
||||||
@ -49,7 +49,8 @@ export async function migrate(
|
|||||||
.with(EverHadUserKeyMigrator, 9, 10)
|
.with(EverHadUserKeyMigrator, 9, 10)
|
||||||
.with(OrganizationKeyMigrator, 10, 11)
|
.with(OrganizationKeyMigrator, 10, 11)
|
||||||
.with(MoveEnvironmentStateToProviders, 11, 12)
|
.with(MoveEnvironmentStateToProviders, 11, 12)
|
||||||
.with(ProviderKeyMigrator, 12, CURRENT_VERSION)
|
.with(ProviderKeyMigrator, 12, 13)
|
||||||
|
.with(MoveBiometricClientKeyHalfToStateProviders, 13, CURRENT_VERSION)
|
||||||
|
|
||||||
.migrate(migrationHelper);
|
.migrate(migrationHelper);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import { MockProxy, any } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MoveBiometricClientKeyHalfToStateProviders,
|
||||||
|
CLIENT_KEY_HALF,
|
||||||
|
} from "./14-move-biometric-client-key-half-state-to-providers";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||||
|
"user-1": {
|
||||||
|
keys: {
|
||||||
|
biometricEncryptionClientKeyHalf: "user1-key-half",
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
keys: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
"user_user-1_biometricSettings_clientKeyHalf": "user1-key-half",
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||||
|
"user-1": {
|
||||||
|
keys: {
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
keys: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("DesktopBiometricState migrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: MoveBiometricClientKeyHalfToStateProviders;
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 13);
|
||||||
|
sut = new MoveBiometricClientKeyHalfToStateProviders(13, 14);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove biometricEncryptionClientKeyHalf from all accounts", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
keys: {
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set biometricEncryptionClientKeyHalf value for account that have it", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user-1", CLIENT_KEY_HALF, "user1-key-half");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call extra setToUser", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 14);
|
||||||
|
sut = new MoveBiometricClientKeyHalfToStateProviders(13, 14);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should null out new values", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user-1", CLIENT_KEY_HALF, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add explicit value back to accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
keys: {
|
||||||
|
biometricEncryptionClientKeyHalf: "user1-key-half",
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["user-2", "user-3"])(
|
||||||
|
"should not try to restore values to missing accounts",
|
||||||
|
async (userId) => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,65 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
settings?: {
|
||||||
|
disableAutoBiometricsPrompt?: boolean;
|
||||||
|
biometricUnlock?: boolean;
|
||||||
|
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||||
|
};
|
||||||
|
keys?: { biometricEncryptionClientKeyHalf?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Biometric text, no auto prompt text, fingerprint validated, and prompt cancelled are refreshed on every app start, so we don't need to migrate them
|
||||||
|
export const CLIENT_KEY_HALF: KeyDefinitionLike = {
|
||||||
|
key: "clientKeyHalf",
|
||||||
|
stateDefinition: { name: "biometricSettings" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MoveBiometricClientKeyHalfToStateProviders extends Migrator<13, 14> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
legacyAccounts.map(async ({ userId, account }) => {
|
||||||
|
// Move account data
|
||||||
|
if (account?.keys?.biometricEncryptionClientKeyHalf != null) {
|
||||||
|
await helper.setToUser(
|
||||||
|
userId,
|
||||||
|
CLIENT_KEY_HALF,
|
||||||
|
account.keys.biometricEncryptionClientKeyHalf,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete old account data
|
||||||
|
delete account?.keys?.biometricEncryptionClientKeyHalf;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||||
|
let updatedAccount = false;
|
||||||
|
|
||||||
|
const userKeyHalf = await helper.getFromUser<string>(userId, CLIENT_KEY_HALF);
|
||||||
|
|
||||||
|
if (userKeyHalf) {
|
||||||
|
account ??= {};
|
||||||
|
account.keys ??= {};
|
||||||
|
|
||||||
|
updatedAccount = true;
|
||||||
|
account.keys.biometricEncryptionClientKeyHalf = userKeyHalf;
|
||||||
|
await helper.setToUser(userId, CLIENT_KEY_HALF, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedAccount) {
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user