1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +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

@@ -108,9 +108,12 @@
{{ biometricText | i18n }}
</label>
</div>
<small class="help-block" *ngIf="this.form.value.biometric">{{
additionalBiometricSettingsText | i18n
}}</small>
</div>
<div class="form-group" *ngIf="supportsBiometric && this.form.value.biometric">
<div class="checkbox">
<div class="checkbox form-group-child">
<label for="autoPromptBiometrics">
<input
id="autoPromptBiometrics"
@@ -122,6 +125,22 @@
</label>
</div>
</div>
<div class="form-group" *ngIf="supportsBiometric && this.form.value.biometric">
<div class="checkbox form-group-child">
<label for="requirePasswordOnStart">
<input
id="requirePasswordOnStart"
type="checkbox"
formControlName="requirePasswordOnStart"
(change)="updateRequirePasswordOnStart()"
/>
{{ "requirePasswordOnStart" | i18n }}
</label>
</div>
<small class="help-block form-group-child" *ngIf="isWindows">{{
"recommendedForSecurity" | i18n
}}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="approveLoginRequests">

View File

@@ -9,15 +9,15 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { DeviceType, ThemeType, StorageLocation } from "@bitwarden/common/enums";
import { DeviceType, ThemeType, StorageLocation, KeySuffixOptions } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { Utils } from "@bitwarden/common/misc/utils";
import { flagEnabled } from "../../flags";
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
import { isWindowsStore } from "../../utils";
import { SetPinComponent } from "../components/set-pin.component";
@@ -37,10 +37,12 @@ export class SettingsComponent implements OnInit {
clearClipboardOptions: any[];
supportsBiometric: boolean;
biometricText: string;
additionalBiometricSettingsText: string;
autoPromptBiometricsText: string;
showAlwaysShowDock = false;
requireEnableTray = false;
showDuckDuckGoIntegrationOption = false;
isWindows: boolean;
enableTrayText: string;
enableTrayDescText: string;
@@ -70,6 +72,7 @@ export class SettingsComponent implements OnInit {
pin: [null as boolean | null],
biometric: false,
autoPromptBiometrics: false,
requirePasswordOnStart: false,
approveLoginRequests: false,
// Account Preferences
clearClipboard: [null as number | null],
@@ -100,7 +103,7 @@ export class SettingsComponent implements OnInit {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private stateService: StateService,
private stateService: ElectronStateService,
private messagingService: MessagingService,
private cryptoService: CryptoService,
private modalService: ModalService,
@@ -182,6 +185,8 @@ export class SettingsComponent implements OnInit {
}
async ngOnInit() {
this.isWindows = (await this.platformUtilsService.getDevice()) === DeviceType.WindowsDesktop;
if ((await this.stateService.getUserId()) == null) {
return;
}
@@ -216,7 +221,9 @@ export class SettingsComponent implements OnInit {
vaultTimeoutAction: await this.vaultTimeoutSettingsService.getVaultTimeoutAction(),
pin: pinSet[0] || pinSet[1],
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
autoPromptBiometrics: !(await this.stateService.getNoAutoPromptBiometrics()),
autoPromptBiometrics: !(await this.stateService.getDisableAutoBiometricsPrompt()),
requirePasswordOnStart:
(await this.stateService.getBiometricRequirePasswordOnStart()) ?? false,
approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false,
clearClipboard: await this.stateService.getClearClipboard(),
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
@@ -246,6 +253,10 @@ export class SettingsComponent implements OnInit {
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
this.biometricText = await this.stateService.getBiometricText();
this.additionalBiometricSettingsText =
this.biometricText === "unlockWithTouchId"
? "additionalTouchIdSettings"
: "additionalWindowsHelloSettings";
this.autoPromptBiometricsText = await this.stateService.getNoAutoPromptBiometricsText();
this.previousVaultTimeout = this.form.value.vaultTimeout;
@@ -379,26 +390,52 @@ export class SettingsComponent implements OnInit {
return;
}
const authResult = await this.platformUtilsService.authenticateBiometric();
if (!authResult) {
this.form.controls.biometric.setValue(false);
return;
}
this.form.controls.biometric.setValue(true);
await this.stateService.setBiometricUnlock(true);
if (this.isWindows) {
// Recommended settings for Windows Hello
this.form.controls.requirePasswordOnStart.setValue(true);
this.form.controls.autoPromptBiometrics.setValue(false);
await this.stateService.setDisableAutoBiometricsPrompt(true);
await this.stateService.setBiometricRequirePasswordOnStart(true);
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
}
await this.cryptoService.toggleKey();
// Validate the key is stored in case biometrics fail.
const biometricSet = await this.cryptoService.hasKeyStored(KeySuffixOptions.Biometric);
this.form.controls.biometric.setValue(biometricSet);
if (!biometricSet) {
await this.stateService.setBiometricUnlock(null);
}
}
async updateAutoPromptBiometrics() {
if (this.form.value.autoPromptBiometrics) {
await this.stateService.setNoAutoPromptBiometrics(null);
// require password on start must be disabled if auto prompt biometrics is enabled
this.form.controls.requirePasswordOnStart.setValue(false);
await this.updateRequirePasswordOnStart();
await this.stateService.setDisableAutoBiometricsPrompt(null);
} else {
await this.stateService.setNoAutoPromptBiometrics(true);
await this.stateService.setDisableAutoBiometricsPrompt(true);
}
}
async updateRequirePasswordOnStart() {
if (this.form.value.requirePasswordOnStart) {
// auto prompt biometrics must be disabled if require password on start is enabled
this.form.controls.autoPromptBiometrics.setValue(false);
await this.updateAutoPromptBiometrics();
await this.stateService.setBiometricRequirePasswordOnStart(true);
} else {
await this.stateService.setBiometricRequirePasswordOnStart(false);
await this.stateService.setBiometricEncryptionClientKeyHalf(null);
}
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
await this.cryptoService.toggleKey();
}
async saveFavicons() {
await this.stateService.setDisableFavicon(!this.form.value.enableFavicons);
await this.stateService.setDisableFavicon(!this.form.value.enableFavicons, {

View File

@@ -48,11 +48,12 @@ import { ElectronPlatformUtilsService } from "../../services/electron-platform-u
import { ElectronRendererMessagingService } from "../../services/electron-renderer-messaging.service";
import { ElectronRendererSecureStorageService } from "../../services/electron-renderer-secure-storage.service";
import { ElectronRendererStorageService } from "../../services/electron-renderer-storage.service";
import { ElectronStateService } from "../../services/electron-state.service";
import { ElectronStateService as ElectronStateServiceAbstraction } from "../../services/electron-state.service.abstraction";
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
import { I18nService } from "../../services/i18n.service";
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
import { NativeMessagingService } from "../../services/native-messaging.service";
import { StateService } from "../../services/state.service";
import { PasswordRepromptService } from "../../vault/services/password-reprompt.service";
import { SearchBarService } from "../layout/search/search-bar.service";
@@ -112,17 +113,6 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
{ provide: AbstractStorageService, useClass: ElectronRendererStorageService },
{ provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService },
{ provide: MEMORY_STORAGE, useClass: MemoryStorageService },
{
provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService,
deps: [
CryptoFunctionServiceAbstraction,
EncryptService,
PlatformUtilsServiceAbstraction,
LogServiceAbstraction,
StateServiceAbstraction,
],
},
{
provide: SystemServiceAbstraction,
useClass: SystemService,
@@ -136,7 +126,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
{
provide: StateServiceAbstraction,
useClass: StateService,
useClass: ElectronStateService,
deps: [
AbstractStorageService,
SECURE_STORAGE,
@@ -147,6 +137,10 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
STATE_SERVICE_USE_CACHE,
],
},
{
provide: ElectronStateServiceAbstraction,
useExisting: StateServiceAbstraction,
},
{
provide: FileDownloadService,
useClass: DesktopFileDownloadService,
@@ -182,6 +176,17 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
useClass: LoginService,
deps: [StateServiceAbstraction],
},
{
provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService,
deps: [
CryptoFunctionServiceAbstraction,
EncryptService,
PlatformUtilsServiceAbstraction,
LogService,
StateServiceAbstraction,
],
},
],
})
export class ServicesModule {}

View File

@@ -53,7 +53,7 @@
</div>
</div>
<div class="buttons with-rows">
<div class="buttons-row" *ngIf="supportsBiometric && biometricLock">
<div class="buttons-row" *ngIf="supportsBiometric && biometricLock && biometricReady">
<button
type="button"
class="btn block"

View File

@@ -13,14 +13,17 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { DeviceType, KeySuffixOptions } from "@bitwarden/common/enums";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { ElectronStateService } from "../services/electron-state.service.abstraction";
import { BiometricStorageAction, BiometricMessage } from "../types/biometric-message";
const BroadcasterSubscriptionId = "LockComponent";
@Component({
@@ -29,6 +32,7 @@ const BroadcasterSubscriptionId = "LockComponent";
})
export class LockComponent extends BaseLockComponent {
private deferFocus: boolean = null;
protected biometricReady = false;
protected oldOs = false;
protected deprecated = false;
@@ -41,7 +45,7 @@ export class LockComponent extends BaseLockComponent {
vaultTimeoutService: VaultTimeoutService,
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
environmentService: EnvironmentService,
stateService: StateService,
protected override stateService: ElectronStateService,
apiService: ApiService,
private route: ActivatedRoute,
private broadcasterService: BroadcasterService,
@@ -88,7 +92,10 @@ export class LockComponent extends BaseLockComponent {
async ngOnInit() {
await super.ngOnInit();
const autoPromptBiometric = !(await this.stateService.getNoAutoPromptBiometrics());
const autoPromptBiometric = !(await this.stateService.getDisableAutoBiometricsPrompt());
this.biometricReady = await this.canUseBiometric();
await this.displayBiometricUpdateWarning();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.queryParams.subscribe((params) => {
@@ -135,7 +142,44 @@ export class LockComponent extends BaseLockComponent {
this.showPassword = false;
}
private async canUseBiometric() {
const userId = await this.stateService.getUserId();
const val = await ipcRenderer.invoke("biometric", {
action: BiometricStorageAction.EnabledForUser,
key: `${userId}_masterkey_biometric`,
keySuffix: KeySuffixOptions.Biometric,
userId: userId,
} as BiometricMessage);
return val != null ? (JSON.parse(val) as boolean) : null;
}
private focusInput() {
document.getElementById(this.pinLock ? "pin" : "masterPassword").focus();
}
private async displayBiometricUpdateWarning(): Promise<void> {
if (await this.stateService.getDismissedBiometricRequirePasswordOnStart()) {
return;
}
if (this.platformUtilsService.getDevice() !== DeviceType.WindowsDesktop) {
return;
}
if (await this.stateService.getBiometricUnlock()) {
const response = await this.platformUtilsService.showDialog(
this.i18nService.t("windowsBiometricUpdateWarning"),
this.i18nService.t("windowsBiometricUpdateWarningTitle"),
this.i18nService.t("yes"),
this.i18nService.t("no")
);
await this.stateService.setBiometricRequirePasswordOnStart(response);
if (response) {
await this.stateService.setDisableAutoBiometricsPrompt(true);
}
this.supportsBiometric = await this.canUseBiometric();
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
}
}
}

View File

@@ -1392,20 +1392,32 @@
"unlockWithWindowsHello": {
"message": "Unlock with Windows Hello"
},
"additionalWindowsHelloSettings": {
"message": "Additional Windows Hello settings"
},
"windowsHelloConsentMessage": {
"message": "Verify for Bitwarden."
},
"unlockWithTouchId": {
"message": "Unlock with Touch ID"
},
"additionalTouchIdSettings": {
"message": "Additional Touch ID settings"
},
"touchIdConsentMessage": {
"message": "unlock your vault"
},
"autoPromptWindowsHello": {
"message": "Ask for Windows Hello on launch"
"message": "Ask for Windows Hello on app start"
},
"autoPromptTouchId": {
"message": "Ask for Touch ID on launch"
"message": "Ask for Touch ID on app start"
},
"requirePasswordOnStart": {
"message": "Require password or PIN on app start"
},
"recommendedForSecurity": {
"message": "Recommended for security."
},
"lockWithMasterPassOnRestart": {
"message": "Lock with master password on restart"
@@ -2234,6 +2246,12 @@
}
}
},
"windowsBiometricUpdateWarning": {
"message": "Bitwarden recommends updating your biometric settings to require your master password (or PIN) on the first unlock. Would you like to update your settings now?"
},
"windowsBiometricUpdateWarningTitle": {
"message": "Recommended Settings Update"
},
"windows8SoonDeprecated": {
"message": "The operating system you are using will no longer be supported after the 2023.5.0 release. Upgrade to a supported operating system. Continuing without updating your operating system may result in unexpected behavior or security risks.",
"description": "Windows 8, 8.1 and Server 2012 R2 are no longer supported by Electron & Chromium. Show a notice on the login and lock screen while 2023.4.0 is the active version."

View File

@@ -5,7 +5,6 @@ import { app } from "electron";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { StateService } from "@bitwarden/common/services/state.service";
import { BiometricsService, BiometricsServiceAbstraction } from "./main/biometric/index";
import { DesktopCredentialStorageListener } from "./main/desktop-credential-storage-listener";
@@ -19,6 +18,7 @@ import { WindowMain } from "./main/window.main";
import { Account } from "./models/account";
import { ElectronLogService } from "./services/electron-log.service";
import { ElectronMainMessagingService } from "./services/electron-main-messaging.service";
import { ElectronStateService } from "./services/electron-state.service";
import { ElectronStorageService } from "./services/electron-storage.service";
import { I18nService } from "./services/i18n.service";
@@ -28,7 +28,7 @@ export class Main {
storageService: ElectronStorageService;
memoryStorageService: MemoryStorageService;
messagingService: ElectronMainMessagingService;
stateService: StateService;
stateService: ElectronStateService;
desktopCredentialStorageListener: DesktopCredentialStorageListener;
windowMain: WindowMain;
@@ -85,7 +85,7 @@ export class Main {
// TODO: this state service will have access to on disk storage, but not in memory storage.
// If we could get this to work using the stateService singleton that the rest of the app uses we could save
// ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events.
this.stateService = new StateService(
this.stateService = new ElectronStateService(
this.storageService,
null,
this.memoryStorageService,
@@ -128,7 +128,8 @@ export class Main {
this.desktopCredentialStorageListener = new DesktopCredentialStorageListener(
"Bitwarden",
this.biometricsService
this.biometricsService,
this.logService
);
this.nativeMessagingMain = new NativeMessagingMain(

View File

@@ -1,25 +1,21 @@
import { ipcMain, systemPreferences } from "electron";
import { systemPreferences } from "electron";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { passwords } from "@bitwarden/desktop-native";
import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction";
import { OsBiometricService } from "./biometrics.service.abstraction";
export default class BiometricDarwinMain implements BiometricsServiceAbstraction {
export default class BiometricDarwinMain implements OsBiometricService {
constructor(private i18nservice: I18nService, private stateService: StateService) {}
async init() {
await this.stateService.setEnableBiometric(await this.supportsBiometric());
await this.stateService.setBiometricText("unlockWithTouchId");
await this.stateService.setNoAutoPromptBiometricsText("autoPromptTouchId");
ipcMain.handle("biometric", async () => {
return await this.authenticateBiometric();
});
}
supportsBiometric(): Promise<boolean> {
return Promise.resolve(systemPreferences.canPromptTouchID());
async osSupportsBiometric(): Promise<boolean> {
return systemPreferences.canPromptTouchID();
}
async authenticateBiometric(): Promise<boolean> {
@@ -30,4 +26,35 @@ export default class BiometricDarwinMain implements BiometricsServiceAbstraction
return false;
}
}
async getBiometricKey(service: string, key: string): Promise<string | null> {
const success = await this.authenticateBiometric();
if (!success) {
throw new Error("Biometric authentication failed");
}
return await passwords.getPassword(service, key);
}
async setBiometricKey(service: string, key: string, value: string): Promise<void> {
if (await this.valueUpToDate(service, key, value)) {
return;
}
return await passwords.setPassword(service, key, value);
}
async deleteBiometricKey(service: string, key: string): Promise<void> {
return await passwords.deletePassword(service, key);
}
private async valueUpToDate(service: string, key: string, value: string): Promise<boolean> {
try {
const existing = await passwords.getPassword(service, key);
return existing === value;
} catch {
return false;
}
}
}

View File

@@ -1,48 +1,224 @@
import { ipcMain } from "electron";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { biometrics } from "@bitwarden/desktop-native";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { biometrics, passwords } from "@bitwarden/desktop-native";
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
import { WindowMain } from "../window.main";
import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction";
import { OsBiometricService } from "./biometrics.service.abstraction";
const KEY_WITNESS_SUFFIX = "_witness";
const WITNESS_VALUE = "known key";
export default class BiometricWindowsMain implements OsBiometricService {
// Use set helper method instead of direct access
private _iv: string | null = null;
// Use getKeyMaterial helper instead of direct access
private _osKeyHalf: string | null = null;
export default class BiometricWindowsMain implements BiometricsServiceAbstraction {
constructor(
private i18nservice: I18nService,
private i18nService: I18nService,
private windowMain: WindowMain,
private stateService: StateService,
private stateService: ElectronStateService,
private logService: LogService
) {}
async init() {
let supportsBiometric = false;
try {
supportsBiometric = await this.supportsBiometric();
} catch (e) {
this.logService.error(e);
}
await this.stateService.setEnableBiometric(supportsBiometric);
await this.stateService.setBiometricText("unlockWithWindowsHello");
await this.stateService.setNoAutoPromptBiometricsText("autoPromptWindowsHello");
ipcMain.handle("biometric", async () => {
return await this.authenticateBiometric();
});
}
async supportsBiometric(): Promise<boolean> {
try {
return await biometrics.available();
} catch {
return false;
async osSupportsBiometric(): Promise<boolean> {
return await biometrics.available();
}
async getBiometricKey(
service: string,
storageKey: string,
clientKeyHalfB64: string
): Promise<string | null> {
const value = await passwords.getPassword(service, storageKey);
if (value == null || value == "") {
return null;
} else if (!EncString.isSerializedEncString(value)) {
// Update to format encrypted with client key half
const storageDetails = await this.getStorageDetails({
clientKeyHalfB64,
});
await biometrics.setBiometricSecret(
service,
storageKey,
value,
storageDetails.key_material,
storageDetails.ivB64
);
return value;
} else {
const encValue = new EncString(value);
this.setIv(encValue.iv);
const storageDetails = await this.getStorageDetails({
clientKeyHalfB64,
});
return await biometrics.getBiometricSecret(service, storageKey, storageDetails.key_material);
}
}
async setBiometricKey(
service: string,
storageKey: string,
value: string,
clientKeyPartB64: string | undefined
): Promise<void> {
const parsedValue = SymmetricCryptoKey.fromString(value);
if (await this.valueUpToDate({ value: parsedValue, clientKeyPartB64, service, storageKey })) {
return;
}
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
const storedValue = await biometrics.setBiometricSecret(
service,
storageKey,
value,
storageDetails.key_material,
storageDetails.ivB64
);
const parsedStoredValue = new EncString(storedValue);
await this.storeValueWitness(
parsedValue,
parsedStoredValue,
service,
storageKey,
clientKeyPartB64
);
}
async deleteBiometricKey(service: string, key: string): Promise<void> {
await passwords.deletePassword(service, key);
await passwords.deletePassword(service, key + KEY_WITNESS_SUFFIX);
}
async authenticateBiometric(): Promise<boolean> {
const hwnd = this.windowMain.win.getNativeWindowHandle();
return await biometrics.prompt(hwnd, this.i18nservice.t("windowsHelloConsentMessage"));
return await biometrics.prompt(hwnd, this.i18nService.t("windowsHelloConsentMessage"));
}
private async getStorageDetails({
clientKeyHalfB64,
}: {
clientKeyHalfB64: string;
}): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> {
if (this._osKeyHalf == null) {
// Prompts Windows Hello
const keyMaterial = await biometrics.deriveKeyMaterial(this._iv);
this._osKeyHalf = keyMaterial.keyB64;
this._iv = keyMaterial.ivB64;
}
return {
key_material: {
osKeyPartB64: this._osKeyHalf,
clientKeyPartB64: clientKeyHalfB64,
},
ivB64: this._iv,
};
}
// Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey
// when we want to force a re-derive of the key material.
private setIv(iv: string) {
this._iv = iv;
this._osKeyHalf = null;
}
/**
* Stores a witness key alongside the encrypted value. This is used to determine if the value is up to date.
*
* @param unencryptedValue The key to store
* @param encryptedValue The encrypted value of the key to store. Used to sync IV of the witness key with the stored key.
* @param service The service to store the witness key under
* @param storageKey The key to store the witness key under. The witness key will be stored under storageKey + {@link KEY_WITNESS_SUFFIX}
* @returns
*/
private async storeValueWitness(
unencryptedValue: SymmetricCryptoKey,
encryptedValue: EncString,
service: string,
storageKey: string,
clientKeyPartB64: string
) {
if (encryptedValue.iv == null || encryptedValue == null) {
return;
}
const storageDetails = {
keyMaterial: this.witnessKeyMaterial(unencryptedValue, clientKeyPartB64),
ivB64: encryptedValue.iv,
};
await biometrics.setBiometricSecret(
service,
storageKey + KEY_WITNESS_SUFFIX,
WITNESS_VALUE,
storageDetails.keyMaterial,
storageDetails.ivB64
);
}
/**
* Uses a witness key stored alongside the encrypted value to determine if the value is up to date.
* @param value The value being validated
* @param service The service the value is stored under
* @param storageKey The key the value is stored under. The witness key will be stored under storageKey + {@link KEY_WITNESS_SUFFIX}
* @returns Boolean indicating if the value is up to date.
*/
// Uses a witness key stored alongside the encrypted value to determine if the value is up to date.
private async valueUpToDate({
value,
clientKeyPartB64,
service,
storageKey,
}: {
value: SymmetricCryptoKey;
clientKeyPartB64: string;
service: string;
storageKey: string;
}): Promise<boolean> {
const witnessKeyMaterial = this.witnessKeyMaterial(value, clientKeyPartB64);
if (witnessKeyMaterial == null) {
return false;
}
let witness = null;
try {
witness = await biometrics.getBiometricSecret(
service,
storageKey + KEY_WITNESS_SUFFIX,
witnessKeyMaterial
);
} catch {
this.logService.debug("Error retrieving witness key, assuming value is not up to date.");
return false;
}
if (witness === WITNESS_VALUE) {
return true;
}
return false;
}
/** Derives a witness key from a symmetric key being stored for biometric protection */
private witnessKeyMaterial(
symmetricKey: SymmetricCryptoKey,
clientKeyPartB64: string
): biometrics.KeyMaterial {
const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64;
return {
osKeyPartB64: key,
clientKeyPartB64,
};
}
}

View File

@@ -1,5 +1,44 @@
export abstract class BiometricsServiceAbstraction {
init: () => Promise<void>;
supportsBiometric: () => Promise<boolean>;
osSupportsBiometric: () => Promise<boolean>;
canAuthBiometric: ({
service,
key,
userId,
}: {
service: string;
key: string;
userId: string;
}) => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>;
getBiometricKey: (service: string, key: string) => Promise<string | null>;
setBiometricKey: (service: string, key: string, value: string) => Promise<void>;
setEncryptionKeyHalf: ({
service,
key,
value,
}: {
service: string;
key: string;
value: string;
}) => void;
deleteBiometricKey: (service: string, key: string) => Promise<void>;
}
export interface OsBiometricService {
init: () => Promise<void>;
osSupportsBiometric: () => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>;
getBiometricKey: (
service: string,
key: string,
clientKeyHalfB64: string | undefined
) => Promise<string | null>;
setBiometricKey: (
service: string,
key: string,
value: string,
clientKeyHalfB64: string | undefined
) => Promise<void>;
deleteBiometricKey: (service: string, key: string) => Promise<void>;
}

View File

@@ -1,16 +1,16 @@
import { mock } from "jest-mock-extended";
import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
import { WindowMain } from "../window.main";
import BiometricDarwinMain from "./biometric.darwin.main";
import BiometricWindowsMain from "./biometric.windows.main";
import { BiometricsService } from "./biometrics.service";
import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction";
import { OsBiometricService } from "./biometrics.service.abstraction";
jest.mock("@bitwarden/desktop-native", () => {
return {
@@ -22,11 +22,11 @@ jest.mock("@bitwarden/desktop-native", () => {
describe("biometrics tests", function () {
const i18nService = mock<I18nService>();
const windowMain = mock<WindowMain>();
const stateService = mock<StateService>();
const stateService = mock<ElectronStateService>();
const logService = mock<LogService>();
const messagingService = mock<MessagingService>();
it("Should call the platformspecific methods", () => {
it("Should call the platformspecific methods", async () => {
const sut = new BiometricsService(
i18nService,
windowMain,
@@ -36,13 +36,14 @@ describe("biometrics tests", function () {
process.platform
);
const mockService = mock<BiometricsServiceAbstraction>();
const mockService = mock<OsBiometricService>();
(sut as any).platformSpecificService = mockService;
sut.init();
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
expect(mockService.init).toBeCalled();
sut.supportsBiometric();
expect(mockService.supportsBiometric).toBeCalled();
await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
expect(mockService.osSupportsBiometric).toBeCalled();
sut.authenticateBiometric();
expect(mockService.authenticateBiometric).toBeCalled();
@@ -78,4 +79,50 @@ describe("biometrics tests", function () {
expect(internalService).toBeInstanceOf(BiometricDarwinMain);
});
});
describe("can auth biometric", () => {
let sut: BiometricsService;
let innerService: MockProxy<OsBiometricService>;
beforeEach(() => {
sut = new BiometricsService(
i18nService,
windowMain,
stateService,
logService,
messagingService,
process.platform
);
innerService = mock();
(sut as any).platformSpecificService = innerService;
sut.init();
});
it("should return false if client key half is required and not provided", async () => {
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(true);
const result = await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
expect(result).toBe(false);
});
it("should call osSupportsBiometric if client key half is provided", async () => {
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
expect(innerService.init).toBeCalled();
await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
expect(innerService.osSupportsBiometric).toBeCalled();
});
it("should call osSupportBiometric if client key half is not required", async () => {
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(false);
innerService.osSupportsBiometric.mockResolvedValue(true);
const result = await sut.canAuthBiometric({ service: "test", key: "test", userId: "test" });
expect(result).toBe(true);
expect(innerService.osSupportsBiometric).toBeCalled();
});
});
});

View File

@@ -1,19 +1,20 @@
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
import { WindowMain } from "../window.main";
import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction";
import { BiometricsServiceAbstraction, OsBiometricService } from "./biometrics.service.abstraction";
export class BiometricsService implements BiometricsServiceAbstraction {
private platformSpecificService: BiometricsServiceAbstraction;
private platformSpecificService: OsBiometricService;
private clientKeyHalves = new Map<string, string>();
constructor(
private i18nService: I18nService,
private windowMain: WindowMain,
private stateService: StateService,
private stateService: ElectronStateService,
private logService: LogService,
private messagingService: MessagingService,
private platform: NodeJS.Platform
@@ -50,16 +51,121 @@ export class BiometricsService implements BiometricsServiceAbstraction {
return await this.platformSpecificService.init();
}
async supportsBiometric(): Promise<boolean> {
return await this.platformSpecificService.supportsBiometric();
async osSupportsBiometric() {
return await this.platformSpecificService.osSupportsBiometric();
}
async canAuthBiometric({
service,
key,
userId,
}: {
service: string;
key: string;
userId: string;
}): Promise<boolean> {
const requireClientKeyHalf = await this.stateService.getBiometricRequirePasswordOnStart({
userId,
});
const clientKeyHalfB64 = this.getClientKeyHalf(service, key);
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
return clientKeyHalfSatisfied && (await this.osSupportsBiometric());
}
async authenticateBiometric(): Promise<boolean> {
let result = false;
this.interruptProcessReload(
() => {
return this.platformSpecificService.authenticateBiometric();
},
(response) => {
result = response;
return !response;
}
);
return result;
}
async getBiometricKey(service: string, storageKey: string): Promise<string | null> {
return await this.interruptProcessReload(async () => {
await this.enforceClientKeyHalf(service, storageKey);
return await this.platformSpecificService.getBiometricKey(
service,
storageKey,
this.getClientKeyHalf(service, storageKey)
);
});
}
async setBiometricKey(service: string, storageKey: string, value: string): Promise<void> {
await this.enforceClientKeyHalf(service, storageKey);
return await this.platformSpecificService.setBiometricKey(
service,
storageKey,
value,
this.getClientKeyHalf(service, storageKey)
);
}
/** Registers the client-side encryption key half for the OS stored Biometric key. The other half is protected by the OS.*/
async setEncryptionKeyHalf({
service,
key,
value,
}: {
service: string;
key: string;
value: string;
}): Promise<void> {
if (value == null) {
this.clientKeyHalves.delete(this.clientKeyHalfKey(service, key));
} else {
this.clientKeyHalves.set(this.clientKeyHalfKey(service, key), value);
}
}
async deleteBiometricKey(service: string, storageKey: string): Promise<void> {
this.clientKeyHalves.delete(this.clientKeyHalfKey(service, storageKey));
return await this.platformSpecificService.deleteBiometricKey(service, storageKey);
}
private async interruptProcessReload<T>(
callback: () => Promise<T>,
restartReloadCallback: (arg: T) => boolean = () => false
): Promise<T> {
this.messagingService.send("cancelProcessReload");
const response = await this.platformSpecificService.authenticateBiometric();
if (!response) {
let restartReload = false;
let response: T;
try {
response = await callback();
restartReload ||= restartReloadCallback(response);
} catch {
restartReload = true;
}
if (restartReload) {
this.messagingService.send("startProcessReload");
}
return response;
}
private clientKeyHalfKey(service: string, key: string): string {
return `${service}:${key}`;
}
private getClientKeyHalf(service: string, key: string): string | undefined {
return this.clientKeyHalves.get(this.clientKeyHalfKey(service, key)) ?? undefined;
}
private async enforceClientKeyHalf(service: string, storageKey: string): Promise<void> {
const requireClientKeyHalf = await this.stateService.getBiometricRequirePasswordOnStart();
const clientKeyHalfB64 = this.getClientKeyHalf(service, storageKey);
if (requireClientKeyHalf && !clientKeyHalfB64) {
throw new Error("Biometric key requirements not met. No client key half provided.");
}
}
}

View File

@@ -1,16 +1,20 @@
import { ipcMain } from "electron";
import { BiometricKey } from "@bitwarden/common/auth/types/biometric-key";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { passwords } from "@bitwarden/desktop-native";
import { BiometricMessage, BiometricStorageAction } from "../types/biometric-message";
import { BiometricsServiceAbstraction } from "./biometric/index";
const AuthRequiredSuffix = "_biometric";
const AuthenticatedActions = ["getPassword"];
export class DesktopCredentialStorageListener {
constructor(
private serviceName: string,
private biometricService: BiometricsServiceAbstraction
private biometricService: BiometricsServiceAbstraction,
private logService: ConsoleLogService
) {}
init() {
@@ -22,46 +26,107 @@ export class DesktopCredentialStorageListener {
serviceName += message.keySuffix;
}
const authenticationRequired =
AuthenticatedActions.includes(message.action) && AuthRequiredSuffix === message.keySuffix;
const authenticated = !authenticationRequired || (await this.authenticateBiometric());
let val: string | boolean = null;
if (authenticated && message.action && message.key) {
if (message.action && message.key) {
if (message.action === "getPassword") {
val = await this.getPassword(serviceName, message.key);
val = await this.getPassword(serviceName, message.key, message.keySuffix);
} else if (message.action === "hasPassword") {
const result = await this.getPassword(serviceName, message.key);
const result = await passwords.getPassword(serviceName, message.key);
val = result != null;
} else if (message.action === "setPassword" && message.value) {
await passwords.setPassword(serviceName, message.key, message.value);
await this.setPassword(serviceName, message.key, message.value, message.keySuffix);
} else if (message.action === "deletePassword") {
await passwords.deletePassword(serviceName, message.key);
await this.deletePassword(serviceName, message.key, message.keySuffix);
}
}
return val;
} catch {
return null;
} catch (e) {
if (
e.message === "Password not found." ||
e.message === "The specified item could not be found in the keychain."
) {
return null;
}
this.logService.info(e);
}
});
ipcMain.handle("biometric", async (event: any, message: BiometricMessage) => {
try {
let serviceName = this.serviceName;
message.keySuffix = "_" + (message.keySuffix ?? "");
if (message.keySuffix !== "_") {
serviceName += message.keySuffix;
}
let val: string | boolean = null;
if (!message.action) {
return val;
}
switch (message.action) {
case BiometricStorageAction.EnabledForUser:
if (!message.key || !message.userId) {
break;
}
val = await this.biometricService.canAuthBiometric({
service: serviceName,
key: message.key,
userId: message.userId,
});
break;
case BiometricStorageAction.OsSupported:
val = await this.biometricService.osSupportsBiometric();
break;
default:
}
return val;
} catch (e) {
this.logService.info(e);
}
});
}
// Gracefully handle old keytar values, and if detected updated the entry to the proper format
private async getPassword(serviceName: string, key: string) {
let val = await passwords.getPassword(serviceName, key);
private async getPassword(serviceName: string, key: string, keySuffix: string) {
let val: string;
if (keySuffix === AuthRequiredSuffix) {
val = (await this.biometricService.getBiometricKey(serviceName, key)) ?? null;
} else {
val = await passwords.getPassword(serviceName, key);
}
try {
JSON.parse(val);
} catch (e) {
val = await passwords.getPasswordKeytar(serviceName, key);
await passwords.setPassword(serviceName, key, val);
throw new Error("Password in bad format" + e + val);
}
return val;
}
private async authenticateBiometric(): Promise<boolean> {
if (this.biometricService) {
return await this.biometricService.authenticateBiometric();
private async setPassword(serviceName: string, key: string, value: string, keySuffix: string) {
if (keySuffix === AuthRequiredSuffix) {
const valueObj = JSON.parse(value) as BiometricKey;
await this.biometricService.setEncryptionKeyHalf({
service: serviceName,
key,
value: valueObj?.clientEncKeyHalf,
});
// Value is usually a JSON string, but we need to pass the key half as well, so we re-stringify key here.
await this.biometricService.setBiometricKey(serviceName, key, JSON.stringify(valueObj?.key));
} else {
await passwords.setPassword(serviceName, key, value);
}
}
private async deletePassword(serviceName: string, key: string, keySuffix: string) {
if (keySuffix === AuthRequiredSuffix) {
await this.biometricService.deleteBiometricKey(serviceName, key);
} else {
await passwords.deletePassword(serviceName, key);
}
return false;
}
}

View File

@@ -1,14 +1,25 @@
import { Jsonify } from "type-fest";
import {
Account as BaseAccount,
AccountSettings as BaseAccountSettings,
AccountKeys as BaseAccountKeys,
} from "@bitwarden/common/models/domain/account";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
export class AccountSettings extends BaseAccountSettings {
vaultTimeout = -1; // On Restart
requirePasswordOnStart?: boolean;
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
}
export class AccountKeys extends BaseAccountKeys {
biometricEncryptionClientKeyHalf?: Jsonify<EncString>;
}
export class Account extends BaseAccount {
settings?: AccountSettings = new AccountSettings();
keys?: AccountKeys = new AccountKeys();
constructor(init: Partial<Account>) {
super(init);

View File

@@ -331,6 +331,10 @@ form,
}
}
.form-group-child {
margin-left: 20px;
}
.checkbox {
position: relative;
display: block;

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);
}
}

View File

@@ -0,0 +1,11 @@
export enum BiometricStorageAction {
EnabledForUser = "enabled",
OsSupported = "osSupported",
}
export type BiometricMessage = {
action: BiometricStorageAction;
keySuffix?: string;
key?: string;
userId?: string;
};