mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 14:14:55 +00:00
enhance(e2ee): store encrypted password in keychain on desktop
instead of OPFS
This commit is contained in:
@@ -46,7 +46,8 @@
|
||||
"semver": "7.5.2",
|
||||
"socks-proxy-agent": "8.0.2",
|
||||
"update-electron-app": "2.0.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.5",
|
||||
"keytar": "^7.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.3",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[electron.fs-watcher :as watcher]
|
||||
[electron.git :as git]
|
||||
[electron.handler-interface :refer [handle]]
|
||||
[electron.keychain :as keychain]
|
||||
[electron.logger :as logger]
|
||||
[electron.plugin :as plugin]
|
||||
[electron.server :as server]
|
||||
@@ -617,6 +618,15 @@
|
||||
(defmethod handle :cancel-all-requests [_ 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]
|
||||
(logger/error "Error: no ipc handler for:" args))
|
||||
|
||||
|
||||
68
src/electron/electron/keychain.cljs
Normal file
68
src/electron/electron/keychain.cljs
Normal file
@@ -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)))
|
||||
@@ -1,11 +1,44 @@
|
||||
(ns frontend.handler.e2ee
|
||||
"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.state :as state]
|
||||
[frontend.util :as util]
|
||||
[lambdaisland.glogi :as log]
|
||||
[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
|
||||
[]
|
||||
(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
|
||||
[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))
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
Each graph has an AES key.
|
||||
Server stores the encrypted AES key, public key, and encrypted private key."
|
||||
(:require ["/frontend/idbkv" :as idb-keyval]
|
||||
[clojure.string :as string]
|
||||
[frontend.common.crypt :as crypt]
|
||||
[frontend.common.file.opfs :as opfs]
|
||||
[frontend.common.missionary :as c.m]
|
||||
[frontend.common.thread-api :refer [def-thread-api]]
|
||||
[frontend.worker.rtc.ws-util :as ws-util]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.db :as ldb]
|
||||
[missionary.core :as m]
|
||||
[promesa.core :as p])
|
||||
@@ -17,16 +19,41 @@
|
||||
|
||||
(defonce ^:private store (delay (idb-keyval/newStore "localforage" "keyvaluepairs" 2)))
|
||||
(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
|
||||
[refresh-token password]
|
||||
(p/let [result (crypt/<encrypt-text-by-text-password refresh-token password)
|
||||
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
|
||||
[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)
|
||||
password (crypt/<decrypt-text-by-text-password refresh-token data)]
|
||||
password))
|
||||
|
||||
Reference in New Issue
Block a user