From 06acecff67d5b8a149863a0a1facc7ac322ea280 Mon Sep 17 00:00:00 2001
From: huhuanming <huanming@onekey.so>
Date: Thu, 31 Oct 2024 21:32:10 +0800
Subject: [PATCH] fix: restrict the Windows Hello-related libraries from
 running in child processes.

---
 apps/desktop/scripts/build.js                 |  2 +-
 apps/desktop/src-electron/app.ts              | 68 +++++++++-------
 apps/desktop/src-electron/service/enum.ts     |  4 +
 apps/desktop/src-electron/service/index.ts    | 79 +++++++++++++++++++
 .../src-electron/service/windowsHello.ts      | 70 ++++++++++++++++
 5 files changed, 193 insertions(+), 30 deletions(-)
 create mode 100644 apps/desktop/src-electron/service/enum.ts
 create mode 100644 apps/desktop/src-electron/service/index.ts
 create mode 100644 apps/desktop/src-electron/service/windowsHello.ts

diff --git a/apps/desktop/scripts/build.js b/apps/desktop/scripts/build.js
index 9159c62b05d..a6d8641b0db 100644
--- a/apps/desktop/scripts/build.js
+++ b/apps/desktop/scripts/build.js
@@ -12,7 +12,7 @@ const gitRevision = childProcess
 
 const hrstart = process.hrtime();
 build({
-  entryPoints: ['app.ts', 'preload.ts'].map((f) =>
+  entryPoints: ['app.ts', 'preload.ts', 'service/windowsHello.ts'].map((f) =>
     path.join(electronSource, f),
   ),
   platform: 'node',
diff --git a/apps/desktop/src-electron/app.ts b/apps/desktop/src-electron/app.ts
index f5513d461f1..b3be7422590 100644
--- a/apps/desktop/src-electron/app.ts
+++ b/apps/desktop/src-electron/app.ts
@@ -22,10 +22,6 @@ import {
 import contextMenu from 'electron-context-menu';
 import isDev from 'electron-is-dev';
 import logger from 'electron-log/main';
-import windowsSecurityCredentialsUiModule, {
-  UserConsentVerificationResult,
-  UserConsentVerifierAvailability,
-} from 'electron-windows-security';
 
 import {
   ONEKEY_APP_DEEP_LINK_NAME,
@@ -48,6 +44,11 @@ import * as store from './libs/store';
 import { parseContentPList } from './libs/utils';
 import initProcess, { restartBridge } from './process';
 import { resourcesPath, staticPath } from './resoucePath';
+import {
+  checkAvailabilityAsync,
+  requestVerificationAsync,
+  startServices,
+} from './service';
 
 logger.initialize();
 logger.transports.file.maxSize = 1024 * 1024 * 10;
@@ -538,18 +539,17 @@ function createMainWindow() {
 
   ipcMain.on(ipcMessageKeys.TOUCH_ID_CAN_PROMPT, async (event) => {
     if (isWin) {
-      const result = await new Promise((resolve) => {
-        windowsSecurityCredentialsUiModule.UserConsentVerifier.checkAvailabilityAsync(
-          (error, status) => {
-            if (error) {
-              resolve(false);
-            } else {
-              resolve(status === UserConsentVerifierAvailability.available);
-            }
-          },
+      logger.info('[TOUCH_ID_CAN_PROMPT] Windows checkAvailabilityAsync');
+      try {
+        const result = await checkAvailabilityAsync();
+        event.returnValue = result;
+      } catch (error) {
+        logger.info(
+          '[TOUCH_ID_CAN_PROMPT] Windows checkAvailabilityAsync',
+          error,
         );
-      });
-      event.returnValue = result;
+        event.returnValue = false;
+      }
       return;
     }
     const result = systemPreferences?.canPromptTouchID?.();
@@ -596,23 +596,32 @@ function createMainWindow() {
 
   ipcMain.on(ipcMessageKeys.TOUCH_ID_PROMPT, async (event, msg: string) => {
     if (isWin) {
-      windowsSecurityCredentialsUiModule.UserConsentVerifier.requestVerificationAsync(
-        msg,
-        (error, status) => {
-          if (error) {
-            event.reply(ipcMessageKeys.TOUCH_ID_PROMPT_RES, {
-              success: false,
-              error: error.message,
-            });
-          } else {
-            event.reply(ipcMessageKeys.TOUCH_ID_PROMPT_RES, {
-              success: status === UserConsentVerificationResult.verified,
-            });
-          }
-        },
+      logger.info(
+        '[TOUCH_ID_PROMPT] Windows requestVerificationAsync',
+        isAppReady,
       );
+      try {
+        const { success, error } = await requestVerificationAsync(msg);
+        event.reply(ipcMessageKeys.TOUCH_ID_PROMPT_RES, { success });
+        if (error) {
+          logger.info(
+            '[TOUCH_ID_PROMPT] Windows requestVerificationAsync error',
+            error,
+          );
+        }
+      } catch (e: any) {
+        logger.info(
+          '[TOUCH_ID_PROMPT] Windows requestVerificationAsync error',
+          e,
+        );
+        event.reply(ipcMessageKeys.TOUCH_ID_PROMPT_RES, {
+          success: false,
+          error: e.message,
+        });
+      }
       return;
     }
+
     try {
       await systemPreferences.promptTouchID(msg);
       event.reply(ipcMessageKeys.TOUCH_ID_PROMPT_RES, { success: true });
@@ -903,6 +912,7 @@ if (!singleInstance && !process.mas) {
   app.on('ready', async () => {
     const locale = await initLocale();
     logger.info('locale >>>> ', locale);
+    startServices();
     if (!mainWindow) {
       mainWindow = createMainWindow();
       initMenu();
diff --git a/apps/desktop/src-electron/service/enum.ts b/apps/desktop/src-electron/service/enum.ts
new file mode 100644
index 00000000000..f9cde16a3c7
--- /dev/null
+++ b/apps/desktop/src-electron/service/enum.ts
@@ -0,0 +1,4 @@
+export enum EWindowHelloEventType {
+  CheckAvailabilityAsync = 'checkAvailabilityAsync',
+  RequestVerificationAsync = 'requestVerificationAsync',
+}
diff --git a/apps/desktop/src-electron/service/index.ts b/apps/desktop/src-electron/service/index.ts
new file mode 100644
index 00000000000..d5835762de5
--- /dev/null
+++ b/apps/desktop/src-electron/service/index.ts
@@ -0,0 +1,79 @@
+import path from 'path';
+
+import { utilityProcess } from 'electron/main';
+import Logger from 'electron-log/main';
+
+import { EWindowHelloEventType } from './enum';
+
+import type { UtilityProcess } from 'electron/main';
+
+let windowsHelloChild: UtilityProcess | null = null;
+let windowsHelloCallbacks: {
+  type: string;
+  callback: (e: any) => void;
+  timestamp: number;
+}[] = [];
+export const startServices = () => {
+  windowsHelloChild = utilityProcess.fork(
+    // After build, the directory is 'dist' and WindowsHello file is located in 'dist/service'
+    path.join(__dirname, './service/windowsHello.js'),
+  );
+  windowsHelloChild.on('message', (e: { type: string; result: boolean }) => {
+    Logger.info('parent process--onMessage', e);
+    const callbacks = windowsHelloCallbacks.filter(
+      (callbackItem) => callbackItem.type === e.type,
+    );
+    if (callbacks.length) {
+      callbacks.forEach((callbackItem) => {
+        // Callbacks older than 1 minute will not be executed
+        if (Date.now() - callbackItem.timestamp < 60 * 1000) {
+          callbackItem.callback(e.result);
+        }
+      });
+      windowsHelloCallbacks = windowsHelloCallbacks.filter(
+        (callbackItem) => !callbacks.includes(callbackItem),
+      );
+    }
+  });
+};
+
+let cacheWindowsHelloSupported: boolean | null = null;
+export const checkAvailabilityAsync = async () => {
+  if (cacheWindowsHelloSupported === null) {
+    cacheWindowsHelloSupported = await Promise.race<boolean>([
+      new Promise<boolean>((resolve) => {
+        windowsHelloCallbacks.push({
+          type: EWindowHelloEventType.CheckAvailabilityAsync,
+          callback: resolve,
+          timestamp: Date.now(),
+        });
+        windowsHelloChild?.postMessage({
+          type: EWindowHelloEventType.CheckAvailabilityAsync,
+        });
+      }),
+      new Promise((resolve) =>
+        setTimeout(() => {
+          cacheWindowsHelloSupported = false;
+          resolve(cacheWindowsHelloSupported);
+        }, 500),
+      ),
+    ]);
+  }
+  return cacheWindowsHelloSupported;
+};
+
+export const requestVerificationAsync = (message: string) =>
+  new Promise<{
+    success: boolean;
+    error?: string;
+  }>((resolve) => {
+    windowsHelloCallbacks.push({
+      type: EWindowHelloEventType.RequestVerificationAsync,
+      callback: resolve,
+      timestamp: Date.now(),
+    });
+    windowsHelloChild?.postMessage({
+      type: EWindowHelloEventType.RequestVerificationAsync,
+      params: message,
+    });
+  });
diff --git a/apps/desktop/src-electron/service/windowsHello.ts b/apps/desktop/src-electron/service/windowsHello.ts
new file mode 100644
index 00000000000..4fc6513f1ef
--- /dev/null
+++ b/apps/desktop/src-electron/service/windowsHello.ts
@@ -0,0 +1,70 @@
+import windowsSecurityCredentialsUiModule, {
+  UserConsentVerificationResult,
+  UserConsentVerifierAvailability,
+} from 'electron-windows-security';
+
+import { EWindowHelloEventType } from './enum';
+
+function checkWindowsHelloAvailability(callback: (result: boolean) => void) {
+  try {
+    windowsSecurityCredentialsUiModule.UserConsentVerifier.checkAvailabilityAsync(
+      (error, status) => {
+        if (error) {
+          callback(false);
+        } else {
+          callback(status === UserConsentVerifierAvailability.available);
+        }
+      },
+    );
+  } catch (error) {
+    return false;
+  }
+}
+
+function requestVerificationAsync(
+  message: string,
+  callback: (params: { success: boolean; error?: string }) => void,
+) {
+  windowsSecurityCredentialsUiModule.UserConsentVerifier.requestVerificationAsync(
+    message,
+    (error, status) => {
+      if (error) {
+        callback({
+          success: false,
+          error: error.message,
+        });
+      } else {
+        callback({
+          success: status === UserConsentVerificationResult.verified,
+        });
+      }
+    },
+  );
+}
+
+// Child process
+process.parentPort.on(
+  'message',
+  (e: { data: { type: string; params: unknown } }) => {
+    switch (e.data.type) {
+      case 'checkAvailabilityAsync':
+        checkWindowsHelloAvailability((result) => {
+          process.parentPort.postMessage({
+            type: EWindowHelloEventType.CheckAvailabilityAsync,
+            result,
+          });
+        });
+        break;
+      case 'requestVerificationAsync':
+        requestVerificationAsync(e.data.params as string, (result) => {
+          process.parentPort.postMessage({
+            type: EWindowHelloEventType.RequestVerificationAsync,
+            result,
+          });
+        });
+        break;
+      default:
+        break;
+    }
+  },
+);