1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 23:33:31 +00:00

[PM-17121/17204] Fix fingerprint dialogs and disabled active biometric lock component (#12928)

* Fix biometrics unlock window being empty

* Add trust on sensitive action

* Add dialog for outdated desktop app and fix spelling

* Use updated fingerprint method

* Refactor connected app trust

* Move connected apps to ephemeral value store and show error on outdated browser

* Move trust logic to only occur when fingerprint setting is enabled

* Add more tests

* Simplify code

* Update ephemeral value list call to "listEphemeralValueKeys"

* Fix trust being ignored
This commit is contained in:
Bernd Schoolmann
2025-01-21 21:26:34 +01:00
committed by GitHub
parent 5fbe7abda5
commit ef20ca83b6
10 changed files with 499 additions and 68 deletions

View File

@@ -4868,5 +4868,11 @@
},
"extraWide": {
"message": "Extra wide"
},
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
"updateDesktopAppOrDisableFingerprintDialogMessage": {
"message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings."
}
}

View File

@@ -162,10 +162,6 @@ export class NativeMessagingBackground {
HashAlgorithmForEncryption,
);
if (this.validatingFingerprint) {
this.validatingFingerprint = false;
await this.biometricStateService.setFingerprintValidated(true);
}
this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.logService.info("[Native Messaging IPC] Secure channel established");
@@ -200,15 +196,24 @@ export class NativeMessagingBackground {
}
return;
case "verifyFingerprint": {
if (this.sharedSecret == null) {
this.logService.info(
"[Native Messaging IPC] Desktop app requested trust verification by fingerprint.",
);
this.validatingFingerprint = true;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.showFingerprintDialog();
}
this.logService.info("[Native Messaging IPC] Legacy app is requesting fingerprint");
this.messagingService.send("showUpdateDesktopAppOrDisableFingerprintDialog", {});
break;
}
case "verifyDesktopIPCFingerprint": {
this.logService.info(
"[Native Messaging IPC] Desktop app requested trust verification by fingerprint.",
);
await this.showFingerprintDialog();
break;
}
case "verifiedDesktopIPCFingerprint": {
await this.biometricStateService.setFingerprintValidated(true);
this.messagingService.send("hideNativeMessagingFingerprintDialog", {});
break;
}
case "rejectedDesktopIPCFingerprint": {
this.messagingService.send("hideNativeMessagingFingerprintDialog", {});
break;
}
case "wrongUserId":
@@ -428,12 +433,9 @@ export class NativeMessagingBackground {
}
private async showFingerprintDialog() {
const fingerprint = await this.keyService.getFingerprint(
(await firstValueFrom(this.accountService.activeAccount$))?.id,
this.publicKey,
);
const fingerprint = await this.keyService.getFingerprint(this.appId, this.publicKey);
this.messagingService.send("showNativeMessagingFinterprintDialog", {
this.messagingService.send("showNativeMessagingFingerprintDialog", {
fingerprint: fingerprint,
});
}

View File

@@ -128,11 +128,18 @@ export class AppComponent implements OnInit, OnDestroy {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.showDialog(msg);
} else if (msg.command === "showNativeMessagingFinterprintDialog") {
} else if (msg.command === "showNativeMessagingFingerprintDialog") {
// TODO: Should be refactored to live in another service.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.showNativeMessagingFingerprintDialog(msg);
} else if (msg.command === "showUpdateDesktopAppOrDisableFingerprintDialog") {
// TODO: Should be refactored to live in another service.
await this.showDialog({
title: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogTitle"),
content: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogMessage"),
type: "warning",
});
} else if (msg.command === "showToast") {
this.toastService._showToast(msg);
} else if (msg.command === "reloadProcess") {

View File

@@ -1,7 +1,9 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { filter, Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { MessageListener } from "@bitwarden/common/platform/messaging";
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
export type DesktopSyncVerificationDialogParams = {
@@ -13,8 +15,30 @@ export type DesktopSyncVerificationDialogParams = {
standalone: true,
imports: [JslibModule, ButtonModule, DialogModule],
})
export class DesktopSyncVerificationDialogComponent {
constructor(@Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams) {}
export class DesktopSyncVerificationDialogComponent implements OnDestroy, OnInit {
private destroy$ = new Subject<void>();
constructor(
@Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams,
private dialogRef: DialogRef<DesktopSyncVerificationDialogComponent>,
private messageListener: MessageListener,
) {}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngOnInit(): void {
this.messageListener.allMessages$
.pipe(
filter((m) => m.command === "hideNativeMessagingFingerprintDialog"),
takeUntil(this.destroy$),
)
.subscribe(() => {
this.dialogRef.close();
});
}
static open(dialogService: DialogService, data: DesktopSyncVerificationDialogParams) {
return dialogService.open(DesktopSyncVerificationDialogComponent, {

View File

@@ -3483,5 +3483,11 @@
},
"upgradeOrganizationDesc": {
"message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features."
},
"updateBrowserOrDisableFingerprintDialogTitle": {
"message": "Extension update required"
},
"updateBrowserOrDisableFingerprintDialogMessage": {
"message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings."
}
}

View File

@@ -123,6 +123,7 @@ const ephemeralStore = {
getEphemeralValue: (key: string): Promise<string> => ipcRenderer.invoke("getEphemeralValue", key),
removeEphemeralValue: (key: string): Promise<void> =>
ipcRenderer.invoke("deleteEphemeralValue", key),
listEphemeralValueKeys: (): Promise<string[]> => ipcRenderer.invoke("listEphemeralValueKeys"),
};
const localhostCallbackService = {

View File

@@ -17,5 +17,8 @@ export class EphemeralValueStorageService {
ipcMain.handle("deleteEphemeralValue", async (event, key: string) => {
this.ephemeralValues.delete(key);
});
ipcMain.handle("listEphemeralValueKeys", async (event) => {
return Array.from(this.ephemeralValues.keys());
});
}
}

View File

@@ -9,21 +9,22 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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 { FakeAccountService } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
import { DialogService, I18nMockService } from "@bitwarden/components";
import {
KeyService,
BiometricsService,
BiometricStateService,
BiometricsCommands,
} from "@bitwarden/key-management";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { BiometricMessageHandlerService } from "./biometric-message-handler.service";
(global as any).ipc = {
platform: {
reloadProcess: jest.fn(),
},
};
const SomeUser = "SomeUser" as UserId;
const AnotherUser = "SomeOtherUser" as UserId;
const accounts = {
@@ -54,6 +55,7 @@ describe("BiometricMessageHandlerService", () => {
let accountService: AccountService;
let authService: MockProxy<AuthService>;
let ngZone: MockProxy<NgZone>;
let i18nService: MockProxy<I18nMockService>;
beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>();
@@ -69,6 +71,24 @@ describe("BiometricMessageHandlerService", () => {
accountService = new FakeAccountService(accounts);
authService = mock<AuthService>();
ngZone = mock<NgZone>();
i18nService = mock<I18nMockService>();
(global as any).ipc = {
platform: {
ephemeralStore: {
listEphemeralValueKeys: jest.fn(),
getEphemeralValue: jest.fn(),
removeEphemeralValue: jest.fn(),
setEphemeralValue: jest.fn(),
},
nativeMessaging: {
sendMessage: jest.fn(),
},
reloadProcess: jest.fn(),
},
};
cryptoFunctionService.rsaEncrypt.mockResolvedValue(Utils.fromUtf8ToArray("encrypted"));
cryptoFunctionService.randomBytes.mockResolvedValue(new Uint8Array(64) as CsprngArray);
service = new BiometricMessageHandlerService(
cryptoFunctionService,
@@ -83,9 +103,256 @@ describe("BiometricMessageHandlerService", () => {
accountService,
authService,
ngZone,
i18nService,
);
});
describe("setup encryption", () => {
it("should reject when user is not in app", async () => {
await service.handleMessage({
appId: "appId",
message: {
command: "setupEncryption",
messageId: 0,
userId: "unknownUser" as UserId,
},
});
expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
appId: "appId",
command: "wrongUserId",
});
});
it("should setup secure communication", async () => {
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
"connectedApp_appId",
]);
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: null,
trusted: false,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: "setupEncryption",
messageId: 0,
userId: SomeUser,
publicKey: Utils.fromUtf8ToB64("publicKey"),
},
});
expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
appId: "appId",
command: "setupEncryption",
messageId: -1,
sharedSecret: Utils.fromUtf8ToB64("encrypted"),
});
expect((global as any).ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: false,
}),
);
});
it("should invalidate encryption if connection is not secured", async () => {
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
"connectedApp_appId",
]);
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: null,
trusted: false,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: "biometricUnlock",
messageId: 0,
userId: SomeUser,
},
});
expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
appId: "appId",
command: "invalidateEncryption",
});
});
it("should show update dialog when legacy unlock is requested with fingerprint active", async () => {
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true);
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
"connectedApp_appId",
]);
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: false,
}),
);
encryptService.decryptToUtf8.mockResolvedValue(
JSON.stringify({
command: "biometricUnlock",
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: "biometricUnlock",
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
},
});
expect(dialogService.openSimpleDialog).toHaveBeenCalled();
});
it("should send verify fingerprint when fingerprinting is required on modern unlock, and dialog is accepted, and set to trusted", async () => {
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true);
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
"connectedApp_appId",
]);
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: false,
}),
);
ngZone.run.mockReturnValue({
closed: of(true),
});
encryptService.decryptToUtf8.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
},
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "verifyDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "verifiedDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: true,
}),
);
});
it("should send reject fingerprint when fingerprinting is required on modern unlock, and dialog is rejected, and it should not set to trusted", async () => {
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true);
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
"connectedApp_appId",
]);
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: false,
}),
);
ngZone.run.mockReturnValue({
closed: of(false),
});
encryptService.decryptToUtf8.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
},
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "verifyDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "rejectedDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.ephemeralStore.setEphemeralValue).not.toHaveBeenCalledWith(
"connectedApp_appId",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: true,
}),
);
});
it("should not attempt to verify when the connected app is already trusted", async () => {
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true);
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
"connectedApp_appId",
]);
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: true,
}),
);
encryptService.decryptToUtf8.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
},
});
expect(ipc.platform.nativeMessaging.sendMessage).not.toHaveBeenCalledWith({
command: "verifyDesktopIPCFingerprint",
appId: "appId",
});
});
});
describe("process reload", () => {
const testCases = [
// don't reload when the active user is the requested one and unlocked

View File

@@ -1,13 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable, NgZone } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { combineLatest, concatMap, firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/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";
@@ -31,6 +32,50 @@ import { DesktopSettingsService } from "../platform/services/desktop-settings.se
const MessageValidTimeout = 10 * 1000;
const HashAlgorithmForAsymmetricEncryption = "sha1";
type ConnectedApp = {
publicKey: string;
sessionSecret: string;
trusted: boolean;
};
const ConnectedAppPrefix = "connectedApp_";
class ConnectedApps {
async get(appId: string): Promise<ConnectedApp> {
if (!(await this.has(appId))) {
return null;
}
return JSON.parse(
await ipc.platform.ephemeralStore.getEphemeralValue(`${ConnectedAppPrefix}${appId}`),
);
}
async list(): Promise<string[]> {
return (await ipc.platform.ephemeralStore.listEphemeralValueKeys())
.filter((key) => key.startsWith(ConnectedAppPrefix))
.map((key) => key.replace(ConnectedAppPrefix, ""));
}
async set(appId: string, value: ConnectedApp) {
await ipc.platform.ephemeralStore.setEphemeralValue(
`${ConnectedAppPrefix}${appId}`,
JSON.stringify(value),
);
}
async has(appId: string) {
return (await this.list()).find((id) => id === appId) != null;
}
async clear() {
const connected = await this.list();
for (const appId of connected) {
await ipc.platform.ephemeralStore.removeEphemeralValue(`${ConnectedAppPrefix}${appId}`);
}
}
}
@Injectable()
export class BiometricMessageHandlerService {
constructor(
@@ -46,7 +91,21 @@ export class BiometricMessageHandlerService {
private accountService: AccountService,
private authService: AuthService,
private ngZone: NgZone,
) {}
private i18nService: I18nService,
) {
combineLatest([
this.desktopSettingService.browserIntegrationFingerprintEnabled$,
this.desktopSettingService.browserIntegrationEnabled$,
])
.pipe(
concatMap(async () => {
await this.connectedApps.clear();
}),
)
.subscribe();
}
private connectedApps: ConnectedApps = new ConnectedApps();
async handleMessage(msg: LegacyMessageWrapper) {
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
@@ -69,39 +128,24 @@ export class BiometricMessageHandlerService {
return;
}
if (await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)) {
this.logService.info("[Native Messaging IPC] Requesting fingerprint verification.");
ipc.platform.nativeMessaging.sendMessage({
command: "verifyFingerprint",
appId: appId,
});
const fingerprint = await this.keyService.getFingerprint(
rawMessage.userId,
remotePublicKey,
if (await this.connectedApps.has(appId)) {
this.logService.info(
"[Native Messaging IPC] Public key for app id changed. Invalidating trust",
);
this.messagingService.send("setFocus");
const dialogRef = this.ngZone.run(() =>
BrowserSyncVerificationDialogComponent.open(this.dialogService, { fingerprint }),
);
const browserSyncVerified = await firstValueFrom(dialogRef.closed);
if (browserSyncVerified !== true) {
this.logService.info("[Native Messaging IPC] Fingerprint verification failed.");
return;
}
}
await this.connectedApps.set(appId, {
publicKey: Utils.fromBufferToB64(remotePublicKey),
sessionSecret: null,
trusted: false,
});
await this.secureCommunication(remotePublicKey, appId);
return;
}
if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) {
if ((await this.connectedApps.get(appId))?.sessionSecret == null) {
this.logService.info(
"[Native Messaging IPC] Epheremal secret for secure channel is missing. Invalidating encryption...",
"[Native Messaging IPC] Session secret for secure channel is missing. Invalidating encryption...",
);
ipc.platform.nativeMessaging.sendMessage({
command: "invalidateEncryption",
@@ -113,7 +157,7 @@ export class BiometricMessageHandlerService {
const message: LegacyMessage = JSON.parse(
await this.encryptService.decryptToUtf8(
rawMessage as EncString,
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
SymmetricCryptoKey.fromString((await this.connectedApps.get(appId)).sessionSecret),
),
);
@@ -202,6 +246,18 @@ export class BiometricMessageHandlerService {
}
// TODO: legacy, remove after 2025.3
case BiometricsCommands.Unlock: {
if (
await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)
) {
await this.send({ command: "biometricUnlock", response: "not available" }, appId);
await this.dialogService.openSimpleDialog({
title: this.i18nService.t("updateBrowserOrDisableFingerprintDialogTitle"),
content: this.i18nService.t("updateBrowserOrDisableFingerprintDialogMessage"),
type: "warning",
});
return;
}
const isTemporarilyDisabled =
(await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) &&
!((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available);
@@ -283,7 +339,7 @@ export class BiometricMessageHandlerService {
const encrypted = await this.encryptService.encrypt(
JSON.stringify(message),
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
SymmetricCryptoKey.fromString((await this.connectedApps.get(appId)).sessionSecret),
);
ipc.platform.nativeMessaging.sendMessage({
@@ -295,10 +351,9 @@ export class BiometricMessageHandlerService {
private async secureCommunication(remotePublicKey: Uint8Array, appId: string) {
const secret = await this.cryptoFunctionService.randomBytes(64);
await ipc.platform.ephemeralStore.setEphemeralValue(
appId,
new SymmetricCryptoKey(secret).keyB64,
);
const connectedApp = await this.connectedApps.get(appId);
connectedApp.sessionSecret = new SymmetricCryptoKey(secret).keyB64;
await this.connectedApps.set(appId, connectedApp);
this.logService.info("[Native Messaging IPC] Setting up secure channel");
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
@@ -320,6 +375,18 @@ export class BiometricMessageHandlerService {
appId: string,
) {
const messageUserId = message.userId as UserId;
if (!(await this.validateFingerprint(appId))) {
await this.send(
{
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId,
response: false,
},
appId,
);
return;
}
try {
const userKey = await this.biometricsService.unlockWithBiometricsForUser(messageUserId);
if (userKey != null) {
@@ -369,4 +436,54 @@ export class BiometricMessageHandlerService {
}
}
}
async validateFingerprint(appId: string): Promise<boolean> {
if (await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)) {
const appToValidate = await this.connectedApps.get(appId);
if (appToValidate == null) {
return false;
}
if (appToValidate.trusted) {
return true;
}
ipc.platform.nativeMessaging.sendMessage({
command: "verifyDesktopIPCFingerprint",
appId: appId,
});
const fingerprint = await this.keyService.getFingerprint(
appId,
Utils.fromB64ToArray(appToValidate.publicKey),
);
this.messagingService.send("setFocus");
const dialogRef = this.ngZone.run(() =>
BrowserSyncVerificationDialogComponent.open(this.dialogService, { fingerprint }),
);
const browserSyncVerified = await firstValueFrom(dialogRef.closed);
if (browserSyncVerified !== true) {
this.logService.info("[Native Messaging IPC] Fingerprint verification failed.");
ipc.platform.nativeMessaging.sendMessage({
command: "rejectedDesktopIPCFingerprint",
appId: appId,
});
return false;
} else {
this.logService.info("[Native Messaging IPC] Fingerprint verified.");
ipc.platform.nativeMessaging.sendMessage({
command: "verifiedDesktopIPCFingerprint",
appId: appId,
});
}
appToValidate.trusted = true;
await this.connectedApps.set(appId, appToValidate);
}
return true;
}
}