1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

Rework Desktop Biometrics (#5234)

This commit is contained in:
Matt Gibson
2023-04-18 09:09:47 -04:00
committed by GitHub
parent 4852992662
commit 830af7b06d
55 changed files with 2497 additions and 564 deletions

View File

@@ -30,8 +30,8 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export class CryptoService implements CryptoServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private encryptService: EncryptService,
protected cryptoFunctionService: CryptoFunctionService,
protected encryptService: EncryptService,
protected platformUtilService: PlatformUtilsService,
protected logService: LogService,
protected stateService: StateService
@@ -716,16 +716,19 @@ export class CryptoService implements CryptoServiceAbstraction {
// ---HELPERS---
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
} else if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
if (storeAuto) {
await this.storeAutoKey(key, userId);
} else {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
}
}
protected async storeAutoKey(key: SymmetricCryptoKey, userId?: string) {
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
}
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) {
let shouldStoreKey = false;
if (keySuffix === KeySuffixOptions.Auto) {

View File

@@ -1,5 +1,5 @@
import { BehaviorSubject, concatMap } from "rxjs";
import { Jsonify } from "type-fest";
import { Jsonify, JsonValue } from "type-fest";
import { LogService } from "../abstractions/log.service";
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
@@ -18,6 +18,7 @@ import { CollectionView } from "../admin-console/models/view/collection.view";
import { EnvironmentUrls } from "../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../auth/models/domain/kdf-config";
import { BiometricKey } from "../auth/types/biometric-key";
import { HtmlStorageLocation, KdfType, StorageLocation, ThemeType, UriMatchType } from "../enums";
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
import { StateFactory } from "../factories/stateFactory";
@@ -607,7 +608,7 @@ export class StateService<
);
}
async setCryptoMasterKeyBiometric(value: string, options?: StorageOptions): Promise<void> {
async setCryptoMasterKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
await this.defaultSecureStorageOptions()
@@ -1136,24 +1137,6 @@ export class StateService<
);
}
async getEnableBiometric(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.enableBiometrics ?? false
);
}
async setEnableBiometric(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.enableBiometrics = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEnableBrowserIntegration(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -1876,24 +1859,6 @@ export class StateService<
);
}
async getNoAutoPromptBiometrics(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.noAutoPromptBiometrics ?? false
);
}
async setNoAutoPromptBiometrics(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.noAutoPromptBiometrics = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getNoAutoPromptBiometricsText(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
@@ -2848,7 +2813,11 @@ export class StateService<
return this.reconcileOptions(options, defaultOptions);
}
private async saveSecureStorageKey(key: string, value: string, options?: StorageOptions) {
private async saveSecureStorageKey<T extends JsonValue>(
key: string,
value: T,
options?: StorageOptions
) {
return value == null
? await this.secureStorageService.remove(`${options.userId}${key}`, options)
: await this.secureStorageService.save(`${options.userId}${key}`, value, options);

View File

@@ -174,6 +174,22 @@ export class StateMigrationService<
await this.setCurrentStateVersion(StateVersion.Six);
break;
}
case StateVersion.Six: {
const authenticatedAccounts = await this.getAuthenticatedAccounts();
const globals = (await this.getGlobals()) as any;
for (const account of authenticatedAccounts) {
const migratedAccount = await this.migrateAccountFrom6To7(
globals?.noAutoPromptBiometrics,
account
);
await this.set(account.profile.userId, migratedAccount);
}
if (globals) {
delete globals.noAutoPromptBiometrics;
}
await this.set(keys.global, globals);
await this.setCurrentStateVersion(StateVersion.Seven);
}
}
currentStateVersion += 1;
@@ -204,7 +220,7 @@ export class StateMigrationService<
// 1. Check for an existing storage value from the old storage structure OR
// 2. Check for a value already set by processes that run before migration OR
// 3. Assign the default value
const globals =
const globals: any =
(await this.get<GlobalState>(keys.global)) ?? this.stateFactory.createGlobal(null);
globals.stateVersion = StateVersion.Two;
globals.environmentUrls =
@@ -525,6 +541,16 @@ export class StateMigrationService<
return account;
}
protected async migrateAccountFrom6To7(
globalSetting: boolean,
account: TAccount
): Promise<TAccount> {
if (globalSetting) {
account.settings = Object.assign({}, account.settings, { disableAutoBiometricsPrompt: true });
}
return account;
}
protected get options(): StorageOptions {
return { htmlStorageLocation: HtmlStorageLocation.Local };
}

View File

@@ -5,6 +5,7 @@ import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { Utils } from "../misc/utils";
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { CsprngArray } from "../types/csprng";
export class WebCryptoFunctionService implements CryptoFunctionService {
private crypto: Crypto;
@@ -350,10 +351,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return [publicKey, privateKey];
}
randomBytes(length: number): Promise<ArrayBuffer> {
randomBytes(length: number): Promise<CsprngArray> {
const arr = new Uint8Array(length);
this.crypto.getRandomValues(arr);
return Promise.resolve(arr.buffer);
return Promise.resolve(arr.buffer as CsprngArray);
}
private toBuf(value: string | ArrayBuffer): ArrayBuffer {