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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
80
apps/desktop/src/services/electron-state.service.ts
Normal file
80
apps/desktop/src/services/electron-state.service.ts
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user