mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 19:53:59 +00:00
Move clientkeyhalf to os platform implementation
This commit is contained in:
@@ -5,7 +5,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
@@ -148,9 +147,9 @@ describe("MainBiometricsService", function () {
|
||||
];
|
||||
|
||||
for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) {
|
||||
innerService.osSupportsBiometric.mockResolvedValue(supportsBiometric as boolean);
|
||||
innerService.osBiometricsNeedsSetup.mockResolvedValue(needsSetup as boolean);
|
||||
innerService.osBiometricsCanAutoSetup.mockResolvedValue(canAutoSetup as boolean);
|
||||
innerService.supportsBiometrics.mockResolvedValue(supportsBiometric as boolean);
|
||||
innerService.needsSetup.mockResolvedValue(needsSetup as boolean);
|
||||
innerService.canAutoSetup.mockResolvedValue(canAutoSetup as boolean);
|
||||
|
||||
const actual = await sut.getBiometricsStatus();
|
||||
expect(actual).toBe(expected);
|
||||
@@ -221,7 +220,7 @@ describe("MainBiometricsService", function () {
|
||||
|
||||
await sut.setupBiometrics();
|
||||
|
||||
expect(osBiometricsService.osBiometricsSetup).toHaveBeenCalled();
|
||||
expect(osBiometricsService.runSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,7 +281,7 @@ describe("MainBiometricsService", function () {
|
||||
it("should return the biometric key if a valid key is returned", async () => {
|
||||
const userId = "test" as UserId;
|
||||
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
|
||||
const biometricKey = Utils.fromBufferToB64(new Uint8Array(64));
|
||||
const biometricKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
osBiometricsService.getBiometricKey.mockResolvedValue(biometricKey);
|
||||
|
||||
const userKey = await sut.unlockWithBiometricsForUser(userId);
|
||||
|
||||
@@ -2,8 +2,6 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -16,15 +14,13 @@ import { OsBiometricService } from "./os-biometrics.service";
|
||||
|
||||
export class MainBiometricsService extends DesktopBiometricsService {
|
||||
private osBiometricsService: OsBiometricService;
|
||||
private clientKeyHalves = new Map<string, string | null>();
|
||||
private shouldAutoPrompt = true;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
private messagingService: MessagingService,
|
||||
private platform: NodeJS.Platform,
|
||||
platform: NodeJS.Platform,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -37,6 +33,9 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
this.logService,
|
||||
this.biometricStateService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
);
|
||||
} else if (platform === "darwin") {
|
||||
// eslint-disable-next-line
|
||||
@@ -45,7 +44,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
} else if (platform === "linux") {
|
||||
// eslint-disable-next-line
|
||||
const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default;
|
||||
this.osBiometricsService = new OsBiometricsServiceLinux(this.i18nService, this.windowMain);
|
||||
this.osBiometricsService = new OsBiometricsServiceLinux(
|
||||
this.biometricStateService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
@@ -60,11 +63,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
* @returns the status of the biometrics of the platform
|
||||
*/
|
||||
async getBiometricsStatus(): Promise<BiometricsStatus> {
|
||||
if (!(await this.osBiometricsService.osSupportsBiometric())) {
|
||||
if (!(await this.osBiometricsService.supportsBiometrics())) {
|
||||
return BiometricsStatus.HardwareUnavailable;
|
||||
} else {
|
||||
if (await this.osBiometricsService.osBiometricsNeedsSetup()) {
|
||||
if (await this.osBiometricsService.osBiometricsCanAutoSetup()) {
|
||||
if (await this.osBiometricsService.needsSetup()) {
|
||||
if (await this.osBiometricsService.canAutoSetup()) {
|
||||
return BiometricsStatus.AutoSetupNeeded;
|
||||
} else {
|
||||
return BiometricsStatus.ManualSetupNeeded;
|
||||
@@ -85,20 +88,12 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
if (!(await this.biometricStateService.getBiometricUnlockEnabled(userId))) {
|
||||
return BiometricsStatus.NotEnabledLocally;
|
||||
}
|
||||
|
||||
const platformStatus = await this.getBiometricsStatus();
|
||||
if (!(platformStatus === BiometricsStatus.Available)) {
|
||||
return platformStatus;
|
||||
}
|
||||
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
if (!clientKeyHalfSatisfied) {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
|
||||
return BiometricsStatus.Available;
|
||||
return await this.osBiometricsService.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
@@ -106,7 +101,7 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async setupBiometrics(): Promise<void> {
|
||||
return await this.osBiometricsService.osBiometricsSetup();
|
||||
return await this.osBiometricsService.runSetup();
|
||||
}
|
||||
|
||||
async authenticateWithBiometrics(): Promise<boolean> {
|
||||
@@ -114,51 +109,23 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null> {
|
||||
const biometricKey = await this.osBiometricsService.getBiometricKey(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
this.clientKeyHalves.get(userId) ?? undefined,
|
||||
);
|
||||
if (biometricKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SymmetricCryptoKey.fromString(biometricKey) as UserKey;
|
||||
return (await this.osBiometricsService.getBiometricKey(userId)) as UserKey;
|
||||
}
|
||||
|
||||
async setBiometricProtectedUnlockKeyForUser(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<void> {
|
||||
const service = "Bitwarden_biometric";
|
||||
const storageKey = `${userId}_user_biometric`;
|
||||
if (!this.clientKeyHalves.has(userId)) {
|
||||
const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(key, userId);
|
||||
if (clientKeyHalf == null) {
|
||||
throw new Error("Client key half is required for biometric unlock but not set.");
|
||||
} else {
|
||||
this.clientKeyHalves.set(userId, Utils.fromBufferToB64(clientKeyHalf));
|
||||
}
|
||||
}
|
||||
|
||||
return await this.osBiometricsService.setBiometricKey(
|
||||
service,
|
||||
storageKey,
|
||||
key.toBase64(),
|
||||
this.clientKeyHalves.get(userId) ?? undefined,
|
||||
);
|
||||
return await this.osBiometricsService.setBiometricKey(userId, key);
|
||||
}
|
||||
|
||||
async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> {
|
||||
return await this.osBiometricsService.deleteBiometricKey(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
);
|
||||
return await this.osBiometricsService.deleteBiometricKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to auto-prompt the user for biometric unlock; this can be used to prevent auto-prompting being initiated by a process reload.
|
||||
* Reasons for enabling auto prompt include: Starting the app, un-minimizing the app, manually account switching
|
||||
* Reasons for enabling auto mpt include: Starting the app, un-minimizing the app, manually account switching
|
||||
* @param value Whether to auto-prompt the user for biometric unlock
|
||||
*/
|
||||
async setShouldAutopromptNow(value: boolean): Promise<void> {
|
||||
@@ -176,30 +143,4 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
async canEnableBiometricUnlock(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getOrCreateBiometricEncryptionClientKeyHalf(
|
||||
key: SymmetricCryptoKey,
|
||||
userId: UserId,
|
||||
): Promise<Uint8Array | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: Uint8Array | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
const encKey = await this.encryptService.encryptBytes(keyBytes, key);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { isFlatpak, isLinux, isSnapStore } from "../../utils";
|
||||
|
||||
import { OsBiometricService } from "./os-biometrics.service";
|
||||
@@ -28,59 +32,62 @@ const polkitPolicy = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
const policyFileName = "com.bitwarden.Bitwarden.policy";
|
||||
const policyPath = "/usr/share/polkit-1/actions/";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
constructor(
|
||||
private i18nservice: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
private _iv: string | null = null;
|
||||
// Use getKeyMaterial helper instead of direct access
|
||||
private _osKeyHalf: string | null = null;
|
||||
private clientKeyHalves = new Map<string, Uint8Array | null>();
|
||||
|
||||
async setBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
value: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<void> {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
const clientKeyPartB64 = Utils.fromBufferToB64(
|
||||
await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key),
|
||||
);
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
await biometrics.setBiometricSecret(
|
||||
service,
|
||||
key,
|
||||
value,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
key.toBase64(),
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
}
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
await passwords.deletePassword(service, key);
|
||||
async deleteBiometricKey(userId: UserId): Promise<void> {
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
|
||||
}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<string | null> {
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const success = await this.authenticateBiometric();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Biometric authentication failed");
|
||||
}
|
||||
|
||||
const value = await passwords.getPassword(service, storageKey);
|
||||
const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
|
||||
if (value == null || value == "") {
|
||||
return null;
|
||||
} else {
|
||||
const clientKeyHalf = this.clientKeyHalves.get(userId.toString());
|
||||
const clientKeyPartB64 = Utils.fromBufferToB64(clientKeyHalf);
|
||||
const encValue = new EncString(value);
|
||||
this.setIv(encValue.iv);
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
const storedValue = await biometrics.getBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
storageDetails.key_material,
|
||||
);
|
||||
return storedValue;
|
||||
return SymmetricCryptoKey.fromString(storedValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +97,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
// We assume all linux distros have some polkit implementation
|
||||
// that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup.
|
||||
// Snap does not have access at the moment to polkit
|
||||
@@ -100,7 +107,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return await passwords.isAvailable();
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup(): Promise<boolean> {
|
||||
async needsSetup(): Promise<boolean> {
|
||||
if (isSnapStore()) {
|
||||
return false;
|
||||
}
|
||||
@@ -110,7 +117,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
// We cannot auto setup on snap or flatpak since the filesystem is sandboxed.
|
||||
// The user needs to manually set up the polkit policy outside of the sandbox
|
||||
// since we allow access to polkit via dbus for the sandboxed clients, the authentication works from
|
||||
@@ -118,7 +125,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return isLinux() && !isSnapStore() && !isFlatpak();
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {
|
||||
async runSetup(): Promise<void> {
|
||||
const process = spawn("pkexec", [
|
||||
"bash",
|
||||
"-c",
|
||||
@@ -167,4 +174,46 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
ivB64: this._iv,
|
||||
};
|
||||
}
|
||||
|
||||
private async getOrCreateBiometricEncryptionClientKeyHalf(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
return this.clientKeyHalves.get(userId) || null;
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: Uint8Array | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
const encKey = await this.encryptService.encryptBytes(keyBytes, key);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
this.clientKeyHalves.set(userId.toString(), clientKeyHalf);
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
if (!clientKeyHalfSatisfied) {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { systemPreferences } from "electron";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
import { OsBiometricService } from "./os-biometrics.service";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceMac implements OsBiometricService {
|
||||
constructor(private i18nservice: I18nService) {}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return systemPreferences.canPromptTouchID();
|
||||
}
|
||||
|
||||
@@ -21,44 +29,52 @@ export default class OsBiometricsServiceMac implements OsBiometricService {
|
||||
}
|
||||
}
|
||||
|
||||
async getBiometricKey(service: string, key: string): Promise<string | null> {
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const success = await this.authenticateBiometric();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Biometric authentication failed");
|
||||
}
|
||||
const keyB64 = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
if (keyB64 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await passwords.getPassword(service, key);
|
||||
return SymmetricCryptoKey.fromString(keyB64);
|
||||
}
|
||||
|
||||
async setBiometricKey(service: string, key: string, value: string): Promise<void> {
|
||||
if (await this.valueUpToDate(service, key, value)) {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
if (await this.valueUpToDate(userId, key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await passwords.setPassword(service, key, value);
|
||||
return await passwords.setPassword(SERVICE, getLookupKeyForUser(userId), key.toBase64());
|
||||
}
|
||||
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
return await passwords.deletePassword(service, key);
|
||||
async deleteBiometricKey(user: UserId): Promise<void> {
|
||||
return await passwords.deletePassword(SERVICE, getLookupKeyForUser(user));
|
||||
}
|
||||
|
||||
private async valueUpToDate(service: string, key: string, value: string): Promise<boolean> {
|
||||
private async valueUpToDate(user: UserId, key: SymmetricCryptoKey): Promise<boolean> {
|
||||
try {
|
||||
const existing = await passwords.getPassword(service, key);
|
||||
return existing === value;
|
||||
const existing = await passwords.getPassword(SERVICE, getLookupKeyForUser(user));
|
||||
return existing === key.toBase64();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
async needsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
async runSetup(): Promise<void> {}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
|
||||
@@ -13,87 +17,103 @@ import { OsBiometricService } from "./os-biometrics.service";
|
||||
const KEY_WITNESS_SUFFIX = "_witness";
|
||||
const WITNESS_VALUE = "known key";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceWindows 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;
|
||||
private clientKeyHalves = new Map<string, Uint8Array | null>();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return await biometrics.available();
|
||||
}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
clientKeyHalfB64: string,
|
||||
): Promise<string | null> {
|
||||
const value = await passwords.getPassword(service, storageKey);
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
|
||||
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,
|
||||
clientKeyHalfB64: null, // todo fix
|
||||
});
|
||||
|
||||
await biometrics.setBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
value,
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
return value;
|
||||
return SymmetricCryptoKey.fromString(value);
|
||||
} else {
|
||||
const encValue = new EncString(value);
|
||||
this.setIv(encValue.iv);
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64,
|
||||
clientKeyHalfB64: null, // todo fix
|
||||
});
|
||||
return await biometrics.getBiometricSecret(service, storageKey, storageDetails.key_material);
|
||||
return SymmetricCryptoKey.fromString(
|
||||
await biometrics.getBiometricSecret(
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
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 })) {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
|
||||
if (
|
||||
await this.valueUpToDate({
|
||||
value: key,
|
||||
clientKeyPartB64: Utils.fromBufferToB64(clientKeyHalf),
|
||||
service: SERVICE,
|
||||
storageKey: getLookupKeyForUser(userId),
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf),
|
||||
});
|
||||
const storedValue = await biometrics.setBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
value,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
key.toBase64(),
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
const parsedStoredValue = new EncString(storedValue);
|
||||
await this.storeValueWitness(
|
||||
parsedValue,
|
||||
key,
|
||||
parsedStoredValue,
|
||||
service,
|
||||
storageKey,
|
||||
clientKeyPartB64,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
Utils.fromBufferToB64(clientKeyHalf),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
await passwords.deletePassword(service, key);
|
||||
await passwords.deletePassword(service, key + KEY_WITNESS_SUFFIX);
|
||||
async deleteBiometricKey(userId: UserId): Promise<void> {
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId) + KEY_WITNESS_SUFFIX);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
@@ -240,13 +260,55 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
return result;
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
async needsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
async runSetup(): Promise<void> {}
|
||||
|
||||
private async getOrCreateBiometricEncryptionClientKeyHalf(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId.toString())) {
|
||||
return this.clientKeyHalves.get(userId.toString());
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: Uint8Array | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
const encKey = await this.encryptService.encryptBytes(keyBytes, key);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
this.clientKeyHalves.set(userId.toString(), clientKeyHalf);
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
if (!clientKeyHalfSatisfied) {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
export interface OsBiometricService {
|
||||
osSupportsBiometric(): Promise<boolean>;
|
||||
supportsBiometrics(): Promise<boolean>;
|
||||
/**
|
||||
* Check whether support for biometric unlock requires setup. This can be automatic or manual.
|
||||
*
|
||||
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
|
||||
*/
|
||||
osBiometricsNeedsSetup: () => Promise<boolean>;
|
||||
needsSetup: () => Promise<boolean>;
|
||||
/**
|
||||
* Check whether biometrics can be automatically setup, or requires user interaction.
|
||||
*
|
||||
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
|
||||
*/
|
||||
osBiometricsCanAutoSetup: () => Promise<boolean>;
|
||||
canAutoSetup: () => Promise<boolean>;
|
||||
/**
|
||||
* Starts automatic biometric setup, which places the required configuration files / changes the required settings.
|
||||
*/
|
||||
osBiometricsSetup: () => Promise<void>;
|
||||
runSetup: () => Promise<void>;
|
||||
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>;
|
||||
getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null>;
|
||||
setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void>;
|
||||
deleteBiometricKey(userId: UserId): Promise<void>;
|
||||
getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus>;
|
||||
}
|
||||
|
||||
@@ -192,7 +192,6 @@ export class Main {
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
this.logService,
|
||||
this.messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
|
||||
Reference in New Issue
Block a user