1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00:00

[PM-22745] Move clientkeyhalf to os impl (#15140)

* Move clientkeyhalf to main

* Move clientkeyhalf to os platform implementation

* Cleanup

* Fix tests

* Tests

* Add tests

* Add tests

* Fix types

* Undo linux debugging changes

* Fix typo

* Update apps/desktop/src/key-management/biometrics/os-biometrics.service.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update apps/desktop/src/key-management/biometrics/os-biometrics.service.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update apps/desktop/src/key-management/biometrics/os-biometrics.service.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix build

---------

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2025-06-17 13:12:15 +02:00
committed by GitHub
parent 1dd7eae466
commit 0ce4a2ce39
15 changed files with 481 additions and 382 deletions

View File

@@ -1,3 +1,4 @@
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { BiometricsService } from "@bitwarden/key-management"; import { BiometricsService } from "@bitwarden/key-management";
@@ -6,10 +7,10 @@ import { BiometricsService } from "@bitwarden/key-management";
* specifically for the main process. * specifically for the main process.
*/ */
export abstract class DesktopBiometricsService extends BiometricsService { export abstract class DesktopBiometricsService extends BiometricsService {
abstract setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void>; abstract setBiometricProtectedUnlockKeyForUser(
userId: UserId,
value: SymmetricCryptoKey,
): Promise<void>;
abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void>; abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void>;
abstract setupBiometrics(): Promise<void>; abstract setupBiometrics(): Promise<void>;
abstract setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void>;
} }

View File

@@ -1,5 +1,6 @@
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -37,17 +38,12 @@ export class MainBiometricsIPCListener {
} }
return await this.biometricService.setBiometricProtectedUnlockKeyForUser( return await this.biometricService.setBiometricProtectedUnlockKeyForUser(
message.userId as UserId, message.userId as UserId,
message.key, SymmetricCryptoKey.fromString(message.key),
); );
case BiometricAction.RemoveKeyForUser: case BiometricAction.RemoveKeyForUser:
return await this.biometricService.deleteBiometricUnlockKeyForUser( return await this.biometricService.deleteBiometricUnlockKeyForUser(
message.userId as UserId, message.userId as UserId,
); );
case BiometricAction.SetClientKeyHalf:
return await this.biometricService.setClientKeyHalfForUser(
message.userId as UserId,
message.key,
);
case BiometricAction.Setup: case BiometricAction.Setup:
return await this.biometricService.setupBiometrics(); return await this.biometricService.setupBiometrics();

View File

@@ -1,10 +1,10 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; 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 { 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 { UserId } from "@bitwarden/common/types/guid";
import { import {
BiometricsService, BiometricsService,
@@ -13,6 +13,7 @@ import {
} from "@bitwarden/key-management"; } from "@bitwarden/key-management";
import { WindowMain } from "../../main/window.main"; import { WindowMain } from "../../main/window.main";
import { MainCryptoFunctionService } from "../../platform/main/main-crypto-function.service";
import { MainBiometricsService } from "./main-biometrics.service"; import { MainBiometricsService } from "./main-biometrics.service";
import OsBiometricsServiceLinux from "./os-biometrics-linux.service"; import OsBiometricsServiceLinux from "./os-biometrics-linux.service";
@@ -27,21 +28,25 @@ jest.mock("@bitwarden/desktop-napi", () => {
}; };
}); });
const unlockKey = new SymmetricCryptoKey(new Uint8Array(64));
describe("MainBiometricsService", function () { describe("MainBiometricsService", function () {
const i18nService = mock<I18nService>(); const i18nService = mock<I18nService>();
const windowMain = mock<WindowMain>(); const windowMain = mock<WindowMain>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const messagingService = mock<MessagingService>();
const biometricStateService = mock<BiometricStateService>(); const biometricStateService = mock<BiometricStateService>();
const cryptoFunctionService = mock<MainCryptoFunctionService>();
const encryptService = mock<EncryptService>();
it("Should call the platformspecific methods", async () => { it("Should call the platformspecific methods", async () => {
const sut = new MainBiometricsService( const sut = new MainBiometricsService(
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const mockService = mock<OsBiometricService>(); const mockService = mock<OsBiometricService>();
@@ -57,9 +62,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
"win32", "win32",
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const internalService = (sut as any).osBiometricsService; const internalService = (sut as any).osBiometricsService;
@@ -72,9 +78,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
"darwin", "darwin",
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const internalService = (sut as any).osBiometricsService; const internalService = (sut as any).osBiometricsService;
expect(internalService).not.toBeNull(); expect(internalService).not.toBeNull();
@@ -86,9 +93,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
"linux", "linux",
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const internalService = (sut as any).osBiometricsService; const internalService = (sut as any).osBiometricsService;
@@ -106,9 +114,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
innerService = mock(); innerService = mock();
@@ -131,9 +140,9 @@ describe("MainBiometricsService", function () {
]; ];
for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) { for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) {
innerService.osSupportsBiometric.mockResolvedValue(supportsBiometric as boolean); innerService.supportsBiometrics.mockResolvedValue(supportsBiometric as boolean);
innerService.osBiometricsNeedsSetup.mockResolvedValue(needsSetup as boolean); innerService.needsSetup.mockResolvedValue(needsSetup as boolean);
innerService.osBiometricsCanAutoSetup.mockResolvedValue(canAutoSetup as boolean); innerService.canAutoSetup.mockResolvedValue(canAutoSetup as boolean);
const actual = await sut.getBiometricsStatus(); const actual = await sut.getBiometricsStatus();
expect(actual).toBe(expected); expect(actual).toBe(expected);
@@ -175,12 +184,23 @@ describe("MainBiometricsService", function () {
biometricStateService.getRequirePasswordOnStart.mockResolvedValue( biometricStateService.getRequirePasswordOnStart.mockResolvedValue(
requirePasswordOnStart as boolean, requirePasswordOnStart as boolean,
); );
(sut as any).clientKeyHalves = new Map(); if (!requirePasswordOnStart) {
const userId = "test" as UserId; (sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
if (hasKeyHalf) { .fn()
(sut as any).clientKeyHalves.set(userId, "test"); .mockResolvedValue(BiometricsStatus.Available);
} else {
if (hasKeyHalf) {
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
.fn()
.mockResolvedValue(BiometricsStatus.Available);
} else {
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
.fn()
.mockResolvedValue(BiometricsStatus.UnlockNeeded);
}
} }
const userId = "test" as UserId;
const actual = await sut.getBiometricsStatusForUser(userId); const actual = await sut.getBiometricsStatusForUser(userId);
expect(actual).toBe(expected); expect(actual).toBe(expected);
} }
@@ -193,50 +213,17 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const osBiometricsService = mock<OsBiometricService>(); const osBiometricsService = mock<OsBiometricService>();
(sut as any).osBiometricsService = osBiometricsService; (sut as any).osBiometricsService = osBiometricsService;
await sut.setupBiometrics(); await sut.setupBiometrics();
expect(osBiometricsService.osBiometricsSetup).toHaveBeenCalled(); expect(osBiometricsService.runSetup).toHaveBeenCalled();
});
});
describe("setClientKeyHalfForUser", () => {
let sut: MainBiometricsService;
beforeEach(() => {
sut = new MainBiometricsService(
i18nService,
windowMain,
logService,
messagingService,
process.platform,
biometricStateService,
);
});
it("should set the client key half for the user", async () => {
const userId = "test" as UserId;
const keyHalf = "testKeyHalf";
await sut.setClientKeyHalfForUser(userId, keyHalf);
expect((sut as any).clientKeyHalves.has(userId)).toBe(true);
expect((sut as any).clientKeyHalves.get(userId)).toBe(keyHalf);
});
it("should reset the client key half for the user", async () => {
const userId = "test" as UserId;
await sut.setClientKeyHalfForUser(userId, null);
expect((sut as any).clientKeyHalves.has(userId)).toBe(true);
expect((sut as any).clientKeyHalves.get(userId)).toBe(null);
}); });
}); });
@@ -246,9 +233,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const osBiometricsService = mock<OsBiometricService>(); const osBiometricsService = mock<OsBiometricService>();
(sut as any).osBiometricsService = osBiometricsService; (sut as any).osBiometricsService = osBiometricsService;
@@ -268,9 +256,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
osBiometricsService = mock<OsBiometricService>(); osBiometricsService = mock<OsBiometricService>();
(sut as any).osBiometricsService = osBiometricsService; (sut as any).osBiometricsService = osBiometricsService;
@@ -278,34 +267,24 @@ describe("MainBiometricsService", function () {
it("should return null if no biometric key is returned ", async () => { it("should return null if no biometric key is returned ", async () => {
const userId = "test" as UserId; const userId = "test" as UserId;
(sut as any).clientKeyHalves.set(userId, "testKeyHalf"); osBiometricsService.getBiometricKey.mockResolvedValue(null);
const userKey = await sut.unlockWithBiometricsForUser(userId); const userKey = await sut.unlockWithBiometricsForUser(userId);
expect(userKey).toBeNull(); expect(userKey).toBeNull();
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId);
"Bitwarden_biometric",
`${userId}_user_biometric`,
"testKeyHalf",
);
}); });
it("should return the biometric key if a valid key is returned", async () => { it("should return the biometric key if a valid key is returned", async () => {
const userId = "test" as UserId; const userId = "test" as UserId;
(sut as any).clientKeyHalves.set(userId, "testKeyHalf"); const biometricKey = new SymmetricCryptoKey(new Uint8Array(64));
const biometricKey = Utils.fromBufferToB64(new Uint8Array(64));
osBiometricsService.getBiometricKey.mockResolvedValue(biometricKey); osBiometricsService.getBiometricKey.mockResolvedValue(biometricKey);
const userKey = await sut.unlockWithBiometricsForUser(userId); const userKey = await sut.unlockWithBiometricsForUser(userId);
expect(userKey).not.toBeNull(); expect(userKey).not.toBeNull();
expect(userKey!.keyB64).toBe(biometricKey); expect(userKey!.keyB64).toBe(biometricKey.toBase64());
expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64);
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId);
"Bitwarden_biometric",
`${userId}_user_biometric`,
"testKeyHalf",
);
}); });
}); });
@@ -318,37 +297,21 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
osBiometricsService = mock<OsBiometricService>(); osBiometricsService = mock<OsBiometricService>();
(sut as any).osBiometricsService = osBiometricsService; (sut as any).osBiometricsService = osBiometricsService;
}); });
it("should throw an error if no client key half is provided", async () => {
const userId = "test" as UserId;
const unlockKey = "testUnlockKey";
await expect(sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey)).rejects.toThrow(
"No client key half provided for user",
);
});
it("should call the platform specific setBiometricKey method", async () => { it("should call the platform specific setBiometricKey method", async () => {
const userId = "test" as UserId; const userId = "test" as UserId;
const unlockKey = "testUnlockKey";
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey); await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey);
expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith( expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(userId, unlockKey);
"Bitwarden_biometric",
`${userId}_user_biometric`,
unlockKey,
"testKeyHalf",
);
}); });
}); });
@@ -358,9 +321,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const osBiometricsService = mock<OsBiometricService>(); const osBiometricsService = mock<OsBiometricService>();
(sut as any).osBiometricsService = osBiometricsService; (sut as any).osBiometricsService = osBiometricsService;
@@ -369,10 +333,7 @@ describe("MainBiometricsService", function () {
await sut.deleteBiometricUnlockKeyForUser(userId); await sut.deleteBiometricUnlockKeyForUser(userId);
expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith( expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(userId);
"Bitwarden_biometric",
`${userId}_user_biometric`,
);
}); });
}); });
@@ -384,9 +345,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
}); });
@@ -413,9 +375,10 @@ describe("MainBiometricsService", function () {
i18nService, i18nService,
windowMain, windowMain,
logService, logService,
messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
cryptoFunctionService,
); );
const shouldAutoPrompt = await sut.getShouldAutopromptNow(); const shouldAutoPrompt = await sut.getShouldAutopromptNow();

View File

@@ -1,6 +1,7 @@
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
@@ -13,16 +14,16 @@ import { OsBiometricService } from "./os-biometrics.service";
export class MainBiometricsService extends DesktopBiometricsService { export class MainBiometricsService extends DesktopBiometricsService {
private osBiometricsService: OsBiometricService; private osBiometricsService: OsBiometricService;
private clientKeyHalves = new Map<string, string | null>();
private shouldAutoPrompt = true; private shouldAutoPrompt = true;
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
private windowMain: WindowMain, private windowMain: WindowMain,
private logService: LogService, private logService: LogService,
private messagingService: MessagingService, platform: NodeJS.Platform,
private platform: NodeJS.Platform,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private encryptService: EncryptService,
private cryptoFunctionService: CryptoFunctionService,
) { ) {
super(); super();
if (platform === "win32") { if (platform === "win32") {
@@ -32,6 +33,9 @@ export class MainBiometricsService extends DesktopBiometricsService {
this.i18nService, this.i18nService,
this.windowMain, this.windowMain,
this.logService, this.logService,
this.biometricStateService,
this.encryptService,
this.cryptoFunctionService,
); );
} else if (platform === "darwin") { } else if (platform === "darwin") {
// eslint-disable-next-line // eslint-disable-next-line
@@ -40,7 +44,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
} else if (platform === "linux") { } else if (platform === "linux") {
// eslint-disable-next-line // eslint-disable-next-line
const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default; 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 { } else {
throw new Error("Unsupported platform"); throw new Error("Unsupported platform");
} }
@@ -55,11 +63,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
* @returns the status of the biometrics of the platform * @returns the status of the biometrics of the platform
*/ */
async getBiometricsStatus(): Promise<BiometricsStatus> { async getBiometricsStatus(): Promise<BiometricsStatus> {
if (!(await this.osBiometricsService.osSupportsBiometric())) { if (!(await this.osBiometricsService.supportsBiometrics())) {
return BiometricsStatus.HardwareUnavailable; return BiometricsStatus.HardwareUnavailable;
} else { } else {
if (await this.osBiometricsService.osBiometricsNeedsSetup()) { if (await this.osBiometricsService.needsSetup()) {
if (await this.osBiometricsService.osBiometricsCanAutoSetup()) { if (await this.osBiometricsService.canAutoSetup()) {
return BiometricsStatus.AutoSetupNeeded; return BiometricsStatus.AutoSetupNeeded;
} else { } else {
return BiometricsStatus.ManualSetupNeeded; return BiometricsStatus.ManualSetupNeeded;
@@ -80,20 +88,12 @@ export class MainBiometricsService extends DesktopBiometricsService {
if (!(await this.biometricStateService.getBiometricUnlockEnabled(userId))) { if (!(await this.biometricStateService.getBiometricUnlockEnabled(userId))) {
return BiometricsStatus.NotEnabledLocally; return BiometricsStatus.NotEnabledLocally;
} }
const platformStatus = await this.getBiometricsStatus(); const platformStatus = await this.getBiometricsStatus();
if (!(platformStatus === BiometricsStatus.Available)) { if (!(platformStatus === BiometricsStatus.Available)) {
return platformStatus; return platformStatus;
} }
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); return await this.osBiometricsService.getBiometricsFirstUnlockStatusForUser(userId);
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
if (!clientKeyHalfSatisfied) {
return BiometricsStatus.UnlockNeeded;
}
return BiometricsStatus.Available;
} }
async authenticateBiometric(): Promise<boolean> { async authenticateBiometric(): Promise<boolean> {
@@ -101,11 +101,7 @@ export class MainBiometricsService extends DesktopBiometricsService {
} }
async setupBiometrics(): Promise<void> { async setupBiometrics(): Promise<void> {
return await this.osBiometricsService.osBiometricsSetup(); return await this.osBiometricsService.runSetup();
}
async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void> {
this.clientKeyHalves.set(userId, value);
} }
async authenticateWithBiometrics(): Promise<boolean> { async authenticateWithBiometrics(): Promise<boolean> {
@@ -113,43 +109,23 @@ export class MainBiometricsService extends DesktopBiometricsService {
} }
async unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null> { async unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null> {
const biometricKey = await this.osBiometricsService.getBiometricKey( return (await this.osBiometricsService.getBiometricKey(userId)) as UserKey;
"Bitwarden_biometric",
`${userId}_user_biometric`,
this.clientKeyHalves.get(userId) ?? undefined,
);
if (biometricKey == null) {
return null;
}
return SymmetricCryptoKey.fromString(biometricKey) as UserKey;
} }
async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void> { async setBiometricProtectedUnlockKeyForUser(
const service = "Bitwarden_biometric"; userId: UserId,
const storageKey = `${userId}_user_biometric`; key: SymmetricCryptoKey,
if (!this.clientKeyHalves.has(userId)) { ): Promise<void> {
throw new Error("No client key half provided for user"); return await this.osBiometricsService.setBiometricKey(userId, key);
}
return await this.osBiometricsService.setBiometricKey(
service,
storageKey,
value,
this.clientKeyHalves.get(userId) ?? undefined,
);
} }
async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> { async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> {
return await this.osBiometricsService.deleteBiometricKey( return await this.osBiometricsService.deleteBiometricKey(userId);
"Bitwarden_biometric",
`${userId}_user_biometric`,
);
} }
/** /**
* Set whether to auto-prompt the user for biometric unlock; this can be used to prevent auto-prompting being initiated by a process reload. * 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-prompt include: Starting the app, un-minimizing the app, manually account switching
* @param value Whether to auto-prompt the user for biometric unlock * @param value Whether to auto-prompt the user for biometric unlock
*/ */
async setShouldAutopromptNow(value: boolean): Promise<void> { async setShouldAutopromptNow(value: boolean): Promise<void> {

View File

@@ -1,10 +1,14 @@
import { spawn } from "child_process"; 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 { 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 { 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 { isFlatpak, isLinux, isSnapStore } from "../../utils";
import { OsBiometricService } from "./os-biometrics.service"; 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 policyFileName = "com.bitwarden.Bitwarden.policy";
const policyPath = "/usr/share/polkit-1/actions/"; 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 { export default class OsBiometricsServiceLinux implements OsBiometricService {
constructor( constructor(
private i18nservice: I18nService, private biometricStateService: BiometricStateService,
private windowMain: WindowMain, private encryptService: EncryptService,
private cryptoFunctionService: CryptoFunctionService,
) {} ) {}
private _iv: string | null = null; private _iv: string | null = null;
// Use getKeyMaterial helper instead of direct access // Use getKeyMaterial helper instead of direct access
private _osKeyHalf: string | null = null; private _osKeyHalf: string | null = null;
private clientKeyHalves = new Map<UserId, Uint8Array | null>();
async setBiometricKey( async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
service: string, const clientKeyPartB64 = Utils.fromBufferToB64(
key: string, await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key),
value: string, );
clientKeyPartB64: string | undefined,
): Promise<void> {
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
await biometrics.setBiometricSecret( await biometrics.setBiometricSecret(
service, SERVICE,
key, getLookupKeyForUser(userId),
value, key.toBase64(),
storageDetails.key_material, storageDetails.key_material,
storageDetails.ivB64, storageDetails.ivB64,
); );
} }
async deleteBiometricKey(service: string, key: string): Promise<void> { async deleteBiometricKey(userId: UserId): Promise<void> {
await passwords.deletePassword(service, key); await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
} }
async getBiometricKey( async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
service: string,
storageKey: string,
clientKeyPartB64: string | undefined,
): Promise<string | null> {
const success = await this.authenticateBiometric(); const success = await this.authenticateBiometric();
if (!success) { if (!success) {
throw new Error("Biometric authentication failed"); 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 == "") { if (value == null || value == "") {
return null; return null;
} else { } else {
const clientKeyHalf = this.clientKeyHalves.get(userId);
const clientKeyPartB64 = Utils.fromBufferToB64(clientKeyHalf);
const encValue = new EncString(value); const encValue = new EncString(value);
this.setIv(encValue.iv); this.setIv(encValue.iv);
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
const storedValue = await biometrics.getBiometricSecret( const storedValue = await biometrics.getBiometricSecret(
service, SERVICE,
storageKey, getLookupKeyForUser(userId),
storageDetails.key_material, storageDetails.key_material,
); );
return storedValue; return SymmetricCryptoKey.fromString(storedValue);
} }
} }
@@ -89,7 +96,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
return await biometrics.prompt(hwnd, ""); return await biometrics.prompt(hwnd, "");
} }
async osSupportsBiometric(): Promise<boolean> { async supportsBiometrics(): Promise<boolean> {
// We assume all linux distros have some polkit implementation // We assume all linux distros have some polkit implementation
// that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup. // that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup.
// Snap does not have access at the moment to polkit // Snap does not have access at the moment to polkit
@@ -99,7 +106,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
return await passwords.isAvailable(); return await passwords.isAvailable();
} }
async osBiometricsNeedsSetup(): Promise<boolean> { async needsSetup(): Promise<boolean> {
if (isSnapStore()) { if (isSnapStore()) {
return false; return false;
} }
@@ -108,7 +115,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
return !(await biometrics.available()); return !(await biometrics.available());
} }
async osBiometricsCanAutoSetup(): Promise<boolean> { async canAutoSetup(): Promise<boolean> {
// We cannot auto setup on snap or flatpak since the filesystem is sandboxed. // 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 // 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 // since we allow access to polkit via dbus for the sandboxed clients, the authentication works from
@@ -116,7 +123,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
return isLinux() && !isSnapStore() && !isFlatpak(); return isLinux() && !isSnapStore() && !isFlatpak();
} }
async osBiometricsSetup(): Promise<void> { async runSetup(): Promise<void> {
const process = spawn("pkexec", [ const process = spawn("pkexec", [
"bash", "bash",
"-c", "-c",
@@ -165,4 +172,46 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
ivB64: this._iv, 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, 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;
}
} }

View File

@@ -1,14 +1,22 @@
import { systemPreferences } from "electron"; import { systemPreferences } from "electron";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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 { passwords } from "@bitwarden/desktop-napi";
import { BiometricsStatus } from "@bitwarden/key-management";
import { OsBiometricService } from "./os-biometrics.service"; 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 { export default class OsBiometricsServiceMac implements OsBiometricService {
constructor(private i18nservice: I18nService) {} constructor(private i18nservice: I18nService) {}
async osSupportsBiometric(): Promise<boolean> { async supportsBiometrics(): Promise<boolean> {
return systemPreferences.canPromptTouchID(); 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(); const success = await this.authenticateBiometric();
if (!success) { if (!success) {
throw new Error("Biometric authentication failed"); 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> { async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
if (await this.valueUpToDate(service, key, value)) { if (await this.valueUpToDate(userId, key)) {
return; 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> { async deleteBiometricKey(user: UserId): Promise<void> {
return await passwords.deletePassword(service, key); 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 { try {
const existing = await passwords.getPassword(service, key); const existing = await passwords.getPassword(SERVICE, getLookupKeyForUser(user));
return existing === value; return existing === key.toBase64();
} catch { } catch {
return false; return false;
} }
} }
async osBiometricsNeedsSetup() { async needsSetup() {
return false; return false;
} }
async osBiometricsCanAutoSetup(): Promise<boolean> { async canAutoSetup(): Promise<boolean> {
return false; return false;
} }
async osBiometricsSetup(): Promise<void> {} async runSetup(): Promise<void> {}
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
return BiometricsStatus.Available;
}
} }

View File

@@ -0,0 +1,143 @@
import { mock } from "jest-mock-extended";
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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
import OsBiometricsServiceWindows from "./os-biometrics-windows.service";
jest.mock("@bitwarden/desktop-napi", () => ({
biometrics: {
available: jest.fn(),
setBiometricSecret: jest.fn(),
getBiometricSecret: jest.fn(),
deriveKeyMaterial: jest.fn(),
prompt: jest.fn(),
},
passwords: {
getPassword: jest.fn(),
deletePassword: jest.fn(),
},
}));
describe("OsBiometricsServiceWindows", () => {
let service: OsBiometricsServiceWindows;
let biometricStateService: BiometricStateService;
beforeEach(() => {
const i18nService = mock<I18nService>();
const logService = mock<LogService>();
biometricStateService = mock<BiometricStateService>();
const encryptionService = mock<EncryptService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
service = new OsBiometricsServiceWindows(
i18nService,
null,
logService,
biometricStateService,
encryptionService,
cryptoFunctionService,
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("getBiometricsFirstUnlockStatusForUser", () => {
const userId = "test-user-id" as UserId;
it("should return Available when requirePasswordOnRestart is false", async () => {
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false);
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
expect(result).toBe(BiometricsStatus.Available);
});
it("should return Available when requirePasswordOnRestart is true and client key half is set", async () => {
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
(service as any).clientKeyHalves = new Map<string, Uint8Array>();
(service as any).clientKeyHalves.set(userId, new Uint8Array([1, 2, 3, 4]));
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
expect(result).toBe(BiometricsStatus.Available);
});
it("should return UnlockNeeded when requirePasswordOnRestart is true and client key half is not set", async () => {
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
(service as any).clientKeyHalves = new Map<string, Uint8Array>();
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
expect(result).toBe(BiometricsStatus.UnlockNeeded);
});
});
describe("getOrCreateBiometricEncryptionClientKeyHalf", () => {
const userId = "test-user-id" as UserId;
const key = new SymmetricCryptoKey(new Uint8Array(64));
let encryptionService: EncryptService;
let cryptoFunctionService: CryptoFunctionService;
beforeEach(() => {
encryptionService = mock<EncryptService>();
cryptoFunctionService = mock<CryptoFunctionService>();
service = new OsBiometricsServiceWindows(
mock<I18nService>(),
null,
mock<LogService>(),
biometricStateService,
encryptionService,
cryptoFunctionService,
);
});
it("should return null if getRequirePasswordOnRestart is false", async () => {
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false);
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
expect(result).toBeNull();
});
it("should return cached key half if already present", async () => {
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
const cachedKeyHalf = new Uint8Array([10, 20, 30]);
(service as any).clientKeyHalves.set(userId.toString(), cachedKeyHalf);
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
expect(result).toBe(cachedKeyHalf);
});
it("should decrypt and return existing encrypted client key half", async () => {
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
biometricStateService.getEncryptedClientKeyHalf = jest
.fn()
.mockResolvedValue(new Uint8Array([1, 2, 3]));
const decrypted = new Uint8Array([4, 5, 6]);
encryptionService.decryptBytes = jest.fn().mockResolvedValue(decrypted);
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(userId);
expect(encryptionService.decryptBytes).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]), key);
expect(result).toEqual(decrypted);
expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(decrypted);
});
it("should generate, encrypt, store, and cache a new key half if none exists", async () => {
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
biometricStateService.getEncryptedClientKeyHalf = jest.fn().mockResolvedValue(null);
const randomBytes = new Uint8Array([7, 8, 9]);
cryptoFunctionService.randomBytes = jest.fn().mockResolvedValue(randomBytes);
const encrypted = new Uint8Array([10, 11, 12]);
encryptionService.encryptBytes = jest.fn().mockResolvedValue(encrypted);
biometricStateService.setEncryptedClientKeyHalf = jest.fn().mockResolvedValue(undefined);
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32);
expect(encryptionService.encryptBytes).toHaveBeenCalledWith(randomBytes, key);
expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(
encrypted,
userId,
);
expect(result).toBeNull();
expect((service as any).clientKeyHalves.get(userId.toString())).toBeNull();
});
});
});

View File

@@ -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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncryptionType } from "@bitwarden/common/platform/enums"; import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; 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 { biometrics, passwords } from "@bitwarden/desktop-napi";
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
import { WindowMain } from "../../main/window.main"; import { WindowMain } from "../../main/window.main";
@@ -13,87 +17,107 @@ import { OsBiometricService } from "./os-biometrics.service";
const KEY_WITNESS_SUFFIX = "_witness"; const KEY_WITNESS_SUFFIX = "_witness";
const WITNESS_VALUE = "known key"; const WITNESS_VALUE = "known key";
const SERVICE = "Bitwarden_biometric";
function getLookupKeyForUser(userId: UserId): string {
return `${userId}_user_biometric`;
}
export default class OsBiometricsServiceWindows implements OsBiometricService { export default class OsBiometricsServiceWindows implements OsBiometricService {
// Use set helper method instead of direct access // Use set helper method instead of direct access
private _iv: string | null = null; private _iv: string | null = null;
// Use getKeyMaterial helper instead of direct access // Use getKeyMaterial helper instead of direct access
private _osKeyHalf: string | null = null; private _osKeyHalf: string | null = null;
private clientKeyHalves = new Map<UserId, Uint8Array>();
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
private windowMain: WindowMain, private windowMain: WindowMain,
private logService: LogService, private logService: LogService,
private biometricStateService: BiometricStateService,
private encryptService: EncryptService,
private cryptoFunctionService: CryptoFunctionService,
) {} ) {}
async osSupportsBiometric(): Promise<boolean> { async supportsBiometrics(): Promise<boolean> {
return await biometrics.available(); return await biometrics.available();
} }
async getBiometricKey( async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
service: string, const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
storageKey: string, let clientKeyHalfB64: string | null = null;
clientKeyHalfB64: string, if (this.clientKeyHalves.has(userId)) {
): Promise<string | null> { clientKeyHalfB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId));
const value = await passwords.getPassword(service, storageKey); }
if (value == null || value == "") { if (value == null || value == "") {
return null; return null;
} else if (!EncString.isSerializedEncString(value)) { } else if (!EncString.isSerializedEncString(value)) {
// Update to format encrypted with client key half // Update to format encrypted with client key half
const storageDetails = await this.getStorageDetails({ const storageDetails = await this.getStorageDetails({
clientKeyHalfB64, clientKeyHalfB64: clientKeyHalfB64,
}); });
await biometrics.setBiometricSecret( await biometrics.setBiometricSecret(
service, SERVICE,
storageKey, getLookupKeyForUser(userId),
value, value,
storageDetails.key_material, storageDetails.key_material,
storageDetails.ivB64, storageDetails.ivB64,
); );
return value; return SymmetricCryptoKey.fromString(value);
} else { } else {
const encValue = new EncString(value); const encValue = new EncString(value);
this.setIv(encValue.iv); this.setIv(encValue.iv);
const storageDetails = await this.getStorageDetails({ const storageDetails = await this.getStorageDetails({
clientKeyHalfB64, clientKeyHalfB64: clientKeyHalfB64,
}); });
return await biometrics.getBiometricSecret(service, storageKey, storageDetails.key_material); return SymmetricCryptoKey.fromString(
await biometrics.getBiometricSecret(
SERVICE,
getLookupKeyForUser(userId),
storageDetails.key_material,
),
);
} }
} }
async setBiometricKey( async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
service: string, const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
storageKey: string,
value: string, if (
clientKeyPartB64: string | undefined, await this.valueUpToDate({
): Promise<void> { value: key,
const parsedValue = SymmetricCryptoKey.fromString(value); clientKeyPartB64: Utils.fromBufferToB64(clientKeyHalf),
if (await this.valueUpToDate({ value: parsedValue, clientKeyPartB64, service, storageKey })) { service: SERVICE,
storageKey: getLookupKeyForUser(userId),
})
) {
return; return;
} }
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); const storageDetails = await this.getStorageDetails({
clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf),
});
const storedValue = await biometrics.setBiometricSecret( const storedValue = await biometrics.setBiometricSecret(
service, SERVICE,
storageKey, getLookupKeyForUser(userId),
value, key.toBase64(),
storageDetails.key_material, storageDetails.key_material,
storageDetails.ivB64, storageDetails.ivB64,
); );
const parsedStoredValue = new EncString(storedValue); const parsedStoredValue = new EncString(storedValue);
await this.storeValueWitness( await this.storeValueWitness(
parsedValue, key,
parsedStoredValue, parsedStoredValue,
service, SERVICE,
storageKey, getLookupKeyForUser(userId),
clientKeyPartB64, Utils.fromBufferToB64(clientKeyHalf),
); );
} }
async deleteBiometricKey(service: string, key: string): Promise<void> { async deleteBiometricKey(userId: UserId): Promise<void> {
await passwords.deletePassword(service, key); await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
await passwords.deletePassword(service, key + KEY_WITNESS_SUFFIX); await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId) + KEY_WITNESS_SUFFIX);
} }
async authenticateBiometric(): Promise<boolean> { async authenticateBiometric(): Promise<boolean> {
@@ -240,13 +264,58 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
return result; return result;
} }
async osBiometricsNeedsSetup() { async needsSetup() {
return false; return false;
} }
async osBiometricsCanAutoSetup(): Promise<boolean> { async canAutoSetup(): Promise<boolean> {
return false; return false;
} }
async osBiometricsSetup(): Promise<void> {} async runSetup(): Promise<void> {}
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);
}
// 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, clientKeyHalf);
return clientKeyHalf;
}
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
if (!requireClientKeyHalf) {
return BiometricsStatus.Available;
}
if (this.clientKeyHalves.has(userId)) {
return BiometricsStatus.Available;
} else {
return BiometricsStatus.UnlockNeeded;
}
}
} }

View File

@@ -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 { export interface OsBiometricService {
osSupportsBiometric(): Promise<boolean>; supportsBiometrics(): Promise<boolean>;
/** /**
* Check whether support for biometric unlock requires setup. This can be automatic or manual. * 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) * @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. * 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. * @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. * Starts automatic biometric setup, which places the required configuration files / changes the required settings.
*/ */
osBiometricsSetup: () => Promise<void>; runSetup(): Promise<void>;
authenticateBiometric(): Promise<boolean>; authenticateBiometric(): Promise<boolean>;
getBiometricKey( getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null>;
service: string, setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void>;
key: string, deleteBiometricKey(userId: UserId): Promise<void>;
clientKeyHalfB64: string | undefined, getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus>;
): Promise<string | null>;
setBiometricKey(
service: string,
key: string,
value: string,
clientKeyHalfB64: string | undefined,
): Promise<void>;
deleteBiometricKey(service: string, key: string): Promise<void>;
} }

View File

@@ -34,8 +34,14 @@ export class RendererBiometricsService extends DesktopBiometricsService {
return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id); return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id);
} }
async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void> { async setBiometricProtectedUnlockKeyForUser(
return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(userId, value); userId: UserId,
value: SymmetricCryptoKey,
): Promise<void> {
return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(
userId,
value.toBase64(),
);
} }
async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> { async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> {
@@ -46,10 +52,6 @@ export class RendererBiometricsService extends DesktopBiometricsService {
return await ipc.keyManagement.biometric.setupBiometrics(); return await ipc.keyManagement.biometric.setupBiometrics();
} }
async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void> {
return await ipc.keyManagement.biometric.setClientKeyHalf(userId, value);
}
async getShouldAutopromptNow(): Promise<boolean> { async getShouldAutopromptNow(): Promise<boolean> {
return await ipc.keyManagement.biometric.getShouldAutoprompt(); return await ipc.keyManagement.biometric.getShouldAutoprompt();
} }

View File

@@ -9,14 +9,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management"; import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management";
import { import {
makeEncString,
makeStaticByteArray,
makeSymmetricCryptoKey, makeSymmetricCryptoKey,
FakeAccountService, FakeAccountService,
mockAccountServiceWith, mockAccountServiceWith,
@@ -80,7 +77,6 @@ describe("ElectronKeyService", () => {
await keyService.setUserKey(userKey, mockUserId); await keyService.setUserKey(userKey, mockUserId);
expect(biometricService.setClientKeyHalfForUser).not.toHaveBeenCalled();
expect(biometricService.setBiometricProtectedUnlockKeyForUser).not.toHaveBeenCalled(); expect(biometricService.setBiometricProtectedUnlockKeyForUser).not.toHaveBeenCalled();
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId); expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
@@ -96,14 +92,12 @@ describe("ElectronKeyService", () => {
await keyService.setUserKey(userKey, mockUserId); await keyService.setUserKey(userKey, mockUserId);
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(mockUserId, null);
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
mockUserId, mockUserId,
userKey.keyB64, userKey,
); );
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId); expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(mockUserId);
}); });
describe("require password on start enabled", () => { describe("require password on start enabled", () => {
@@ -111,73 +105,11 @@ describe("ElectronKeyService", () => {
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true); biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
}); });
it("sets new biometric client key half and biometric unlock key when no biometric client key half stored", async () => { it("sets biometric key", async () => {
const clientKeyHalfBytes = makeStaticByteArray(32);
const clientKeyHalf = Utils.fromBufferToUtf8(clientKeyHalfBytes);
const encryptedClientKeyHalf = makeEncString();
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(null);
cryptoFunctionService.randomBytes.mockResolvedValue(
clientKeyHalfBytes.buffer as CsprngArray,
);
encryptService.encryptString.mockResolvedValue(encryptedClientKeyHalf);
await keyService.setUserKey(userKey, mockUserId); await keyService.setUserKey(userKey, mockUserId);
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(
mockUserId,
clientKeyHalf,
);
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
mockUserId, mockUserId,
userKey.keyB64,
);
expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(
encryptedClientKeyHalf,
mockUserId,
);
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(
mockUserId,
);
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(
mockUserId,
);
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(
mockUserId,
);
expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32);
expect(encryptService.encryptString).toHaveBeenCalledWith(clientKeyHalf, userKey);
});
it("sets decrypted biometric client key half and biometric unlock key when existing biometric client key half stored", async () => {
const encryptedClientKeyHalf = makeEncString();
const clientKeyHalf = Utils.fromBufferToUtf8(makeStaticByteArray(32));
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(
encryptedClientKeyHalf,
);
encryptService.decryptString.mockResolvedValue(clientKeyHalf);
await keyService.setUserKey(userKey, mockUserId);
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(
mockUserId,
clientKeyHalf,
);
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
mockUserId,
userKey.keyB64,
);
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(
mockUserId,
);
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(
mockUserId,
);
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(
mockUserId,
);
expect(encryptService.decryptString).toHaveBeenCalledWith(
encryptedClientKeyHalf,
userKey, userKey,
); );
}); });

View File

@@ -8,9 +8,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { CsprngString } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
import { import {
@@ -77,10 +75,7 @@ export class ElectronKeyService extends DefaultKeyService {
} }
private async storeBiometricsProtectedUserKey(userKey: UserKey, userId: UserId): Promise<void> { private async storeBiometricsProtectedUserKey(userKey: UserKey, userId: UserId): Promise<void> {
// May resolve to null, in which case no client key have is required await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey);
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId);
await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf);
await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64);
} }
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<boolean> { protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<boolean> {
@@ -91,34 +86,4 @@ export class ElectronKeyService extends DefaultKeyService {
await this.biometricService.deleteBiometricUnlockKeyForUser(userId); await this.biometricService.deleteBiometricUnlockKeyForUser(userId);
await super.clearAllStoredUserKeys(userId); await super.clearAllStoredUserKeys(userId);
} }
private async getBiometricEncryptionClientKeyHalf(
userKey: UserKey,
userId: UserId,
): Promise<CsprngString | null> {
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
if (!requireClientKeyHalf) {
return null;
}
// Retrieve existing key half if it exists
let clientKeyHalf: CsprngString | null = null;
const encryptedClientKeyHalf =
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
if (encryptedClientKeyHalf != null) {
clientKeyHalf = (await this.encryptService.decryptString(
encryptedClientKeyHalf,
userKey,
)) as CsprngString;
}
if (clientKeyHalf == null) {
// Set a key half if it doesn't exist
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
const encKey = await this.encryptService.encryptString(clientKeyHalf, userKey);
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
}
return clientKeyHalf;
}
} }

View File

@@ -25,12 +25,13 @@ const biometric = {
action: BiometricAction.GetStatusForUser, action: BiometricAction.GetStatusForUser,
userId: userId, userId: userId,
} satisfies BiometricMessage), } satisfies BiometricMessage),
setBiometricProtectedUnlockKeyForUser: (userId: string, value: string): Promise<void> => setBiometricProtectedUnlockKeyForUser: (userId: string, keyB64: string): Promise<void> => {
ipcRenderer.invoke("biometric", { return ipcRenderer.invoke("biometric", {
action: BiometricAction.SetKeyForUser, action: BiometricAction.SetKeyForUser,
userId: userId, userId: userId,
key: value, key: keyB64,
} satisfies BiometricMessage), } satisfies BiometricMessage);
},
deleteBiometricUnlockKeyForUser: (userId: string): Promise<void> => deleteBiometricUnlockKeyForUser: (userId: string): Promise<void> =>
ipcRenderer.invoke("biometric", { ipcRenderer.invoke("biometric", {
action: BiometricAction.RemoveKeyForUser, action: BiometricAction.RemoveKeyForUser,
@@ -40,12 +41,6 @@ const biometric = {
ipcRenderer.invoke("biometric", { ipcRenderer.invoke("biometric", {
action: BiometricAction.Setup, action: BiometricAction.Setup,
} satisfies BiometricMessage), } satisfies BiometricMessage),
setClientKeyHalf: (userId: string, value: string | null): Promise<void> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.SetClientKeyHalf,
userId: userId,
key: value,
} satisfies BiometricMessage),
getShouldAutoprompt: (): Promise<boolean> => getShouldAutoprompt: (): Promise<boolean> =>
ipcRenderer.invoke("biometric", { ipcRenderer.invoke("biometric", {
action: BiometricAction.GetShouldAutoprompt, action: BiometricAction.GetShouldAutoprompt,

View File

@@ -10,6 +10,7 @@ import { Subject, firstValueFrom } from "rxjs";
import { SsoUrlService } from "@bitwarden/auth/common"; import { SsoUrlService } from "@bitwarden/auth/common";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { ClientType } from "@bitwarden/common/enums"; import { ClientType } from "@bitwarden/common/enums";
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service"; import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
import { Message, MessageSender } from "@bitwarden/common/platform/messaging"; import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- For dependency creation // eslint-disable-next-line no-restricted-imports -- For dependency creation
@@ -187,14 +188,19 @@ export class Main {
this.desktopSettingsService = new DesktopSettingsService(stateProvider); this.desktopSettingsService = new DesktopSettingsService(stateProvider);
const biometricStateService = new DefaultBiometricStateService(stateProvider); const biometricStateService = new DefaultBiometricStateService(stateProvider);
const encryptService = new EncryptServiceImplementation(
this.mainCryptoFunctionService,
this.logService,
true,
);
this.biometricsService = new MainBiometricsService( this.biometricsService = new MainBiometricsService(
this.i18nService, this.i18nService,
this.windowMain, this.windowMain,
this.logService, this.logService,
this.messagingService,
process.platform, process.platform,
biometricStateService, biometricStateService,
encryptService,
this.mainCryptoFunctionService,
); );
this.windowMain = new WindowMain( this.windowMain = new WindowMain(

View File

@@ -9,8 +9,6 @@ export enum BiometricAction {
SetKeyForUser = "setKeyForUser", SetKeyForUser = "setKeyForUser",
RemoveKeyForUser = "removeKeyForUser", RemoveKeyForUser = "removeKeyForUser",
SetClientKeyHalf = "setClientKeyHalf",
Setup = "setup", Setup = "setup",
GetShouldAutoprompt = "getShouldAutoprompt", GetShouldAutoprompt = "getShouldAutoprompt",
@@ -18,21 +16,13 @@ export enum BiometricAction {
} }
export type BiometricMessage = export type BiometricMessage =
| {
action: BiometricAction.SetClientKeyHalf;
userId: string;
key: string | null;
}
| { | {
action: BiometricAction.SetKeyForUser; action: BiometricAction.SetKeyForUser;
userId: string; userId: string;
key: string; key: string;
} }
| { | {
action: Exclude< action: Exclude<BiometricAction, BiometricAction.SetKeyForUser>;
BiometricAction,
BiometricAction.SetClientKeyHalf | BiometricAction.SetKeyForUser
>;
userId?: string; userId?: string;
data?: any; data?: any;
}; };