mirror of
https://github.com/bitwarden/browser
synced 2026-01-04 17:43:39 +00:00
Rework Desktop Biometrics (#5234)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user