Quellcode durchsuchen

enhance(e2ee): store encrypted password in keychain on desktop

instead of OPFS
Tienson Qin vor 2 Monaten
Ursprung
Commit
6ed01dfb93

+ 2 - 1
resources/package.json

@@ -46,7 +46,8 @@
     "semver": "7.5.2",
     "semver": "7.5.2",
     "socks-proxy-agent": "8.0.2",
     "socks-proxy-agent": "8.0.2",
     "update-electron-app": "2.0.1",
     "update-electron-app": "2.0.1",
-    "zod": "^4.1.5"
+    "zod": "^4.1.5",
+    "keytar": "^7.9.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@electron-forge/cli": "^7.8.3",
     "@electron-forge/cli": "^7.8.3",

+ 10 - 0
src/electron/electron/handler.cljs

@@ -22,6 +22,7 @@
             [electron.fs-watcher :as watcher]
             [electron.fs-watcher :as watcher]
             [electron.git :as git]
             [electron.git :as git]
             [electron.handler-interface :refer [handle]]
             [electron.handler-interface :refer [handle]]
+            [electron.keychain :as keychain]
             [electron.logger :as logger]
             [electron.logger :as logger]
             [electron.plugin :as plugin]
             [electron.plugin :as plugin]
             [electron.server :as server]
             [electron.server :as server]
@@ -617,6 +618,15 @@
 (defmethod handle :cancel-all-requests [_ args]
 (defmethod handle :cancel-all-requests [_ args]
   (apply rsapi/cancel-all-requests (rest args)))
   (apply rsapi/cancel-all-requests (rest args)))
 
 
+(defmethod handle :keychain/save-e2ee-password [_window [_ refresh-token encrypted-text]]
+  (keychain/<set-password! refresh-token encrypted-text))
+
+(defmethod handle :keychain/get-e2ee-password [_window [_ refresh-token]]
+  (keychain/<get-password refresh-token))
+
+(defmethod handle :keychain/delete-e2ee-password [_window [_ refresh-token]]
+  (keychain/<delete-password! refresh-token))
+
 (defmethod handle :default [args]
 (defmethod handle :default [args]
   (logger/error "Error: no ipc handler for:" args))
   (logger/error "Error: no ipc handler for:" args))
 
 

+ 68 - 0
src/electron/electron/keychain.cljs

@@ -0,0 +1,68 @@
+(ns electron.keychain
+  "Helper functions for storing E2EE secrets inside the OS keychain."
+  (:require ["crypto" :as crypto]
+            ["electron" :refer [app]]
+            ["keytar" :as keytar]
+            [clojure.string :as string]
+            [electron.logger :as logger]
+            [promesa.core :as p]))
+
+(defonce ^:private service-name
+  (delay
+    (let [app-name (try (.getName app)
+                        (catch :default _ nil))]
+      (if (string/blank? app-name)
+        "Logseq"
+        app-name))))
+
+(defn- keychain-service
+  []
+  (str (force service-name) " E2EE"))
+
+(defn- normalize-account
+  [refresh-token]
+  (when (and (string? refresh-token)
+             (not (string/blank? refresh-token)))
+    (try
+      (let [hash (.createHash crypto "sha256")]
+        (.update hash refresh-token)
+        (.digest hash "hex"))
+      (catch :default e
+        (logger/error ::normalize-account {:error e})
+        nil))))
+
+(defn supported?
+  []
+  (boolean keytar))
+
+(defn <set-password!
+  "Persist `encrypted-text` for the `refresh-token` entry."
+  [refresh-token encrypted-text]
+  (if-let [account (and (supported?) (normalize-account refresh-token))]
+    (-> (p/let [_ (.setPassword keytar (keychain-service) account encrypted-text)]
+          true)
+        (p/catch (fn [e]
+                   (logger/error ::set-password {:error e})
+                   (throw e))))
+    (p/resolved false)))
+
+(defn <get-password
+  "Fetch encrypted text stored for `refresh-token`."
+  [refresh-token]
+  (if-let [account (and (supported?) (normalize-account refresh-token))]
+    (-> (p/let [password (.getPassword keytar (keychain-service) account)]
+          password)
+        (p/catch (fn [e]
+                   (logger/error ::get-password {:error e})
+                   (throw e))))
+    (p/resolved nil)))
+
+(defn <delete-password!
+  [refresh-token]
+  (if-let [account (and (supported?) (normalize-account refresh-token))]
+    (-> (p/let [_ (.deletePassword keytar (keychain-service) account)]
+          true)
+        (p/catch (fn [e]
+                   (logger/error ::delete-password {:error e})
+                   (throw e))))
+    (p/resolved false)))

+ 46 - 1
src/main/frontend/handler/e2ee.cljs

@@ -1,11 +1,44 @@
 (ns frontend.handler.e2ee
 (ns frontend.handler.e2ee
   "rtc E2EE related fns"
   "rtc E2EE related fns"
-  (:require [frontend.common.crypt :as crypt]
+  (:require [electron.ipc :as ipc]
+            [frontend.common.crypt :as crypt]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.util :as util]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]))
             [promesa.core :as p]))
 
 
+(def ^:private save-op :keychain/save-e2ee-password)
+(def ^:private get-op :keychain/get-e2ee-password)
+(def ^:private delete-op :keychain/delete-e2ee-password)
+
+(defn- <keychain-save!
+  [refresh-token encrypted-text]
+  (if (util/electron?)
+    (-> (ipc/ipc save-op refresh-token encrypted-text)
+        (p/catch (fn [e]
+                   (log/error :keychain-save-failed e)
+                   (throw e))))
+    (p/resolved nil)))
+
+(defn- <keychain-get
+  [refresh-token]
+  (if (util/electron?)
+    (-> (ipc/ipc get-op refresh-token)
+        (p/catch (fn [e]
+                   (log/error :keychain-get-failed e)
+                   (throw e))))
+    (p/resolved nil)))
+
+(defn- <keychain-delete!
+  [refresh-token]
+  (if (util/electron?)
+    (-> (ipc/ipc delete-op refresh-token)
+        (p/catch (fn [e]
+                   (log/error :keychain-delete-failed e)
+                   (throw e))))
+    (p/resolved nil)))
+
 (def-thread-api :thread-api/request-e2ee-password
 (def-thread-api :thread-api/request-e2ee-password
   []
   []
   (p/let [password-promise (state/pub-event! [:rtc/request-e2ee-password])
   (p/let [password-promise (state/pub-event! [:rtc/request-e2ee-password])
@@ -25,3 +58,15 @@
 (def-thread-api :thread-api/decrypt-user-e2ee-private-key
 (def-thread-api :thread-api/decrypt-user-e2ee-private-key
   [encrypted-private-key]
   [encrypted-private-key]
   (<decrypt-user-e2ee-private-key encrypted-private-key))
   (<decrypt-user-e2ee-private-key encrypted-private-key))
+
+(def-thread-api :thread-api/electron-save-e2ee-password
+  [refresh-token encrypted-text]
+  (<keychain-save! refresh-token encrypted-text))
+
+(def-thread-api :thread-api/electron-get-e2ee-password
+  [refresh-token]
+  (<keychain-get refresh-token))
+
+(def-thread-api :thread-api/electron-delete-e2ee-password
+  [refresh-token]
+  (<keychain-delete! refresh-token))

+ 29 - 2
src/main/frontend/worker/rtc/crypt.cljs

@@ -4,12 +4,14 @@
   Each graph has an AES key.
   Each graph has an AES key.
   Server stores the encrypted AES key, public key, and encrypted private key."
   Server stores the encrypted AES key, public key, and encrypted private key."
   (:require ["/frontend/idbkv" :as idb-keyval]
   (:require ["/frontend/idbkv" :as idb-keyval]
+            [clojure.string :as string]
             [frontend.common.crypt :as crypt]
             [frontend.common.crypt :as crypt]
             [frontend.common.file.opfs :as opfs]
             [frontend.common.file.opfs :as opfs]
             [frontend.common.missionary :as c.m]
             [frontend.common.missionary :as c.m]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
+            [lambdaisland.glogi :as log]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [missionary.core :as m]
             [missionary.core :as m]
             [promesa.core :as p])
             [promesa.core :as p])
@@ -17,16 +19,41 @@
 
 
 (defonce ^:private store (delay (idb-keyval/newStore "localforage" "keyvaluepairs" 2)))
 (defonce ^:private store (delay (idb-keyval/newStore "localforage" "keyvaluepairs" 2)))
 (defonce ^:private e2ee-password-file "e2ee-password")
 (defonce ^:private e2ee-password-file "e2ee-password")
+(defonce ^:private electron-env?
+  (let [href (try (.. js/self -location -href)
+                  (catch :default _ nil))]
+    (boolean (and (string? href)
+                  (string/includes? href "electron=true")))))
+
+(defn- electron-worker?
+  []
+  electron-env?)
+
+(defn- <electron-save-password-text!
+  [refresh-token encrypted-text]
+  (worker-state/<invoke-main-thread :thread-api/electron-save-e2ee-password refresh-token encrypted-text))
+
+(defn- <electron-read-password-text
+  [refresh-token]
+  (worker-state/<invoke-main-thread :thread-api/electron-get-e2ee-password refresh-token))
 
 
 (defn- <save-e2ee-password
 (defn- <save-e2ee-password
   [refresh-token password]
   [refresh-token password]
   (p/let [result (crypt/<encrypt-text-by-text-password refresh-token password)
   (p/let [result (crypt/<encrypt-text-by-text-password refresh-token password)
           text (ldb/write-transit-str result)]
           text (ldb/write-transit-str result)]
-    (opfs/<write-text! e2ee-password-file text)))
+    (if (electron-worker?)
+      (-> (p/let [_ (<electron-save-password-text! refresh-token text)]
+            nil)
+          (p/catch (fn [e]
+                     (log/error :electron-save-e2ee-password {:error e})
+                     (throw e))))
+      (opfs/<write-text! e2ee-password-file text))))
 
 
 (defn- <read-e2ee-password
 (defn- <read-e2ee-password
   [refresh-token]
   [refresh-token]
-  (p/let [text (opfs/<read-text! e2ee-password-file)
+  (p/let [text (if (electron-worker?)
+                 (<electron-read-password-text refresh-token)
+                 (opfs/<read-text! e2ee-password-file))
           data (ldb/read-transit-str text)
           data (ldb/read-transit-str text)
           password (crypt/<decrypt-text-by-text-password refresh-token data)]
           password (crypt/<decrypt-text-by-text-password refresh-token data)]
     password))
     password))