mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-28 04:08:47 +02:00
[PM-5537] Migrate Biometric Prompts (#7771)
* Fix nextMock arguments
* Add state for biometric prompts
* Use biometric state for prompts
* Migrate biometric prompt data
* wire up biometric state to logouts
* Add migrator to migrate list
* Remove usages of prompt automatically
Explicitly list non-nulled state as intentional
* `npm run prettier` 🤖
* Fix web lock component
This commit is contained in:
parent
19a373d87e
commit
9775e77079
@ -1,5 +1,6 @@
|
|||||||
import { Component, NgZone } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||||
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
@ -19,6 +20,7 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.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";
|
||||||
|
|
||||||
@ -59,6 +61,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
private routerService: BrowserRouterService,
|
private routerService: BrowserRouterService,
|
||||||
|
biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
router,
|
router,
|
||||||
@ -80,6 +83,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
deviceTrustCryptoService,
|
deviceTrustCryptoService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
this.successRoute = "/tabs/current";
|
this.successRoute = "/tabs/current";
|
||||||
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
|
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||||
@ -100,8 +104,9 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
const disableAutoBiometricsPrompt =
|
const disableAutoBiometricsPrompt = await firstValueFrom(
|
||||||
(await this.stateService.getDisableAutoBiometricsPrompt()) ?? true;
|
this.biometricStateService.promptAutomatically$,
|
||||||
|
);
|
||||||
|
|
||||||
window.setTimeout(async () => {
|
window.setTimeout(async () => {
|
||||||
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
|
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PinCryptoServiceAbstraction,
|
PinCryptoServiceAbstraction,
|
||||||
PinCryptoService,
|
PinCryptoService,
|
||||||
@ -64,6 +66,10 @@ import {
|
|||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} 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,
|
||||||
|
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 { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||||
@ -280,6 +286,7 @@ export default class MainBackground {
|
|||||||
individualVaultExportService: IndividualVaultExportServiceAbstraction;
|
individualVaultExportService: IndividualVaultExportServiceAbstraction;
|
||||||
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
|
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
|
||||||
vaultSettingsService: VaultSettingsServiceAbstraction;
|
vaultSettingsService: VaultSettingsServiceAbstraction;
|
||||||
|
biometricStateService: BiometricStateService;
|
||||||
|
|
||||||
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
||||||
backgroundWindow = window;
|
backgroundWindow = window;
|
||||||
@ -321,7 +328,7 @@ export default class MainBackground {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logoutCallback = async (expired: boolean, userId?: string) =>
|
const logoutCallback = async (expired: boolean, userId?: UserId) =>
|
||||||
await this.logout(expired, userId);
|
await this.logout(expired, userId);
|
||||||
|
|
||||||
this.messagingService = this.popupOnlyContext
|
this.messagingService = this.popupOnlyContext
|
||||||
@ -386,6 +393,7 @@ export default class MainBackground {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||||
|
|
||||||
const migrationRunner = new MigrationRunner(
|
const migrationRunner = new MigrationRunner(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
@ -1043,7 +1051,9 @@ export default class MainBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(expired: boolean, userId?: string) {
|
async logout(expired: boolean, userId?: UserId) {
|
||||||
|
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
|
||||||
await this.eventUploadService.uploadEvents(userId);
|
await this.eventUploadService.uploadEvents(userId);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -1058,6 +1068,7 @@ export default class MainBackground {
|
|||||||
this.vaultTimeoutSettingsService.clear(userId),
|
this.vaultTimeoutSettingsService.clear(userId),
|
||||||
this.keyConnectorService.clear(),
|
this.keyConnectorService.clear(),
|
||||||
this.vaultFilterService.clear(),
|
this.vaultFilterService.clear(),
|
||||||
|
this.biometricStateService.logout(userId),
|
||||||
// We intentionally do not clear the autofillSettingsService
|
// We intentionally do not clear the autofillSettingsService
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
||||||
@ -101,6 +102,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
this.accountSwitcherEnabled = enableAccountSwitching();
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
}
|
}
|
||||||
@ -176,7 +178,9 @@ export class SettingsComponent implements OnInit {
|
|||||||
),
|
),
|
||||||
pin: pinStatus !== "DISABLED",
|
pin: pinStatus !== "DISABLED",
|
||||||
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||||
enableAutoBiometricsPrompt: !(await this.stateService.getDisableAutoBiometricsPrompt()),
|
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||||
|
this.biometricStateService.promptAutomatically$,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
this.form.patchValue(initialValues); // Emit event to initialize `pairwise` operator
|
this.form.patchValue(initialValues); // Emit event to initialize `pairwise` operator
|
||||||
|
|
||||||
@ -416,8 +420,8 @@ export class SettingsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAutoBiometricsPrompt() {
|
async updateAutoBiometricsPrompt() {
|
||||||
await this.stateService.setDisableAutoBiometricsPrompt(
|
await this.biometricStateService.setPromptAutomatically(
|
||||||
!this.form.value.enableAutoBiometricsPrompt,
|
this.form.value.enableAutoBiometricsPrompt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
|||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums";
|
import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||||
@ -205,6 +206,7 @@ export class Main {
|
|||||||
derivedStateProvider: DerivedStateProvider;
|
derivedStateProvider: DerivedStateProvider;
|
||||||
stateProvider: StateProvider;
|
stateProvider: StateProvider;
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction;
|
loginStrategyService: LoginStrategyServiceAbstraction;
|
||||||
|
biometricStateService: BiometricStateService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let p = null;
|
let p = null;
|
||||||
@ -627,6 +629,7 @@ export class Main {
|
|||||||
this.collectionService.clear(userId as UserId),
|
this.collectionService.clear(userId as UserId),
|
||||||
this.policyService.clear(userId),
|
this.policyService.clear(userId),
|
||||||
this.passwordGenerationService.clear(),
|
this.passwordGenerationService.clear(),
|
||||||
|
this.biometricStateService.logout(userId as UserId),
|
||||||
]);
|
]);
|
||||||
await this.stateService.clean();
|
await this.stateService.clean();
|
||||||
process.env.BW_SESSION = null;
|
process.env.BW_SESSION = null;
|
||||||
|
@ -16,6 +16,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
|||||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.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";
|
||||||
@ -23,7 +24,6 @@ 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 { ElectronStateService } from "../../platform/services/electron-state.service.abstraction";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-settings",
|
selector: "app-settings",
|
||||||
@ -110,7 +110,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
private stateService: ElectronStateService,
|
private stateService: StateService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
@ -242,7 +242,7 @@ 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 firstValueFrom(this.biometricStateService.promptAutomatically$),
|
||||||
requirePasswordOnStart: await firstValueFrom(
|
requirePasswordOnStart: await firstValueFrom(
|
||||||
this.biometricStateService.requirePasswordOnStart$,
|
this.biometricStateService.requirePasswordOnStart$,
|
||||||
),
|
),
|
||||||
@ -453,9 +453,9 @@ export class SettingsComponent implements OnInit {
|
|||||||
// Recommended settings for Windows Hello
|
// Recommended settings for Windows Hello
|
||||||
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.biometricStateService.setPromptAutomatically(false);
|
||||||
await this.biometricStateService.setRequirePasswordOnStart(true);
|
await this.biometricStateService.setRequirePasswordOnStart(true);
|
||||||
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
|
||||||
}
|
}
|
||||||
await this.cryptoService.refreshAdditionalKeys();
|
await this.cryptoService.refreshAdditionalKeys();
|
||||||
|
|
||||||
@ -475,10 +475,9 @@ export class SettingsComponent implements OnInit {
|
|||||||
// require password on start must be disabled if auto prompt biometrics is enabled
|
// require password on start must be disabled if auto prompt biometrics is enabled
|
||||||
this.form.controls.requirePasswordOnStart.setValue(false);
|
this.form.controls.requirePasswordOnStart.setValue(false);
|
||||||
await this.updateRequirePasswordOnStart();
|
await this.updateRequirePasswordOnStart();
|
||||||
|
await this.biometricStateService.setPromptAutomatically(true);
|
||||||
await this.stateService.setDisableAutoBiometricsPrompt(null);
|
|
||||||
} else {
|
} else {
|
||||||
await this.stateService.setDisableAutoBiometricsPrompt(true);
|
await this.biometricStateService.setPromptAutomatically(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,7 +491,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
await this.biometricStateService.setRequirePasswordOnStart(false);
|
await this.biometricStateService.setRequirePasswordOnStart(false);
|
||||||
}
|
}
|
||||||
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
|
||||||
await this.cryptoService.refreshAdditionalKeys();
|
await this.cryptoService.refreshAdditionalKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||||
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -147,6 +148,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -576,6 +578,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
||||||
await this.policyService.clear(userBeingLoggedOut);
|
await this.policyService.clear(userBeingLoggedOut);
|
||||||
await this.keyConnectorService.clear();
|
await this.keyConnectorService.clear();
|
||||||
|
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
|
||||||
|
|
||||||
preLogoutActiveUserId = this.activeUserId;
|
preLogoutActiveUserId = this.activeUserId;
|
||||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||||
|
@ -57,7 +57,6 @@ import { ElectronRendererMessagingService } from "../../platform/services/electr
|
|||||||
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
|
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
|
||||||
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
|
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
|
||||||
import { ElectronStateService } from "../../platform/services/electron-state.service";
|
import { ElectronStateService } from "../../platform/services/electron-state.service";
|
||||||
import { ElectronStateService as ElectronStateServiceAbstraction } from "../../platform/services/electron-state.service.abstraction";
|
|
||||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||||
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
||||||
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
||||||
@ -140,10 +139,6 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: ElectronStateServiceAbstraction,
|
|
||||||
useExisting: StateServiceAbstraction,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: FileDownloadService,
|
provide: FileDownloadService,
|
||||||
useClass: DesktopFileDownloadService,
|
useClass: DesktopFileDownloadService,
|
||||||
|
@ -21,12 +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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.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 { ElectronStateService } from "../platform/services/electron-state.service.abstraction";
|
|
||||||
|
|
||||||
import { LockComponent } from "./lock.component";
|
import { LockComponent } from "./lock.component";
|
||||||
|
|
||||||
// ipc mock global
|
// ipc mock global
|
||||||
@ -43,14 +42,15 @@ const isWindowVisibleMock = jest.fn();
|
|||||||
describe("LockComponent", () => {
|
describe("LockComponent", () => {
|
||||||
let component: LockComponent;
|
let component: LockComponent;
|
||||||
let fixture: ComponentFixture<LockComponent>;
|
let fixture: ComponentFixture<LockComponent>;
|
||||||
let stateServiceMock: MockProxy<ElectronStateService>;
|
let stateServiceMock: MockProxy<StateService>;
|
||||||
|
const biometricStateService = mock<BiometricStateService>();
|
||||||
let messagingServiceMock: MockProxy<MessagingService>;
|
let messagingServiceMock: MockProxy<MessagingService>;
|
||||||
let broadcasterServiceMock: MockProxy<BroadcasterService>;
|
let broadcasterServiceMock: MockProxy<BroadcasterService>;
|
||||||
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
||||||
let activatedRouteMock: MockProxy<ActivatedRoute>;
|
let activatedRouteMock: MockProxy<ActivatedRoute>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
stateServiceMock = mock<ElectronStateService>();
|
stateServiceMock = mock<StateService>();
|
||||||
stateServiceMock.activeAccount$ = of(null);
|
stateServiceMock.activeAccount$ = of(null);
|
||||||
|
|
||||||
messagingServiceMock = mock<MessagingService>();
|
messagingServiceMock = mock<MessagingService>();
|
||||||
@ -60,9 +60,11 @@ describe("LockComponent", () => {
|
|||||||
activatedRouteMock = mock<ActivatedRoute>();
|
activatedRouteMock = mock<ActivatedRoute>();
|
||||||
activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>();
|
activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>();
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
biometricStateService.promptAutomatically$ = of(false);
|
||||||
TestBed.configureTestingModule({
|
biometricStateService.promptCancelled$ = of(false);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
declarations: [LockComponent, I18nPipe],
|
declarations: [LockComponent, I18nPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -94,7 +96,7 @@ describe("LockComponent", () => {
|
|||||||
useValue: mock<EnvironmentService>(),
|
useValue: mock<EnvironmentService>(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ElectronStateService,
|
provide: StateService,
|
||||||
useValue: stateServiceMock,
|
useValue: stateServiceMock,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -143,18 +145,19 @@ describe("LockComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: BiometricStateService,
|
provide: BiometricStateService,
|
||||||
useValue: mock<BiometricStateService>(),
|
useValue: biometricStateService,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(LockComponent);
|
fixture = TestBed.createComponent(LockComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
jest.clearAllMocks();
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ngOnInit", () => {
|
describe("ngOnInit", () => {
|
||||||
@ -164,15 +167,15 @@ describe("LockComponent", () => {
|
|||||||
expect(superNgOnInitSpy).toHaveBeenCalledTimes(1);
|
expect(superNgOnInitSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set "autoPromptBiometric" to true if "stateService.getDisableAutoBiometricsPrompt()" resolves to false', async () => {
|
it('should set "autoPromptBiometric" to true if "biometricState.promptAutomatically$" resolves to true', async () => {
|
||||||
stateServiceMock.getDisableAutoBiometricsPrompt.mockResolvedValue(false);
|
biometricStateService.promptAutomatically$ = of(true);
|
||||||
|
|
||||||
await component.ngOnInit();
|
await component.ngOnInit();
|
||||||
expect(component["autoPromptBiometric"]).toBe(true);
|
expect(component["autoPromptBiometric"]).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set "autoPromptBiometric" to false if "stateService.getDisableAutoBiometricsPrompt()" resolves to true', async () => {
|
it('should set "autoPromptBiometric" to false if "biometricState.promptAutomatically$" resolves to false', async () => {
|
||||||
stateServiceMock.getDisableAutoBiometricsPrompt.mockResolvedValue(true);
|
biometricStateService.promptAutomatically$ = of(false);
|
||||||
|
|
||||||
await component.ngOnInit();
|
await component.ngOnInit();
|
||||||
expect(component["autoPromptBiometric"]).toBe(false);
|
expect(component["autoPromptBiometric"]).toBe(false);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, NgZone } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { switchMap } from "rxjs";
|
import { firstValueFrom, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||||
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
@ -19,12 +19,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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.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 { ElectronStateService } from "../platform/services/electron-state.service.abstraction";
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "LockComponent";
|
const BroadcasterSubscriptionId = "LockComponent";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -46,7 +45,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
vaultTimeoutService: VaultTimeoutService,
|
vaultTimeoutService: VaultTimeoutService,
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
protected override stateService: ElectronStateService,
|
protected override stateService: StateService,
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
@ -59,7 +58,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
private biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
router,
|
router,
|
||||||
@ -81,12 +80,15 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
deviceTrustCryptoService,
|
deviceTrustCryptoService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
this.autoPromptBiometric = !(await this.stateService.getDisableAutoBiometricsPrompt());
|
this.autoPromptBiometric = await firstValueFrom(
|
||||||
|
this.biometricStateService.promptAutomatically$,
|
||||||
|
);
|
||||||
this.biometricReady = await this.canUseBiometric();
|
this.biometricReady = await this.canUseBiometric();
|
||||||
|
|
||||||
await this.displayBiometricUpdateWarning();
|
await this.displayBiometricUpdateWarning();
|
||||||
@ -140,7 +142,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.stateService.getBiometricPromptCancelled()) {
|
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +164,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async displayBiometricUpdateWarning(): Promise<void> {
|
private async displayBiometricUpdateWarning(): Promise<void> {
|
||||||
if (await this.stateService.getDismissedBiometricRequirePasswordOnStart()) {
|
if (await firstValueFrom(this.biometricStateService.dismissedRequirePasswordOnStartCallout$)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,10 +181,10 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
|
|
||||||
await this.biometricStateService.setRequirePasswordOnStart(response);
|
await this.biometricStateService.setRequirePasswordOnStart(response);
|
||||||
if (response) {
|
if (response) {
|
||||||
await this.stateService.setDisableAutoBiometricsPrompt(true);
|
await this.biometricStateService.setPromptAutomatically(false);
|
||||||
}
|
}
|
||||||
this.supportsBiometric = await this.canUseBiometric();
|
this.supportsBiometric = await this.canUseBiometric();
|
||||||
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
|
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.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";
|
||||||
@ -44,7 +45,7 @@ export class Main {
|
|||||||
memoryStorageService: MemoryStorageService;
|
memoryStorageService: MemoryStorageService;
|
||||||
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
|
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
|
||||||
messagingService: ElectronMainMessagingService;
|
messagingService: ElectronMainMessagingService;
|
||||||
stateService: ElectronStateService;
|
stateService: StateService;
|
||||||
environmentService: EnvironmentService;
|
environmentService: EnvironmentService;
|
||||||
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
||||||
migrationRunner: MigrationRunner;
|
migrationRunner: MigrationRunner;
|
||||||
@ -146,8 +147,11 @@ export class Main {
|
|||||||
false, // Do not use disk caching because this will get out of sync with the renderer service
|
false, // Do not use disk caching because this will get out of sync with the renderer service
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const biometricStateService = new DefaultBiometricStateService(stateProvider);
|
||||||
|
|
||||||
this.windowMain = new WindowMain(
|
this.windowMain = new WindowMain(
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
biometricStateService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.storageService,
|
this.storageService,
|
||||||
(arg) => this.processDeepLink(arg),
|
(arg) => this.processDeepLink(arg),
|
||||||
@ -169,8 +173,6 @@ 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,
|
||||||
|
@ -8,6 +8,7 @@ import { WindowState } from "@bitwarden/common/models/domain/window-state";
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } 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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cleanUserAgent,
|
cleanUserAgent,
|
||||||
@ -36,6 +37,7 @@ export class WindowMain {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private storageService: AbstractStorageService,
|
private storageService: AbstractStorageService,
|
||||||
private argvCallback: (argv: string[]) => void = null,
|
private argvCallback: (argv: string[]) => void = null,
|
||||||
@ -90,11 +92,9 @@ export class WindowMain {
|
|||||||
|
|
||||||
// This method will be called when Electron is shutting
|
// This method will be called when Electron is shutting
|
||||||
// down the application.
|
// down the application.
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", async () => {
|
||||||
// Allow biometric to auto-prompt on reload
|
// Allow biometric to auto-prompt on reload
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.biometricStateService.resetPromptCancelled();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.stateService.setBiometricPromptCancelled(false);
|
|
||||||
this.isQuitting = true;
|
this.isQuitting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,11 +3,11 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
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 BiometricDarwinMain from "./biometric.darwin.main";
|
import BiometricDarwinMain from "./biometric.darwin.main";
|
||||||
import BiometricWindowsMain from "./biometric.windows.main";
|
import BiometricWindowsMain from "./biometric.windows.main";
|
||||||
@ -24,7 +24,7 @@ jest.mock("@bitwarden/desktop-native", () => {
|
|||||||
describe("biometrics tests", function () {
|
describe("biometrics tests", function () {
|
||||||
const i18nService = mock<I18nService>();
|
const i18nService = mock<I18nService>();
|
||||||
const windowMain = mock<WindowMain>();
|
const windowMain = mock<WindowMain>();
|
||||||
const stateService = mock<ElectronStateService>();
|
const stateService = mock<StateService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const messagingService = mock<MessagingService>();
|
const messagingService = mock<MessagingService>();
|
||||||
const biometricStateService = mock<BiometricStateService>();
|
const biometricStateService = mock<BiometricStateService>();
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
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 { BiometricsServiceAbstraction, OsBiometricService } from "./biometrics.service.abstraction";
|
import { BiometricsServiceAbstraction, OsBiometricService } from "./biometrics.service.abstraction";
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
|||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private windowMain: WindowMain,
|
private windowMain: WindowMain,
|
||||||
private stateService: ElectronStateService,
|
private stateService: StateService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private platform: NodeJS.Platform,
|
private platform: NodeJS.Platform,
|
||||||
|
@ -6,6 +6,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
|
|||||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { makeEncString } from "@bitwarden/common/spec";
|
import { makeEncString } from "@bitwarden/common/spec";
|
||||||
@ -19,7 +20,6 @@ import {
|
|||||||
} from "../../../../../libs/common/spec/fake-account-service";
|
} from "../../../../../libs/common/spec/fake-account-service";
|
||||||
|
|
||||||
import { ElectronCryptoService } from "./electron-crypto.service";
|
import { ElectronCryptoService } from "./electron-crypto.service";
|
||||||
import { ElectronStateService } from "./electron-state.service.abstraction";
|
|
||||||
|
|
||||||
describe("electronCryptoService", () => {
|
describe("electronCryptoService", () => {
|
||||||
let sut: ElectronCryptoService;
|
let sut: ElectronCryptoService;
|
||||||
@ -29,7 +29,7 @@ describe("electronCryptoService", () => {
|
|||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
const platformUtilService = mock<PlatformUtilsService>();
|
const platformUtilService = mock<PlatformUtilsService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const stateService = mock<ElectronStateService>();
|
const stateService = mock<StateService>();
|
||||||
let accountService: FakeAccountService;
|
let accountService: FakeAccountService;
|
||||||
let stateProvider: FakeStateProvider;
|
let stateProvider: FakeStateProvider;
|
||||||
const biometricStateService = mock<BiometricStateService>();
|
const biometricStateService = mock<BiometricStateService>();
|
||||||
|
@ -4,6 +4,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
|
|||||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.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";
|
||||||
@ -15,8 +16,6 @@ import { CsprngString } from "@bitwarden/common/types/csprng";
|
|||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { ElectronStateService } from "./electron-state.service.abstraction";
|
|
||||||
|
|
||||||
export class ElectronCryptoService extends CryptoService {
|
export class ElectronCryptoService extends CryptoService {
|
||||||
constructor(
|
constructor(
|
||||||
keyGenerationService: KeyGenerationService,
|
keyGenerationService: KeyGenerationService,
|
||||||
@ -24,7 +23,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
encryptService: EncryptService,
|
encryptService: EncryptService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
protected override stateService: ElectronStateService,
|
stateService: StateService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
|
||||||
|
|
||||||
export abstract class ElectronStateService extends StateService<Account> {
|
|
||||||
getDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<void>;
|
|
||||||
}
|
|
@ -7,12 +7,7 @@ import { DeviceKey } from "@bitwarden/common/types/key";
|
|||||||
|
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
|
|
||||||
import { ElectronStateService as ElectronStateServiceAbstraction } from "./electron-state.service.abstraction";
|
export class ElectronStateService extends BaseStateService<GlobalState, Account> {
|
||||||
|
|
||||||
export class ElectronStateService
|
|
||||||
extends BaseStateService<GlobalState, Account>
|
|
||||||
implements ElectronStateServiceAbstraction
|
|
||||||
{
|
|
||||||
private partialKeys = {
|
private partialKeys = {
|
||||||
deviceKey: "_deviceKey",
|
deviceKey: "_deviceKey",
|
||||||
};
|
};
|
||||||
@ -23,24 +18,6 @@ export class ElectronStateService
|
|||||||
await super.addAccount(account);
|
await super.addAccount(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDismissedBiometricRequirePasswordOnStart(options?: StorageOptions): Promise<boolean> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
return account?.settings?.dismissedBiometricRequirePasswordOnStartCallout;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDismissedBiometricRequirePasswordOnStart(options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.dismissedBiometricRequirePasswordOnStartCallout = true;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> {
|
override async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> {
|
||||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
if (options?.userId == null) {
|
if (options?.userId == null) {
|
||||||
|
@ -5,6 +5,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
@ -24,11 +25,10 @@ import { GenerateResponse } from "../models/native-messaging/encrypted-message-r
|
|||||||
import { MessageResponseData } from "../models/native-messaging/encrypted-message-responses/message-response-data";
|
import { MessageResponseData } from "../models/native-messaging/encrypted-message-responses/message-response-data";
|
||||||
import { SuccessStatusResponse } from "../models/native-messaging/encrypted-message-responses/success-status-response";
|
import { SuccessStatusResponse } from "../models/native-messaging/encrypted-message-responses/success-status-response";
|
||||||
import { UserStatusErrorResponse } from "../models/native-messaging/encrypted-message-responses/user-status-error-response";
|
import { UserStatusErrorResponse } from "../models/native-messaging/encrypted-message-responses/user-status-error-response";
|
||||||
import { ElectronStateService } from "../platform/services/electron-state.service";
|
|
||||||
|
|
||||||
export class EncryptedMessageHandlerService {
|
export class EncryptedMessageHandlerService {
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: ElectronStateService,
|
private stateService: StateService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
|
@ -20,7 +20,9 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
@ -82,6 +84,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private keyConnectorService: KeyConnectorService,
|
private keyConnectorService: KeyConnectorService,
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -265,6 +268,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.policyService.clear(userId),
|
this.policyService.clear(userId),
|
||||||
this.passwordGenerationService.clear(),
|
this.passwordGenerationService.clear(),
|
||||||
this.keyConnectorService.clear(),
|
this.keyConnectorService.clear(),
|
||||||
|
this.biometricStateService.logout(userId as UserId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.searchService.clearIndex();
|
this.searchService.clearIndex();
|
||||||
|
@ -17,6 +17,7 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.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";
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
|
biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
router,
|
router,
|
||||||
@ -66,6 +68,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
deviceTrustCryptoService,
|
deviceTrustCryptoService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
@ -73,6 +74,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||||
protected userVerificationService: UserVerificationService,
|
protected userVerificationService: UserVerificationService,
|
||||||
protected pinCryptoService: PinCryptoServiceAbstraction,
|
protected pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
|
protected biometricStateService: BiometricStateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -117,7 +119,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.stateService.setBiometricPromptCancelled(true);
|
await this.biometricStateService.setPromptCancelled();
|
||||||
const userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
|
const userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
|
||||||
|
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
@ -274,7 +276,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
|
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
|
||||||
await this.stateService.setEverBeenUnlocked(true);
|
await this.stateService.setEverBeenUnlocked(true);
|
||||||
await this.stateService.setBiometricPromptCancelled(false);
|
await this.biometricStateService.resetPromptCancelled();
|
||||||
this.messagingService.send("unlocked");
|
this.messagingService.send("unlocked");
|
||||||
|
|
||||||
if (evaluatePasswordAfterUnlock) {
|
if (evaluatePasswordAfterUnlock) {
|
||||||
|
@ -66,6 +66,7 @@ export class FakeGlobalState<T> implements GlobalState<T> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||||
|
/** Tracks update values resolved by `FakeState.update` */
|
||||||
nextMock = jest.fn<void, [T]>();
|
nextMock = jest.fn<void, [T]>();
|
||||||
|
|
||||||
get state$() {
|
get state$() {
|
||||||
@ -128,6 +129,7 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
|
|||||||
|
|
||||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||||
|
|
||||||
|
/** Tracks update values resolved by `FakeState.update` */
|
||||||
nextMock = jest.fn<void, [T]>();
|
nextMock = jest.fn<void, [T]>();
|
||||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||||
get keyDefinition() {
|
get keyDefinition() {
|
||||||
@ -190,6 +192,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
|
|
||||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||||
|
|
||||||
|
/** Tracks update values resolved by `FakeState.update` */
|
||||||
nextMock = jest.fn<void, [[UserId, T]]>();
|
nextMock = jest.fn<void, [[UserId, T]]>();
|
||||||
|
|
||||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||||
|
@ -169,18 +169,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
* @deprecated For migration purposes only, use setUserKeyBiometric instead
|
* @deprecated For migration purposes only, use setUserKeyBiometric instead
|
||||||
*/
|
*/
|
||||||
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
||||||
/**
|
|
||||||
* Gets a flag for if the biometrics process has been cancelled.
|
|
||||||
* Process reload occurs when biometrics is cancelled, so we store to disk to prevent
|
|
||||||
* it from reprompting and creating a loop.
|
|
||||||
*/
|
|
||||||
getBiometricPromptCancelled: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
/**
|
|
||||||
* Sets a flag for if the biometrics process has been cancelled.
|
|
||||||
* Process reload occurs when biometrics is cancelled, so we store to disk to prevent
|
|
||||||
* it from reprompting and creating a loop.
|
|
||||||
*/
|
|
||||||
setBiometricPromptCancelled: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
|
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
|
||||||
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
|
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
|
||||||
getDecryptedPasswordGenerationHistory: (
|
getDecryptedPasswordGenerationHistory: (
|
||||||
@ -218,8 +206,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
||||||
getDisableAddLoginNotification: (options?: StorageOptions) => Promise<boolean>;
|
getDisableAddLoginNotification: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setDisableAddLoginNotification: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setDisableAddLoginNotification: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
getDisableBadgeCounter: (options?: StorageOptions) => Promise<boolean>;
|
getDisableBadgeCounter: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise<boolean>;
|
getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise<boolean>;
|
||||||
|
@ -8,7 +8,13 @@ import { UserId } from "../../types/guid";
|
|||||||
import { EncryptedString } from "../models/domain/enc-string";
|
import { EncryptedString } from "../models/domain/enc-string";
|
||||||
|
|
||||||
import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service";
|
import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service";
|
||||||
import { ENCRYPTED_CLIENT_KEY_HALF, REQUIRE_PASSWORD_ON_START } from "./biometric.state";
|
import {
|
||||||
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
ENCRYPTED_CLIENT_KEY_HALF,
|
||||||
|
PROMPT_AUTOMATICALLY,
|
||||||
|
PROMPT_CANCELLED,
|
||||||
|
REQUIRE_PASSWORD_ON_START,
|
||||||
|
} from "./biometric.state";
|
||||||
|
|
||||||
describe("BiometricStateService", () => {
|
describe("BiometricStateService", () => {
|
||||||
let sut: BiometricStateService;
|
let sut: BiometricStateService;
|
||||||
@ -96,4 +102,56 @@ describe("BiometricStateService", () => {
|
|||||||
expect(await sut.getRequirePasswordOnStart(userId)).toBe(true);
|
expect(await sut.getRequirePasswordOnStart(userId)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("require password on start callout", () => {
|
||||||
|
it("should be false when not set", async () => {
|
||||||
|
expect(await firstValueFrom(sut.dismissedRequirePasswordOnStartCallout$)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true when set", async () => {
|
||||||
|
await sut.setDismissedRequirePasswordOnStartCallout();
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.dismissedRequirePasswordOnStartCallout$)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update disk state", async () => {
|
||||||
|
await sut.setDismissedRequirePasswordOnStartCallout();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
stateProvider.activeUser.getFake(DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT).nextMock,
|
||||||
|
).toHaveBeenCalledWith([userId, true]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("prompt cancelled", () => {
|
||||||
|
test("observable should be updated", async () => {
|
||||||
|
await sut.setPromptCancelled();
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.promptCancelled$)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update state with set", async () => {
|
||||||
|
await sut.setPromptCancelled();
|
||||||
|
|
||||||
|
const nextMock = stateProvider.activeUser.getFake(PROMPT_CANCELLED).nextMock;
|
||||||
|
expect(nextMock).toHaveBeenCalledWith([userId, true]);
|
||||||
|
expect(nextMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("prompt automatically", () => {
|
||||||
|
test("observable should be updated", async () => {
|
||||||
|
await sut.setPromptAutomatically(true);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.promptAutomatically$)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update state with setPromptAutomatically", async () => {
|
||||||
|
await sut.setPromptAutomatically(true);
|
||||||
|
|
||||||
|
const nextMock = stateProvider.activeUser.getFake(PROMPT_AUTOMATICALLY).nextMock;
|
||||||
|
expect(nextMock).toHaveBeenCalledWith([userId, true]);
|
||||||
|
expect(nextMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,13 @@ import { UserId } from "../../types/guid";
|
|||||||
import { EncryptedString, EncString } from "../models/domain/enc-string";
|
import { EncryptedString, EncString } from "../models/domain/enc-string";
|
||||||
import { ActiveUserState, StateProvider } from "../state";
|
import { ActiveUserState, StateProvider } from "../state";
|
||||||
|
|
||||||
import { ENCRYPTED_CLIENT_KEY_HALF, REQUIRE_PASSWORD_ON_START } from "./biometric.state";
|
import {
|
||||||
|
ENCRYPTED_CLIENT_KEY_HALF,
|
||||||
|
REQUIRE_PASSWORD_ON_START,
|
||||||
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
PROMPT_AUTOMATICALLY,
|
||||||
|
PROMPT_CANCELLED,
|
||||||
|
} from "./biometric.state";
|
||||||
|
|
||||||
export abstract class BiometricStateService {
|
export abstract class BiometricStateService {
|
||||||
/**
|
/**
|
||||||
@ -20,6 +26,24 @@ export abstract class BiometricStateService {
|
|||||||
* tracks the currently active user
|
* tracks the currently active user
|
||||||
*/
|
*/
|
||||||
requirePasswordOnStart$: Observable<boolean>;
|
requirePasswordOnStart$: Observable<boolean>;
|
||||||
|
/**
|
||||||
|
* Indicates the user has been warned about the security implications of using biometrics and, depending on the OS,
|
||||||
|
*
|
||||||
|
* tracks the currently active user.
|
||||||
|
*/
|
||||||
|
dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
|
||||||
|
/**
|
||||||
|
* Whether the user has cancelled the biometric prompt.
|
||||||
|
*
|
||||||
|
* tracks the currently active user
|
||||||
|
*/
|
||||||
|
promptCancelled$: Observable<boolean>;
|
||||||
|
/**
|
||||||
|
* Whether the user has elected to automatically prompt for biometrics.
|
||||||
|
*
|
||||||
|
* tracks the currently active user
|
||||||
|
*/
|
||||||
|
promptAutomatically$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the require password on start state for the currently active user.
|
* Updates the require password on start state for the currently active user.
|
||||||
@ -32,13 +56,38 @@ export abstract class BiometricStateService {
|
|||||||
abstract getEncryptedClientKeyHalf(userId: UserId): Promise<EncString>;
|
abstract getEncryptedClientKeyHalf(userId: UserId): Promise<EncString>;
|
||||||
abstract getRequirePasswordOnStart(userId: UserId): Promise<boolean>;
|
abstract getRequirePasswordOnStart(userId: UserId): Promise<boolean>;
|
||||||
abstract removeEncryptedClientKeyHalf(userId: UserId): Promise<void>;
|
abstract removeEncryptedClientKeyHalf(userId: UserId): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Updates the active user's state to reflect that they've been warned about requiring password on start.
|
||||||
|
*/
|
||||||
|
abstract setDismissedRequirePasswordOnStartCallout(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Updates the active user's state to reflect that they've cancelled the biometric prompt this lock.
|
||||||
|
*/
|
||||||
|
abstract setPromptCancelled(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Resets the active user's state to reflect that they haven't cancelled the biometric prompt this lock.
|
||||||
|
*/
|
||||||
|
abstract resetPromptCancelled(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Updates the currently active user's setting for auto prompting for biometrics on application start and lock
|
||||||
|
* @param prompt Whether or not to prompt for biometrics on application start.
|
||||||
|
*/
|
||||||
|
abstract setPromptAutomatically(prompt: boolean): Promise<void>;
|
||||||
|
|
||||||
|
abstract logout(userId: UserId): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultBiometricStateService implements BiometricStateService {
|
export class DefaultBiometricStateService implements BiometricStateService {
|
||||||
private requirePasswordOnStartState: ActiveUserState<boolean>;
|
private requirePasswordOnStartState: ActiveUserState<boolean>;
|
||||||
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString | undefined>;
|
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString | undefined>;
|
||||||
|
private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>;
|
||||||
|
private promptCancelledState: ActiveUserState<boolean>;
|
||||||
|
private promptAutomaticallyState: ActiveUserState<boolean>;
|
||||||
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
||||||
requirePasswordOnStart$: Observable<boolean>;
|
requirePasswordOnStart$: Observable<boolean>;
|
||||||
|
dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
|
||||||
|
promptCancelled$: Observable<boolean>;
|
||||||
|
promptAutomatically$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {
|
constructor(private stateProvider: StateProvider) {
|
||||||
this.requirePasswordOnStartState = this.stateProvider.getActive(REQUIRE_PASSWORD_ON_START);
|
this.requirePasswordOnStartState = this.stateProvider.getActive(REQUIRE_PASSWORD_ON_START);
|
||||||
@ -50,6 +99,17 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
|||||||
this.encryptedClientKeyHalf$ = this.encryptedClientKeyHalfState.state$.pipe(
|
this.encryptedClientKeyHalf$ = this.encryptedClientKeyHalfState.state$.pipe(
|
||||||
map(encryptedClientKeyHalfToEncString),
|
map(encryptedClientKeyHalfToEncString),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.dismissedRequirePasswordOnStartCalloutState = this.stateProvider.getActive(
|
||||||
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
);
|
||||||
|
this.dismissedRequirePasswordOnStartCallout$ =
|
||||||
|
this.dismissedRequirePasswordOnStartCalloutState.state$.pipe(map((v) => !!v));
|
||||||
|
|
||||||
|
this.promptCancelledState = this.stateProvider.getActive(PROMPT_CANCELLED);
|
||||||
|
this.promptCancelled$ = this.promptCancelledState.state$.pipe(map((v) => !!v));
|
||||||
|
this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY);
|
||||||
|
this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map((v) => !!v));
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRequirePasswordOnStart(value: boolean): Promise<void> {
|
async setRequirePasswordOnStart(value: boolean): Promise<void> {
|
||||||
@ -97,6 +157,25 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
|||||||
|
|
||||||
async logout(userId: UserId): Promise<void> {
|
async logout(userId: UserId): Promise<void> {
|
||||||
await this.stateProvider.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF).update(() => null);
|
await this.stateProvider.getUser(userId, ENCRYPTED_CLIENT_KEY_HALF).update(() => null);
|
||||||
|
await this.stateProvider.getUser(userId, PROMPT_CANCELLED).update(() => null);
|
||||||
|
// Persist auto prompt setting through logout
|
||||||
|
// Persist dismissed require password on start callout through logout
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDismissedRequirePasswordOnStartCallout(): Promise<void> {
|
||||||
|
await this.dismissedRequirePasswordOnStartCalloutState.update(() => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPromptCancelled(): Promise<void> {
|
||||||
|
await this.promptCancelledState.update(() => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPromptCancelled(): Promise<void> {
|
||||||
|
await this.promptCancelledState.update(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPromptAutomatically(prompt: boolean): Promise<void> {
|
||||||
|
await this.promptAutomaticallyState.update(() => prompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,36 @@
|
|||||||
import { ENCRYPTED_CLIENT_KEY_HALF, REQUIRE_PASSWORD_ON_START } from "./biometric.state";
|
import { EncryptedString } from "../models/domain/enc-string";
|
||||||
|
import { KeyDefinition } from "../state";
|
||||||
|
|
||||||
describe("require password on start", () => {
|
import {
|
||||||
const sut = REQUIRE_PASSWORD_ON_START;
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
ENCRYPTED_CLIENT_KEY_HALF,
|
||||||
|
PROMPT_AUTOMATICALLY,
|
||||||
|
PROMPT_CANCELLED,
|
||||||
|
REQUIRE_PASSWORD_ON_START,
|
||||||
|
} from "./biometric.state";
|
||||||
|
|
||||||
it("should deserialize require password on start state", () => {
|
describe.each([
|
||||||
const requirePasswordOnStart = "requirePasswordOnStart";
|
[ENCRYPTED_CLIENT_KEY_HALF, "encryptedClientKeyHalf"],
|
||||||
|
[DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, true],
|
||||||
const result = sut.deserializer(JSON.parse(JSON.stringify(requirePasswordOnStart)));
|
[PROMPT_CANCELLED, true],
|
||||||
|
[PROMPT_AUTOMATICALLY, true],
|
||||||
expect(result).toEqual(requirePasswordOnStart);
|
[REQUIRE_PASSWORD_ON_START, true],
|
||||||
});
|
])(
|
||||||
});
|
"deserializes state %s",
|
||||||
|
(
|
||||||
describe("encrypted client key half", () => {
|
...args: [KeyDefinition<EncryptedString>, EncryptedString] | [KeyDefinition<boolean>, boolean]
|
||||||
const sut = ENCRYPTED_CLIENT_KEY_HALF;
|
) => {
|
||||||
|
it("should deserialize state", () => {
|
||||||
it("should deserialize encrypted client key half state", () => {
|
const [keyDefinition, state] = args;
|
||||||
const encryptedClientKeyHalf = "encryptedClientKeyHalf";
|
// Need to type check to avoid TS error due to array values being unions instead of guaranteed tuple pairs
|
||||||
|
if (typeof state === "boolean") {
|
||||||
const result = sut.deserializer(JSON.parse(JSON.stringify(encryptedClientKeyHalf)));
|
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
||||||
|
expect(deserialized).toEqual(state);
|
||||||
expect(result).toEqual(encryptedClientKeyHalf);
|
return;
|
||||||
});
|
} else {
|
||||||
});
|
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
||||||
|
expect(deserialized).toEqual(state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -28,3 +28,38 @@ export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition<EncryptedString>(
|
|||||||
deserializer: (obj) => obj,
|
deserializer: (obj) => obj,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the user has been warned about the security implications of using biometrics and, depending on the OS,
|
||||||
|
* recommended to require a password on first unlock of an application instance.
|
||||||
|
*/
|
||||||
|
export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new KeyDefinition<boolean>(
|
||||||
|
BIOMETRIC_SETTINGS_DISK,
|
||||||
|
"dismissedBiometricRequirePasswordOnStartCallout",
|
||||||
|
{
|
||||||
|
deserializer: (obj) => obj,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores whether the user has elected to cancel the biometric prompt. This is stored on disk due to process-reload
|
||||||
|
* wiping memory state. We don't want to prompt the user again if they've elected to cancel.
|
||||||
|
*/
|
||||||
|
export const PROMPT_CANCELLED = new KeyDefinition<boolean>(
|
||||||
|
BIOMETRIC_SETTINGS_DISK,
|
||||||
|
"promptCancelled",
|
||||||
|
{
|
||||||
|
deserializer: (obj) => obj,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores whether the user has elected to automatically prompt for biometric unlock on application start.
|
||||||
|
*/
|
||||||
|
export const PROMPT_AUTOMATICALLY = new KeyDefinition<boolean>(
|
||||||
|
BIOMETRIC_SETTINGS_DISK,
|
||||||
|
"promptAutomatically",
|
||||||
|
{
|
||||||
|
deserializer: (obj) => obj,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -203,7 +203,6 @@ export class AccountSettings {
|
|||||||
biometricUnlock?: boolean;
|
biometricUnlock?: boolean;
|
||||||
clearClipboard?: number;
|
clearClipboard?: number;
|
||||||
defaultUriMatch?: UriMatchType;
|
defaultUriMatch?: UriMatchType;
|
||||||
disableAutoBiometricsPrompt?: boolean;
|
|
||||||
disableBadgeCounter?: boolean;
|
disableBadgeCounter?: boolean;
|
||||||
disableGa?: boolean;
|
disableGa?: boolean;
|
||||||
dontShowCardsCurrentTab?: boolean;
|
dontShowCardsCurrentTab?: boolean;
|
||||||
@ -227,7 +226,6 @@ export class AccountSettings {
|
|||||||
avatarColor?: string;
|
avatarColor?: string;
|
||||||
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
||||||
trustDeviceChoiceForDecryption?: boolean;
|
trustDeviceChoiceForDecryption?: boolean;
|
||||||
biometricPromptCancelled?: boolean;
|
|
||||||
|
|
||||||
/** @deprecated July 2023, left for migration purposes*/
|
/** @deprecated July 2023, left for migration purposes*/
|
||||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||||
|
@ -775,24 +775,6 @@ export class StateService<
|
|||||||
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
|
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBiometricPromptCancelled(options?: StorageOptions): Promise<boolean> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
return account?.settings?.biometricPromptCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setBiometricPromptCancelled(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.biometricPromptCancelled = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
|
@withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
|
||||||
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
|
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
|
||||||
return (
|
return (
|
||||||
@ -928,24 +910,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDisableAutoBiometricsPrompt(options?: StorageOptions): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
|
||||||
?.settings?.disableAutoBiometricsPrompt ?? false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDisableAutoBiometricsPrompt(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.disableAutoBiometricsPrompt = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDisableBadgeCounter(options?: StorageOptions): Promise<boolean> {
|
async getDisableBadgeCounter(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||||
|
@ -17,6 +17,7 @@ import { RequirePasswordOnStartMigrator } from "./migrations/19-migrate-require-
|
|||||||
import { PrivateKeyMigrator } from "./migrations/20-move-private-key-to-state-providers";
|
import { PrivateKeyMigrator } from "./migrations/20-move-private-key-to-state-providers";
|
||||||
import { CollectionMigrator } from "./migrations/21-move-collections-state-to-state-provider";
|
import { CollectionMigrator } from "./migrations/21-move-collections-state-to-state-provider";
|
||||||
import { CollapsedGroupingsMigrator } from "./migrations/22-move-collapsed-groupings-to-state-provider";
|
import { CollapsedGroupingsMigrator } from "./migrations/22-move-collapsed-groupings-to-state-provider";
|
||||||
|
import { MoveBiometricPromptsToStateProviders } from "./migrations/23-move-biometric-prompts-to-state-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";
|
||||||
@ -27,7 +28,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 = 22;
|
export const CURRENT_VERSION = 23;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@ -52,7 +53,8 @@ export function createMigrationBuilder() {
|
|||||||
.with(RequirePasswordOnStartMigrator, 18, 19)
|
.with(RequirePasswordOnStartMigrator, 18, 19)
|
||||||
.with(PrivateKeyMigrator, 19, 20)
|
.with(PrivateKeyMigrator, 19, 20)
|
||||||
.with(CollectionMigrator, 20, 21)
|
.with(CollectionMigrator, 20, 21)
|
||||||
.with(CollapsedGroupingsMigrator, 21, CURRENT_VERSION);
|
.with(CollapsedGroupingsMigrator, 21, 22)
|
||||||
|
.with(MoveBiometricPromptsToStateProviders, 22, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
import { MockProxy, any } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MoveBiometricPromptsToStateProviders,
|
||||||
|
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
PROMPT_AUTOMATICALLY,
|
||||||
|
} from "./23-move-biometric-prompts-to-state-providers";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||||
|
"user-1": {
|
||||||
|
settings: {
|
||||||
|
disableAutoBiometricsPrompt: false,
|
||||||
|
dismissedBiometricRequirePasswordOnStartCallout: true,
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
"user_user-1_biometricSettings_dismissedBiometricRequirePasswordOnStartCallout": true,
|
||||||
|
"user_user-1_biometricSettings_promptAutomatically": "false",
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||||
|
"user-1": {
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("MoveBiometricPromptsToStateProviders migrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: MoveBiometricPromptsToStateProviders;
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 22);
|
||||||
|
sut = new MoveBiometricPromptsToStateProviders(22, 23);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove biometricUnlock, dismissedBiometricRequirePasswordOnStartCallout, and biometricEncryptionClientKeyHalf from all accounts", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set dismissedBiometricRequirePasswordOnStartCallout value for account that have it", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"user-1",
|
||||||
|
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call extra setToUser", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 23);
|
||||||
|
sut = new MoveBiometricPromptsToStateProviders(22, 23);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT, PROMPT_AUTOMATICALLY])(
|
||||||
|
"should null out new values %s",
|
||||||
|
async (keyDefinition) => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinition, 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", {
|
||||||
|
settings: {
|
||||||
|
disableAutoBiometricsPrompt: false,
|
||||||
|
dismissedBiometricRequirePasswordOnStartCallout: true,
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
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,99 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
settings?: {
|
||||||
|
disableAutoBiometricsPrompt?: boolean;
|
||||||
|
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// prompt cancelled is refreshed on every app start/quit/unlock, so we don't need to migrate it
|
||||||
|
|
||||||
|
export const DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT: KeyDefinitionLike = {
|
||||||
|
key: "dismissedBiometricRequirePasswordOnStartCallout",
|
||||||
|
stateDefinition: { name: "biometricSettings" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROMPT_AUTOMATICALLY: KeyDefinitionLike = {
|
||||||
|
key: "promptAutomatically",
|
||||||
|
stateDefinition: { name: "biometricSettings" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MoveBiometricPromptsToStateProviders extends Migrator<22, 23> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
legacyAccounts.map(async ({ userId, account }) => {
|
||||||
|
if (account == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Move account data
|
||||||
|
|
||||||
|
if (account?.settings?.dismissedBiometricRequirePasswordOnStartCallout != null) {
|
||||||
|
await helper.setToUser(
|
||||||
|
userId,
|
||||||
|
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
account.settings.dismissedBiometricRequirePasswordOnStartCallout,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account?.settings?.disableAutoBiometricsPrompt != null) {
|
||||||
|
await helper.setToUser(
|
||||||
|
userId,
|
||||||
|
PROMPT_AUTOMATICALLY,
|
||||||
|
!account.settings.disableAutoBiometricsPrompt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old account data
|
||||||
|
delete account?.settings?.dismissedBiometricRequirePasswordOnStartCallout;
|
||||||
|
delete account?.settings?.disableAutoBiometricsPrompt;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||||
|
let updatedAccount = false;
|
||||||
|
|
||||||
|
const userDismissed = await helper.getFromUser<boolean>(
|
||||||
|
userId,
|
||||||
|
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userDismissed) {
|
||||||
|
account ??= {};
|
||||||
|
account.settings ??= {};
|
||||||
|
|
||||||
|
updatedAccount = true;
|
||||||
|
account.settings.dismissedBiometricRequirePasswordOnStartCallout = userDismissed;
|
||||||
|
await helper.setToUser(userId, DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPromptAutomatically = await helper.getFromUser<boolean>(
|
||||||
|
userId,
|
||||||
|
PROMPT_AUTOMATICALLY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userPromptAutomatically != null) {
|
||||||
|
account ??= {};
|
||||||
|
account.settings ??= {};
|
||||||
|
|
||||||
|
updatedAccount = true;
|
||||||
|
account.settings.disableAutoBiometricsPrompt = !userPromptAutomatically;
|
||||||
|
await helper.setToUser(userId, PROMPT_AUTOMATICALLY, 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