mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
Ps/pm 5537/move biometric unlock to state providers (#8099)
* Establish biometric unlock enabled in state providers * Use biometric state service for biometric state values * Migrate biometricUnlock * Fixup Dependencies * linter and import fixes * Fix injection * Fix merge * Use boolean constructor as mapper * Conform to documented test naming conventions * Commit documentation suggestion Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Fix merge commit * Fix test names --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
@@ -453,6 +453,7 @@ export default class MainBackground {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
|
this.biometricStateService,
|
||||||
);
|
);
|
||||||
this.tokenService = new TokenService(this.stateService);
|
this.tokenService = new TokenService(this.stateService);
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
this.appIdService = new AppIdService(this.storageService);
|
||||||
@@ -619,6 +620,7 @@ export default class MainBackground {
|
|||||||
this.tokenService,
|
this.tokenService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
this.biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.pinCryptoService = new PinCryptoService(
|
this.pinCryptoService = new PinCryptoService(
|
||||||
@@ -833,6 +835,7 @@ export default class MainBackground {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.authService,
|
this.authService,
|
||||||
|
this.biometricStateService,
|
||||||
);
|
);
|
||||||
this.commandsBackground = new CommandsBackground(
|
this.commandsBackground = new CommandsBackground(
|
||||||
this,
|
this,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
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 { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
@@ -8,6 +10,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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
@@ -79,6 +82,7 @@ export class NativeMessagingBackground {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {
|
) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@@ -321,10 +325,10 @@ export class NativeMessagingBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for initial setup of biometric unlock
|
// Check for initial setup of biometric unlock
|
||||||
const enabled = await this.stateService.getBiometricUnlock();
|
const enabled = await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$);
|
||||||
if (enabled === null || enabled === false) {
|
if (enabled === null || enabled === false) {
|
||||||
if (message.response === "unlocked") {
|
if (message.response === "unlocked") {
|
||||||
await this.stateService.setBiometricUnlock(true);
|
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import {
|
|||||||
tokenServiceFactory,
|
tokenServiceFactory,
|
||||||
TokenServiceInitOptions,
|
TokenServiceInitOptions,
|
||||||
} from "../../auth/background/service-factories/token-service.factory";
|
} from "../../auth/background/service-factories/token-service.factory";
|
||||||
|
import {
|
||||||
|
biometricStateServiceFactory,
|
||||||
|
BiometricStateServiceInitOptions,
|
||||||
|
} from "../../platform/background/service-factories/biometric-state-service.factory";
|
||||||
import {
|
import {
|
||||||
CryptoServiceInitOptions,
|
CryptoServiceInitOptions,
|
||||||
cryptoServiceFactory,
|
cryptoServiceFactory,
|
||||||
@@ -29,7 +33,8 @@ export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsService
|
|||||||
CryptoServiceInitOptions &
|
CryptoServiceInitOptions &
|
||||||
TokenServiceInitOptions &
|
TokenServiceInitOptions &
|
||||||
PolicyServiceInitOptions &
|
PolicyServiceInitOptions &
|
||||||
StateServiceInitOptions;
|
StateServiceInitOptions &
|
||||||
|
BiometricStateServiceInitOptions;
|
||||||
|
|
||||||
export function vaultTimeoutSettingsServiceFactory(
|
export function vaultTimeoutSettingsServiceFactory(
|
||||||
cache: { vaultTimeoutSettingsService?: AbstractVaultTimeoutSettingsService } & CachedServices,
|
cache: { vaultTimeoutSettingsService?: AbstractVaultTimeoutSettingsService } & CachedServices,
|
||||||
@@ -45,6 +50,7 @@ export function vaultTimeoutSettingsServiceFactory(
|
|||||||
await tokenServiceFactory(cache, opts),
|
await tokenServiceFactory(cache, opts),
|
||||||
await policyServiceFactory(cache, opts),
|
await policyServiceFactory(cache, opts),
|
||||||
await stateServiceFactory(cache, opts),
|
await stateServiceFactory(cache, opts),
|
||||||
|
await biometricStateServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
BiometricStateService,
|
||||||
|
DefaultBiometricStateService,
|
||||||
|
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
|
|
||||||
|
import { FactoryOptions, CachedServices, factory } from "./factory-options";
|
||||||
|
import { StateProviderInitOptions, stateProviderFactory } from "./state-provider.factory";
|
||||||
|
|
||||||
|
type BiometricStateServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
|
export type BiometricStateServiceInitOptions = BiometricStateServiceFactoryOptions &
|
||||||
|
StateProviderInitOptions;
|
||||||
|
|
||||||
|
export function biometricStateServiceFactory(
|
||||||
|
cache: { biometricStateService?: BiometricStateService } & CachedServices,
|
||||||
|
opts: BiometricStateServiceInitOptions,
|
||||||
|
): Promise<BiometricStateService> {
|
||||||
|
return factory(
|
||||||
|
cache,
|
||||||
|
"biometricStateService",
|
||||||
|
opts,
|
||||||
|
async () => new DefaultBiometricStateService(await stateProviderFactory(cache, opts)),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "../../background/service-factories/log-service.factory";
|
} from "../../background/service-factories/log-service.factory";
|
||||||
import { BrowserCryptoService } from "../../services/browser-crypto.service";
|
import { BrowserCryptoService } from "../../services/browser-crypto.service";
|
||||||
|
|
||||||
|
import { biometricStateServiceFactory } from "./biometric-state-service.factory";
|
||||||
import {
|
import {
|
||||||
cryptoFunctionServiceFactory,
|
cryptoFunctionServiceFactory,
|
||||||
CryptoFunctionServiceInitOptions,
|
CryptoFunctionServiceInitOptions,
|
||||||
@@ -60,6 +61,7 @@ export function cryptoServiceFactory(
|
|||||||
await stateServiceFactory(cache, opts),
|
await stateServiceFactory(cache, opts),
|
||||||
await accountServiceFactory(cache, opts),
|
await accountServiceFactory(cache, opts),
|
||||||
await stateProviderFactory(cache, opts),
|
await stateProviderFactory(cache, opts),
|
||||||
|
await biometricStateServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,50 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||||
import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state";
|
import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
export class BrowserCryptoService extends CryptoService {
|
export class BrowserCryptoService extends CryptoService {
|
||||||
|
constructor(
|
||||||
|
keyGenerationService: KeyGenerationService,
|
||||||
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
encryptService: EncryptService,
|
||||||
|
platformUtilService: PlatformUtilsService,
|
||||||
|
logService: LogService,
|
||||||
|
stateService: StateService,
|
||||||
|
accountService: AccountService,
|
||||||
|
stateProvider: StateProvider,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
keyGenerationService,
|
||||||
|
cryptoFunctionService,
|
||||||
|
encryptService,
|
||||||
|
platformUtilService,
|
||||||
|
logService,
|
||||||
|
stateService,
|
||||||
|
accountService,
|
||||||
|
stateProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
return await this.stateService.getBiometricUnlock({ userId: userId });
|
const biometricUnlockPromise =
|
||||||
|
userId == null
|
||||||
|
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||||
|
: this.biometricStateService.getBiometricUnlockEnabled(userId);
|
||||||
|
return await biometricUnlockPromise;
|
||||||
}
|
}
|
||||||
return super.hasUserKeyStored(keySuffix, userId);
|
return super.hasUserKeyStored(keySuffix, userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
await this.stateService.setBiometricUnlock(null);
|
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
||||||
await this.stateService.setBiometricFingerprintValidated(false);
|
await this.stateService.setBiometricFingerprintValidated(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ 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,
|
||||||
|
DefaultBiometricStateService,
|
||||||
|
} 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";
|
||||||
@@ -204,6 +208,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;
|
||||||
@@ -490,11 +495,14 @@ export class Main {
|
|||||||
const lockedCallback = async (userId?: string) =>
|
const lockedCallback = async (userId?: string) =>
|
||||||
await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto);
|
await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto);
|
||||||
|
|
||||||
|
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||||
|
|
||||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.tokenService,
|
this.tokenService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
this.biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.pinCryptoService = new PinCryptoService(
|
this.pinCryptoService = new PinCryptoService(
|
||||||
|
|||||||
@@ -445,12 +445,12 @@ export class SettingsComponent implements OnInit {
|
|||||||
try {
|
try {
|
||||||
if (!enabled || !this.supportsBiometric) {
|
if (!enabled || !this.supportsBiometric) {
|
||||||
this.form.controls.biometric.setValue(false, { emitEvent: false });
|
this.form.controls.biometric.setValue(false, { emitEvent: false });
|
||||||
await this.stateService.setBiometricUnlock(null);
|
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
||||||
await this.cryptoService.refreshAdditionalKeys();
|
await this.cryptoService.refreshAdditionalKeys();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.stateService.setBiometricUnlock(true);
|
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||||
if (this.isWindows) {
|
if (this.isWindows) {
|
||||||
// Recommended settings for Windows Hello
|
// Recommended settings for Windows Hello
|
||||||
this.form.controls.requirePasswordOnStart.setValue(true);
|
this.form.controls.requirePasswordOnStart.setValue(true);
|
||||||
@@ -465,7 +465,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
const biometricSet = await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric);
|
const biometricSet = await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric);
|
||||||
this.form.controls.biometric.setValue(biometricSet, { emitEvent: false });
|
this.form.controls.biometric.setValue(biometricSet, { emitEvent: false });
|
||||||
if (!biometricSet) {
|
if (!biometricSet) {
|
||||||
await this.stateService.setBiometricUnlock(null);
|
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.messagingService.send("redrawMenu");
|
this.messagingService.send("redrawMenu");
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.stateService.getBiometricUnlock()) {
|
if (await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)) {
|
||||||
const response = await this.dialogService.openSimpleDialog({
|
const response = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "windowsBiometricUpdateWarningTitle" },
|
title: { key: "windowsBiometricUpdateWarningTitle" },
|
||||||
content: { key: "windowsBiometricUpdateWarning" },
|
content: { key: "windowsBiometricUpdateWarning" },
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ describe("electronCryptoService", () => {
|
|||||||
encClientKeyHalf.decrypt = jest.fn().mockResolvedValue(decClientKeyHalf);
|
encClientKeyHalf.decrypt = jest.fn().mockResolvedValue(decClientKeyHalf);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
|
it("sets a Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
|
||||||
stateService.getBiometricUnlock.mockResolvedValue(true);
|
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
|
||||||
platformUtilService.supportsSecureStorage.mockReturnValue(true);
|
platformUtilService.supportsSecureStorage.mockReturnValue(true);
|
||||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
||||||
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(encClientKeyHalf);
|
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(encClientKeyHalf);
|
||||||
@@ -90,7 +90,7 @@ describe("electronCryptoService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("clears the Biometric key if getBiometricUnlock is false or the platform does not support secure storage", async () => {
|
it("clears the Biometric key if getBiometricUnlock is false or the platform does not support secure storage", async () => {
|
||||||
stateService.getBiometricUnlock.mockResolvedValue(true);
|
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
|
||||||
platformUtilService.supportsSecureStorage.mockReturnValue(false);
|
platformUtilService.supportsSecureStorage.mockReturnValue(false);
|
||||||
|
|
||||||
await sut.setUserKey(mockUserKey, mockUserId);
|
await sut.setUserKey(mockUserKey, mockUserId);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
@@ -97,7 +99,11 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
|
|
||||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId });
|
const biometricUnlockPromise =
|
||||||
|
userId == null
|
||||||
|
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||||
|
: this.biometricStateService.getBiometricUnlockEnabled(userId);
|
||||||
|
const biometricUnlock = await biometricUnlockPromise;
|
||||||
return biometricUnlock && this.platformUtilService.supportsSecureStorage();
|
return biometricUnlock && this.platformUtilService.supportsSecureStorage();
|
||||||
}
|
}
|
||||||
return await super.shouldStoreKey(keySuffix, userId);
|
return await super.shouldStoreKey(keySuffix, userId);
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ 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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
|
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
|
||||||
@@ -36,6 +38,7 @@ export class NativeMessagingService {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
private nativeMessageHandler: NativeMessageHandlerService,
|
private nativeMessageHandler: NativeMessageHandlerService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
@@ -136,7 +139,11 @@ export class NativeMessagingService {
|
|||||||
return this.send({ command: "biometricUnlock", response: "not supported" }, appId);
|
return this.send({ command: "biometricUnlock", response: "not supported" }, appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await this.stateService.getBiometricUnlock({ userId: message.userId }))) {
|
const biometricUnlockPromise =
|
||||||
|
message.userId == null
|
||||||
|
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||||
|
: this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
||||||
|
if (!(await biometricUnlockPromise)) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
||||||
|
|||||||
@@ -535,6 +535,7 @@ import { ModalService } from "./modal.service";
|
|||||||
TokenServiceAbstraction,
|
TokenServiceAbstraction,
|
||||||
PolicyServiceAbstraction,
|
PolicyServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
|
BiometricStateService,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,8 +74,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getBiometricUnlock: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
|
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
|
||||||
getHasPremiumPersonally: (options?: StorageOptions) => Promise<boolean>;
|
getHasPremiumPersonally: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setHasPremiumPersonally: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setHasPremiumPersonally: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { EncryptedString } from "../models/domain/enc-string";
|
|||||||
|
|
||||||
import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service";
|
import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service";
|
||||||
import {
|
import {
|
||||||
|
BIOMETRIC_UNLOCK_ENABLED,
|
||||||
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
ENCRYPTED_CLIENT_KEY_HALF,
|
ENCRYPTED_CLIENT_KEY_HALF,
|
||||||
PROMPT_AUTOMATICALLY,
|
PROMPT_AUTOMATICALLY,
|
||||||
@@ -35,33 +36,39 @@ describe("BiometricStateService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("requirePasswordOnStart$", () => {
|
describe("requirePasswordOnStart$", () => {
|
||||||
it("should track the requirePasswordOnStart state", async () => {
|
it("emits when the require password on start state changes", async () => {
|
||||||
const state = stateProvider.activeUser.getFake(REQUIRE_PASSWORD_ON_START);
|
const state = stateProvider.activeUser.getFake(REQUIRE_PASSWORD_ON_START);
|
||||||
state.nextState(undefined);
|
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(false);
|
|
||||||
|
|
||||||
state.nextState(true);
|
state.nextState(true);
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(true);
|
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits false when the require password on start state is undefined", async () => {
|
||||||
|
const state = stateProvider.activeUser.getFake(REQUIRE_PASSWORD_ON_START);
|
||||||
|
state.nextState(undefined);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("encryptedClientKeyHalf$", () => {
|
describe("encryptedClientKeyHalf$", () => {
|
||||||
it("should track the encryptedClientKeyHalf state", async () => {
|
it("emits when the encryptedClientKeyHalf state changes", async () => {
|
||||||
const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF);
|
const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF);
|
||||||
state.nextState(undefined);
|
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toBe(null);
|
|
||||||
|
|
||||||
state.nextState(encryptedClientKeyHalf);
|
state.nextState(encryptedClientKeyHalf);
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf);
|
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits false when the encryptedClientKeyHalf state is undefined", async () => {
|
||||||
|
const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF);
|
||||||
|
state.nextState(undefined);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toBe(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setEncryptedClientKeyHalf", () => {
|
describe("setEncryptedClientKeyHalf", () => {
|
||||||
it("should update the encryptedClientKeyHalf$", async () => {
|
it("updates encryptedClientKeyHalf$", async () => {
|
||||||
await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
|
await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf);
|
expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf);
|
||||||
@@ -69,13 +76,13 @@ describe("BiometricStateService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("setRequirePasswordOnStart", () => {
|
describe("setRequirePasswordOnStart", () => {
|
||||||
it("should update the requirePasswordOnStart$", async () => {
|
it("updates the requirePasswordOnStart$", async () => {
|
||||||
await sut.setRequirePasswordOnStart(true);
|
await sut.setRequirePasswordOnStart(true);
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(true);
|
expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove the encryptedClientKeyHalf if the value is false", async () => {
|
it("removes the encryptedClientKeyHalf when the set value is false", async () => {
|
||||||
await sut.setEncryptedClientKeyHalf(encClientKeyHalf, userId);
|
await sut.setEncryptedClientKeyHalf(encClientKeyHalf, userId);
|
||||||
await sut.setRequirePasswordOnStart(false);
|
await sut.setRequirePasswordOnStart(false);
|
||||||
|
|
||||||
@@ -87,7 +94,7 @@ describe("BiometricStateService", () => {
|
|||||||
expect(keyHalfState.nextMock).toHaveBeenCalledWith(null);
|
expect(keyHalfState.nextMock).toHaveBeenCalledWith(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not remove the encryptedClientKeyHalf if the value is true", async () => {
|
it("does not remove the encryptedClientKeyHalf when the value is true", async () => {
|
||||||
await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
|
await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
|
||||||
await sut.setRequirePasswordOnStart(true);
|
await sut.setRequirePasswordOnStart(true);
|
||||||
|
|
||||||
@@ -96,7 +103,7 @@ describe("BiometricStateService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getRequirePasswordOnStart", () => {
|
describe("getRequirePasswordOnStart", () => {
|
||||||
it("should return the requirePasswordOnStart value", async () => {
|
it("returns the requirePasswordOnStart state value", async () => {
|
||||||
stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START.key, true);
|
stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START.key, true);
|
||||||
|
|
||||||
expect(await sut.getRequirePasswordOnStart(userId)).toBe(true);
|
expect(await sut.getRequirePasswordOnStart(userId)).toBe(true);
|
||||||
@@ -104,17 +111,17 @@ describe("BiometricStateService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("require password on start callout", () => {
|
describe("require password on start callout", () => {
|
||||||
it("should be false when not set", async () => {
|
it("is false when not set", async () => {
|
||||||
expect(await firstValueFrom(sut.dismissedRequirePasswordOnStartCallout$)).toBe(false);
|
expect(await firstValueFrom(sut.dismissedRequirePasswordOnStartCallout$)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true when set", async () => {
|
it("is true when set", async () => {
|
||||||
await sut.setDismissedRequirePasswordOnStartCallout();
|
await sut.setDismissedRequirePasswordOnStartCallout();
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.dismissedRequirePasswordOnStartCallout$)).toBe(true);
|
expect(await firstValueFrom(sut.dismissedRequirePasswordOnStartCallout$)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update disk state", async () => {
|
it("updates disk state when called", async () => {
|
||||||
await sut.setDismissedRequirePasswordOnStartCallout();
|
await sut.setDismissedRequirePasswordOnStartCallout();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -123,14 +130,14 @@ describe("BiometricStateService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("prompt cancelled", () => {
|
describe("setPromptCancelled", () => {
|
||||||
test("observable should be updated", async () => {
|
test("observable is updated", async () => {
|
||||||
await sut.setPromptCancelled();
|
await sut.setPromptCancelled();
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.promptCancelled$)).toBe(true);
|
expect(await firstValueFrom(sut.promptCancelled$)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update state with set", async () => {
|
it("updates state", async () => {
|
||||||
await sut.setPromptCancelled();
|
await sut.setPromptCancelled();
|
||||||
|
|
||||||
const nextMock = stateProvider.activeUser.getFake(PROMPT_CANCELLED).nextMock;
|
const nextMock = stateProvider.activeUser.getFake(PROMPT_CANCELLED).nextMock;
|
||||||
@@ -139,14 +146,14 @@ describe("BiometricStateService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("prompt automatically", () => {
|
describe("setPromptAutomatically", () => {
|
||||||
test("observable should be updated", async () => {
|
test("observable is updated", async () => {
|
||||||
await sut.setPromptAutomatically(true);
|
await sut.setPromptAutomatically(true);
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.promptAutomatically$)).toBe(true);
|
expect(await firstValueFrom(sut.promptAutomatically$)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update state with setPromptAutomatically", async () => {
|
it("updates state", async () => {
|
||||||
await sut.setPromptAutomatically(true);
|
await sut.setPromptAutomatically(true);
|
||||||
|
|
||||||
const nextMock = stateProvider.activeUser.getFake(PROMPT_AUTOMATICALLY).nextMock;
|
const nextMock = stateProvider.activeUser.getFake(PROMPT_AUTOMATICALLY).nextMock;
|
||||||
@@ -154,4 +161,50 @@ describe("BiometricStateService", () => {
|
|||||||
expect(nextMock).toHaveBeenCalledTimes(1);
|
expect(nextMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("biometricUnlockEnabled$", () => {
|
||||||
|
it("emits when biometricUnlockEnabled state is updated", async () => {
|
||||||
|
const state = stateProvider.activeUser.getFake(BIOMETRIC_UNLOCK_ENABLED);
|
||||||
|
state.nextState(true);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.biometricUnlockEnabled$)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits false when biometricUnlockEnabled state is undefined", async () => {
|
||||||
|
const state = stateProvider.activeUser.getFake(BIOMETRIC_UNLOCK_ENABLED);
|
||||||
|
state.nextState(undefined);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.biometricUnlockEnabled$)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setBiometricUnlockEnabled", () => {
|
||||||
|
it("updates biometricUnlockEnabled$", async () => {
|
||||||
|
await sut.setBiometricUnlockEnabled(true);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.biometricUnlockEnabled$)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates state", async () => {
|
||||||
|
await sut.setBiometricUnlockEnabled(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
stateProvider.activeUser.getFake(BIOMETRIC_UNLOCK_ENABLED).nextMock,
|
||||||
|
).toHaveBeenCalledWith([userId, true]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBiometricUnlockEnabled", () => {
|
||||||
|
it("returns biometricUnlockEnabled state for the given user", async () => {
|
||||||
|
stateProvider.singleUser.getFake(userId, BIOMETRIC_UNLOCK_ENABLED).nextState(true);
|
||||||
|
|
||||||
|
expect(await sut.getBiometricUnlockEnabled(userId)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the state is not set", async () => {
|
||||||
|
stateProvider.singleUser.getFake(userId, BIOMETRIC_UNLOCK_ENABLED).nextState(undefined);
|
||||||
|
|
||||||
|
expect(await sut.getBiometricUnlockEnabled(userId)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EncryptedString, EncString } from "../models/domain/enc-string";
|
|||||||
import { ActiveUserState, StateProvider } from "../state";
|
import { ActiveUserState, StateProvider } from "../state";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BIOMETRIC_UNLOCK_ENABLED,
|
||||||
ENCRYPTED_CLIENT_KEY_HALF,
|
ENCRYPTED_CLIENT_KEY_HALF,
|
||||||
REQUIRE_PASSWORD_ON_START,
|
REQUIRE_PASSWORD_ON_START,
|
||||||
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
@@ -13,6 +14,10 @@ import {
|
|||||||
} from "./biometric.state";
|
} from "./biometric.state";
|
||||||
|
|
||||||
export abstract class BiometricStateService {
|
export abstract class BiometricStateService {
|
||||||
|
/**
|
||||||
|
* `true` if the currently active user has elected to store a biometric key to unlock their vault.
|
||||||
|
*/
|
||||||
|
biometricUnlockEnabled$: Observable<boolean>; // used to be biometricUnlock
|
||||||
/**
|
/**
|
||||||
* If the user has elected to require a password on first unlock of an application instance, this key will store the
|
* If the user has elected to require a password on first unlock of an application instance, this key will store the
|
||||||
* encrypted client key half used to unlock the vault.
|
* encrypted client key half used to unlock the vault.
|
||||||
@@ -52,6 +57,16 @@ export abstract class BiometricStateService {
|
|||||||
* @param value whether or not a password is required on first unlock after opening the application
|
* @param value whether or not a password is required on first unlock after opening the application
|
||||||
*/
|
*/
|
||||||
abstract setRequirePasswordOnStart(value: boolean): Promise<void>;
|
abstract setRequirePasswordOnStart(value: boolean): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Updates the biometric unlock enabled state for the currently active user.
|
||||||
|
* @param enabled whether or not to store a biometric key to unlock the vault
|
||||||
|
*/
|
||||||
|
abstract setBiometricUnlockEnabled(enabled: boolean): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Gets the biometric unlock enabled state for the given user.
|
||||||
|
* @param userId user Id to check
|
||||||
|
*/
|
||||||
|
abstract getBiometricUnlockEnabled(userId: UserId): Promise<boolean>;
|
||||||
abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString, userId?: UserId): Promise<void>;
|
abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString, userId?: UserId): Promise<void>;
|
||||||
abstract getEncryptedClientKeyHalf(userId: UserId): Promise<EncString>;
|
abstract getEncryptedClientKeyHalf(userId: UserId): Promise<EncString>;
|
||||||
abstract getRequirePasswordOnStart(userId: UserId): Promise<boolean>;
|
abstract getRequirePasswordOnStart(userId: UserId): Promise<boolean>;
|
||||||
@@ -78,11 +93,13 @@ export abstract class BiometricStateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultBiometricStateService implements BiometricStateService {
|
export class DefaultBiometricStateService implements BiometricStateService {
|
||||||
|
private biometricUnlockEnabledState: ActiveUserState<boolean>;
|
||||||
private requirePasswordOnStartState: ActiveUserState<boolean>;
|
private requirePasswordOnStartState: ActiveUserState<boolean>;
|
||||||
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString | undefined>;
|
private encryptedClientKeyHalfState: ActiveUserState<EncryptedString | undefined>;
|
||||||
private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>;
|
private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>;
|
||||||
private promptCancelledState: ActiveUserState<boolean>;
|
private promptCancelledState: ActiveUserState<boolean>;
|
||||||
private promptAutomaticallyState: ActiveUserState<boolean>;
|
private promptAutomaticallyState: ActiveUserState<boolean>;
|
||||||
|
biometricUnlockEnabled$: Observable<boolean>;
|
||||||
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
||||||
requirePasswordOnStart$: Observable<boolean>;
|
requirePasswordOnStart$: Observable<boolean>;
|
||||||
dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
|
dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
|
||||||
@@ -90,6 +107,9 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
|||||||
promptAutomatically$: Observable<boolean>;
|
promptAutomatically$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {
|
constructor(private stateProvider: StateProvider) {
|
||||||
|
this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED);
|
||||||
|
this.biometricUnlockEnabled$ = this.biometricUnlockEnabledState.state$.pipe(map(Boolean));
|
||||||
|
|
||||||
this.requirePasswordOnStartState = this.stateProvider.getActive(REQUIRE_PASSWORD_ON_START);
|
this.requirePasswordOnStartState = this.stateProvider.getActive(REQUIRE_PASSWORD_ON_START);
|
||||||
this.requirePasswordOnStart$ = this.requirePasswordOnStartState.state$.pipe(
|
this.requirePasswordOnStart$ = this.requirePasswordOnStartState.state$.pipe(
|
||||||
map((value) => !!value),
|
map((value) => !!value),
|
||||||
@@ -104,12 +124,22 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
|||||||
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
);
|
);
|
||||||
this.dismissedRequirePasswordOnStartCallout$ =
|
this.dismissedRequirePasswordOnStartCallout$ =
|
||||||
this.dismissedRequirePasswordOnStartCalloutState.state$.pipe(map((v) => !!v));
|
this.dismissedRequirePasswordOnStartCalloutState.state$.pipe(map(Boolean));
|
||||||
|
|
||||||
this.promptCancelledState = this.stateProvider.getActive(PROMPT_CANCELLED);
|
this.promptCancelledState = this.stateProvider.getActive(PROMPT_CANCELLED);
|
||||||
this.promptCancelled$ = this.promptCancelledState.state$.pipe(map((v) => !!v));
|
this.promptCancelled$ = this.promptCancelledState.state$.pipe(map(Boolean));
|
||||||
this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY);
|
this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY);
|
||||||
this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map((v) => !!v));
|
this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBiometricUnlockEnabled(enabled: boolean): Promise<void> {
|
||||||
|
await this.biometricUnlockEnabledState.update(() => enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBiometricUnlockEnabled(userId: UserId): Promise<boolean> {
|
||||||
|
return await firstValueFrom(
|
||||||
|
this.stateProvider.getUser(userId, BIOMETRIC_UNLOCK_ENABLED).state$.pipe(map(Boolean)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRequirePasswordOnStart(value: boolean): Promise<void> {
|
async setRequirePasswordOnStart(value: boolean): Promise<void> {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { EncryptedString } from "../models/domain/enc-string";
|
|||||||
import { KeyDefinition } from "../state";
|
import { KeyDefinition } from "../state";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BIOMETRIC_UNLOCK_ENABLED,
|
||||||
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||||
ENCRYPTED_CLIENT_KEY_HALF,
|
ENCRYPTED_CLIENT_KEY_HALF,
|
||||||
PROMPT_AUTOMATICALLY,
|
PROMPT_AUTOMATICALLY,
|
||||||
@@ -15,22 +16,20 @@ describe.each([
|
|||||||
[PROMPT_CANCELLED, true],
|
[PROMPT_CANCELLED, true],
|
||||||
[PROMPT_AUTOMATICALLY, true],
|
[PROMPT_AUTOMATICALLY, true],
|
||||||
[REQUIRE_PASSWORD_ON_START, true],
|
[REQUIRE_PASSWORD_ON_START, true],
|
||||||
|
[BIOMETRIC_UNLOCK_ENABLED, "test"],
|
||||||
])(
|
])(
|
||||||
"deserializes state %s",
|
"deserializes state %s",
|
||||||
(
|
(
|
||||||
...args: [KeyDefinition<EncryptedString>, EncryptedString] | [KeyDefinition<boolean>, boolean]
|
...args: [KeyDefinition<EncryptedString>, EncryptedString] | [KeyDefinition<boolean>, boolean]
|
||||||
) => {
|
) => {
|
||||||
it("should deserialize state", () => {
|
function testDeserialization<T>(keyDefinition: KeyDefinition<T>, state: T) {
|
||||||
const [keyDefinition, state] = args;
|
|
||||||
// Need to type check to avoid TS error due to array values being unions instead of guaranteed tuple pairs
|
|
||||||
if (typeof state === "boolean") {
|
|
||||||
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
|
||||||
expect(deserialized).toEqual(state);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
||||||
expect(deserialized).toEqual(state);
|
expect(deserialized).toEqual(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("should deserialize state", () => {
|
||||||
|
const [keyDefinition, state] = args;
|
||||||
|
testDeserialization(keyDefinition, state);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { EncryptedString } from "../models/domain/enc-string";
|
import { EncryptedString } from "../models/domain/enc-string";
|
||||||
import { KeyDefinition, BIOMETRIC_SETTINGS_DISK } from "../state";
|
import { KeyDefinition, BIOMETRIC_SETTINGS_DISK } from "../state";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the user elected to store a biometric key to unlock their vault.
|
||||||
|
*/
|
||||||
|
export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition<boolean>(
|
||||||
|
BIOMETRIC_SETTINGS_DISK,
|
||||||
|
"biometricUnlockEnabled",
|
||||||
|
{
|
||||||
|
deserializer: (obj) => obj,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boolean indicating the user has elected to require a password to use their biometric key upon starting the application.
|
* Boolean indicating the user has elected to require a password to use their biometric key upon starting the application.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -200,7 +200,6 @@ export class AccountProfile {
|
|||||||
|
|
||||||
export class AccountSettings {
|
export class AccountSettings {
|
||||||
autoConfirmFingerPrints?: boolean;
|
autoConfirmFingerPrints?: boolean;
|
||||||
biometricUnlock?: boolean;
|
|
||||||
defaultUriMatch?: UriMatchType;
|
defaultUriMatch?: UriMatchType;
|
||||||
disableGa?: boolean;
|
disableGa?: boolean;
|
||||||
dontShowCardsCurrentTab?: boolean;
|
dontShowCardsCurrentTab?: boolean;
|
||||||
|
|||||||
@@ -378,24 +378,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBiometricUnlock(options?: StorageOptions): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
|
||||||
?.settings?.biometricUnlock ?? false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setBiometricUnlock(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.biometricUnlock = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCanAccessPremium(options?: StorageOptions): Promise<boolean> {
|
async getCanAccessPremium(options?: StorageOptions): Promise<boolean> {
|
||||||
if (!(await this.getIsAuthenticated(options))) {
|
if (!(await this.getIsAuthenticated(options))) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { Policy } from "../../admin-console/models/domain/policy";
|
import { Policy } from "../../admin-console/models/domain/policy";
|
||||||
@@ -7,6 +7,7 @@ import { TokenService } from "../../auth/abstractions/token.service";
|
|||||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateService } from "../../platform/abstractions/state.service";
|
||||||
|
import { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
|
||||||
import { AccountDecryptionOptions } from "../../platform/models/domain/account";
|
import { AccountDecryptionOptions } from "../../platform/models/domain/account";
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
let tokenService: MockProxy<TokenService>;
|
let tokenService: MockProxy<TokenService>;
|
||||||
let policyService: MockProxy<PolicyService>;
|
let policyService: MockProxy<PolicyService>;
|
||||||
let stateService: MockProxy<StateService>;
|
let stateService: MockProxy<StateService>;
|
||||||
|
const biometricStateService = mock<BiometricStateService>();
|
||||||
let service: VaultTimeoutSettingsService;
|
let service: VaultTimeoutSettingsService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -29,7 +31,14 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
tokenService,
|
tokenService,
|
||||||
policyService,
|
policyService,
|
||||||
stateService,
|
stateService,
|
||||||
|
biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
biometricStateService.biometricUnlockEnabled$ = of(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("availableVaultTimeoutActions$", () => {
|
describe("availableVaultTimeoutActions$", () => {
|
||||||
@@ -66,7 +75,7 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("contains Lock when the user has biometrics configured", async () => {
|
it("contains Lock when the user has biometrics configured", async () => {
|
||||||
stateService.getBiometricUnlock.mockResolvedValue(true);
|
biometricStateService.biometricUnlockEnabled$ = of(true);
|
||||||
|
|
||||||
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
|
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
|
||||||
|
|
||||||
@@ -79,7 +88,7 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
);
|
);
|
||||||
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
|
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
|
||||||
stateService.getProtectedPin.mockResolvedValue(null);
|
stateService.getProtectedPin.mockResolvedValue(null);
|
||||||
stateService.getBiometricUnlock.mockResolvedValue(false);
|
biometricStateService.biometricUnlockEnabled$ = of(false);
|
||||||
|
|
||||||
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
|
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
|
||||||
|
|
||||||
@@ -127,7 +136,7 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
`(
|
`(
|
||||||
"returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
|
"returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
|
||||||
async ({ unlockMethod, policy, userPreference, expected }) => {
|
async ({ unlockMethod, policy, userPreference, expected }) => {
|
||||||
stateService.getBiometricUnlock.mockResolvedValue(unlockMethod);
|
biometricStateService.biometricUnlockEnabled$ = of(unlockMethod);
|
||||||
stateService.getAccountDecryptionOptions.mockResolvedValue(
|
stateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||||
new AccountDecryptionOptions({ hasMasterPassword: false }),
|
new AccountDecryptionOptions({ hasMasterPassword: false }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defer } from "rxjs";
|
import { defer, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@@ -7,6 +7,8 @@ import { TokenService } from "../../auth/abstractions/token.service";
|
|||||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateService } from "../../platform/abstractions/state.service";
|
||||||
|
import { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - DISABLED: No Pin set
|
* - DISABLED: No Pin set
|
||||||
@@ -21,6 +23,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
|||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise<void> {
|
async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise<void> {
|
||||||
@@ -74,7 +77,11 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isBiometricLockSet(userId?: string): Promise<boolean> {
|
async isBiometricLockSet(userId?: string): Promise<boolean> {
|
||||||
return await this.stateService.getBiometricUnlock({ userId });
|
const biometricUnlockPromise =
|
||||||
|
userId == null
|
||||||
|
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||||
|
: this.biometricStateService.getBiometricUnlockEnabled(userId as UserId);
|
||||||
|
return await biometricUnlockPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVaultTimeout(userId?: string): Promise<number> {
|
async getVaultTimeout(userId?: string): Promise<number> {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { MoveBiometricPromptsToStateProviders } from "./migrations/23-move-biome
|
|||||||
import { SmOnboardingTasksMigrator } from "./migrations/24-move-sm-onboarding-key-to-state-providers";
|
import { SmOnboardingTasksMigrator } from "./migrations/24-move-sm-onboarding-key-to-state-providers";
|
||||||
import { ClearClipboardDelayMigrator } from "./migrations/25-move-clear-clipboard-to-autofill-settings-state-provider";
|
import { ClearClipboardDelayMigrator } from "./migrations/25-move-clear-clipboard-to-autofill-settings-state-provider";
|
||||||
import { BadgeSettingsMigrator } from "./migrations/26-move-badge-settings-to-state-providers";
|
import { BadgeSettingsMigrator } from "./migrations/26-move-badge-settings-to-state-providers";
|
||||||
|
import { MoveBiometricUnlockToStateProviders } from "./migrations/27-move-biometric-unlock-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";
|
||||||
@@ -31,7 +32,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 = 26;
|
export const CURRENT_VERSION = 27;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@@ -60,7 +61,8 @@ export function createMigrationBuilder() {
|
|||||||
.with(MoveBiometricPromptsToStateProviders, 22, 23)
|
.with(MoveBiometricPromptsToStateProviders, 22, 23)
|
||||||
.with(SmOnboardingTasksMigrator, 23, 24)
|
.with(SmOnboardingTasksMigrator, 23, 24)
|
||||||
.with(ClearClipboardDelayMigrator, 24, 25)
|
.with(ClearClipboardDelayMigrator, 24, 25)
|
||||||
.with(BadgeSettingsMigrator, 25, CURRENT_VERSION);
|
.with(BadgeSettingsMigrator, 25, 26)
|
||||||
|
.with(MoveBiometricUnlockToStateProviders, 26, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { MockProxy, any } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BIOMETRIC_UNLOCK_ENABLED,
|
||||||
|
MoveBiometricUnlockToStateProviders,
|
||||||
|
} from "./27-move-biometric-unlock-to-state-providers";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||||
|
"user-1": {
|
||||||
|
settings: {
|
||||||
|
biometricUnlock: true,
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
"user_user-1_biometricSettings_biometricUnlockEnabled": true,
|
||||||
|
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: MoveBiometricUnlockToStateProviders;
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 26);
|
||||||
|
sut = new MoveBiometricUnlockToStateProviders(26, 27);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes biometricUnlock 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("sets biometricUnlock value for account that have it", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user-1", BIOMETRIC_UNLOCK_ENABLED, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call extra setToUser", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 27);
|
||||||
|
sut = new MoveBiometricUnlockToStateProviders(26, 27);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nulls out new values", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user-1", BIOMETRIC_UNLOCK_ENABLED, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds explicit value back to accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
settings: {
|
||||||
|
biometricUnlock: true,
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["user-2", "user-3"])(
|
||||||
|
"does not restore values when accounts are not present",
|
||||||
|
async (userId) => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
settings?: {
|
||||||
|
biometricUnlock?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BIOMETRIC_UNLOCK_ENABLED: KeyDefinitionLike = {
|
||||||
|
key: "biometricUnlockEnabled",
|
||||||
|
stateDefinition: { name: "biometricSettings" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MoveBiometricUnlockToStateProviders extends Migrator<26, 27> {
|
||||||
|
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?.biometricUnlock != null) {
|
||||||
|
await helper.setToUser(
|
||||||
|
userId,
|
||||||
|
BIOMETRIC_UNLOCK_ENABLED,
|
||||||
|
account.settings.biometricUnlock,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old account data
|
||||||
|
delete account?.settings?.biometricUnlock;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||||
|
const biometricUnlock = await helper.getFromUser<boolean>(userId, BIOMETRIC_UNLOCK_ENABLED);
|
||||||
|
|
||||||
|
if (biometricUnlock != null) {
|
||||||
|
account ??= {};
|
||||||
|
account.settings ??= {};
|
||||||
|
|
||||||
|
account.settings.biometricUnlock = biometricUnlock;
|
||||||
|
await helper.setToUser(userId, BIOMETRIC_UNLOCK_ENABLED, null);
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user