1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-07 02:53:28 +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

@@ -2,72 +2,65 @@ import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunc
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { KeySuffixOptions } from "@bitwarden/common/enums";
import { Utils } from "@bitwarden/common/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { CryptoService } from "@bitwarden/common/services/crypto.service";
import { CsprngString } from "@bitwarden/common/types/csprng";
import { ElectronStateService } from "./electron-state.service.abstraction";
export class ElectronCryptoService extends CryptoService {
constructor(
cryptoFunctionService: CryptoFunctionService,
encryptService: EncryptService,
platformUtilService: PlatformUtilsService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
stateService: StateService
protected override stateService: ElectronStateService
) {
super(cryptoFunctionService, encryptService, platformUtilService, logService, stateService);
super(cryptoFunctionService, encryptService, platformUtilsService, logService, stateService);
}
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
await this.upgradeSecurelyStoredKey();
return super.hasKeyStored(keySuffix);
}
protected override async storeKey(key: SymmetricCryptoKey, userId?: string) {
await super.storeKey(key, userId);
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId);
if (storeBiometricKey) {
await this.storeBiometricKey(key, userId);
} else {
this.clearStoredKey(KeySuffixOptions.Auto);
}
if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
} else {
this.clearStoredKey(KeySuffixOptions.Biometric);
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
}
}
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) {
await this.upgradeSecurelyStoredKey();
return super.retrieveKeyFromStorage(keySuffix, userId);
protected async storeBiometricKey(key: SymmetricCryptoKey, userId?: string): Promise<void> {
let clientEncKeyHalf: CsprngString = null;
if (await this.stateService.getBiometricRequirePasswordOnStart({ userId })) {
clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
}
await this.stateService.setCryptoMasterKeyBiometric(
{ key: key.keyB64, clientEncKeyHalf },
{ userId: userId }
);
}
/**
* @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to
* multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication.
*/
private async upgradeSecurelyStoredKey() {
// attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway.
const key = await this.stateService.getCryptoMasterKeyB64();
if (key == null) {
return;
}
private async getBiometricEncryptionClientKeyHalf(userId?: string): Promise<CsprngString | null> {
try {
if (await this.shouldStoreKey(KeySuffixOptions.Auto)) {
await this.stateService.setCryptoMasterKeyAuto(key);
let biometricKey = await this.stateService
.getBiometricEncryptionClientKeyHalf({ userId })
.then((result) => result?.decrypt(null /* user encrypted */))
.then((result) => result as CsprngString);
const userKey = await this.getKeyForUserEncryption();
if (biometricKey == null && userKey != null) {
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
const encKey = await this.encryptService.encrypt(biometricKey, userKey);
await this.stateService.setBiometricEncryptionClientKeyHalf(encKey);
}
if (await this.shouldStoreKey(KeySuffixOptions.Biometric)) {
await this.stateService.setCryptoMasterKeyBiometric(key);
}
} catch (e) {
this.logService.error(
`Encountered error while upgrading obsolete Bitwarden secure storage item:`
);
this.logService.error(e);
}
await this.stateService.setCryptoMasterKeyB64(null);
return biometricKey;
} catch {
return null;
}
}
}

View File

@@ -6,6 +6,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ClientType, DeviceType } from "@bitwarden/common/enums";
import { BiometricMessage, BiometricStorageAction } from "../types/biometric-message";
import { isDev, isMacAppStore } from "../utils";
export class ElectronPlatformUtilsService implements PlatformUtilsService {
@@ -169,9 +170,15 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
}
async supportsBiometric(): Promise<boolean> {
return await this.stateService.getEnableBiometric();
return await ipcRenderer.invoke("biometric", {
action: BiometricStorageAction.OsSupported,
} as BiometricMessage);
}
/** This method is used to authenticate the user presence _only_.
* It should not be used in the process to retrieve
* biometric keys, which has a separate authentication mechanism.
* For biometric keys, invoke "keytar" with a biometric key suffix */
async authenticateBiometric(): Promise<boolean> {
const val = await ipcRenderer.invoke("biometric", {
action: "authenticate",

View File

@@ -0,0 +1,17 @@
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { StorageOptions } from "@bitwarden/common/models/domain/storage-options";
import { Account } from "../models/account";
export abstract class ElectronStateService extends StateService<Account> {
getBiometricEncryptionClientKeyHalf: (options?: StorageOptions) => Promise<EncString>;
setBiometricEncryptionClientKeyHalf: (
value: EncString,
options?: StorageOptions
) => Promise<void>;
getDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<boolean>;
setDismissedBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<void>;
getBiometricRequirePasswordOnStart: (options?: StorageOptions) => Promise<boolean>;
setBiometricRequirePasswordOnStart: (value: boolean, options?: StorageOptions) => Promise<void>;
}

View File

@@ -0,0 +1,80 @@
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { StorageOptions } from "@bitwarden/common/models/domain/storage-options";
import { StateService as BaseStateService } from "@bitwarden/common/services/state.service";
import { Account } from "../models/account";
import { ElectronStateService as ElectronStateServiceAbstraction } from "./electron-state.service.abstraction";
export class ElectronStateService
extends BaseStateService<GlobalState, Account>
implements ElectronStateServiceAbstraction
{
async addAccount(account: Account) {
// Apply desktop overides to default account values
account = new Account(account);
await super.addAccount(account);
}
async getBiometricEncryptionClientKeyHalf(options?: StorageOptions): Promise<EncString> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
const key = account?.keys?.biometricEncryptionClientKeyHalf;
return key == null ? null : new EncString(key);
}
async setBiometricEncryptionClientKeyHalf(
value: EncString,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.keys.biometricEncryptionClientKeyHalf = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getBiometricRequirePasswordOnStart(options?: StorageOptions): Promise<boolean> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
return account?.settings?.requirePasswordOnStart;
}
async setBiometricRequirePasswordOnStart(
value: boolean,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.settings.requirePasswordOnStart = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getDismissedBiometricRequirePasswordOnStart(options?: StorageOptions): Promise<boolean> {
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())
);
}
}

View File

@@ -25,11 +25,11 @@ import { GenerateResponse } from "../models/native-messaging/encrypted-message-r
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 { StateService } from "./state.service";
import { ElectronStateService } from "./electron-state.service";
export class EncryptedMessageHandlerService {
constructor(
private stateService: StateService,
private stateService: ElectronStateService,
private authService: AuthService,
private cipherService: CipherService,
private policyService: PolicyService,

View File

@@ -1,16 +0,0 @@
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { StateService as BaseStateService } from "@bitwarden/common/services/state.service";
import { Account } from "../models/account";
export class StateService
extends BaseStateService<GlobalState, Account>
implements StateServiceAbstraction
{
async addAccount(account: Account) {
// Apply desktop overides to default account values
account = new Account(account);
await super.addAccount(account);
}
}