mirror of
https://github.com/logseq/logseq.git
synced 2026-05-23 12:14:06 +00:00
fix(e2ee): use native secret storage and init remote sync config
This commit is contained in:
@@ -26,6 +26,15 @@
|
||||
([runtime-id]
|
||||
(api/repl :db-worker {:runtime-id runtime-id})))
|
||||
|
||||
(defn worker-node-repl
|
||||
([]
|
||||
(let [runtime-id (->> (api/repl-runtimes :db-worker-node)
|
||||
(map :client-id)
|
||||
first)]
|
||||
(api/repl :db-worker-node {:runtime-id runtime-id})))
|
||||
([runtime-id]
|
||||
(api/repl :db-worker-node {:runtime-id runtime-id})))
|
||||
|
||||
(defn runtime-id-list
|
||||
[app]
|
||||
(->> (api/repl-runtimes app)
|
||||
|
||||
@@ -49,6 +49,28 @@
|
||||
:else
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn native-storage-supported?
|
||||
[]
|
||||
(or (util/electron?) (util/capacitor?)))
|
||||
|
||||
(defn <native-save-secret!
|
||||
[key encrypted-text]
|
||||
(if (native-storage-supported?)
|
||||
(<keychain-save! key encrypted-text)
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn <native-get-secret
|
||||
[key]
|
||||
(if (native-storage-supported?)
|
||||
(<keychain-get key)
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn <native-delete-secret!
|
||||
[key]
|
||||
(if (native-storage-supported?)
|
||||
(<keychain-delete! key)
|
||||
(p/resolved nil)))
|
||||
|
||||
(def-thread-api :thread-api/request-e2ee-password
|
||||
[]
|
||||
(p/let [password-promise (state/pub-event! [:rtc/request-e2ee-password])
|
||||
@@ -71,12 +93,12 @@
|
||||
|
||||
(def-thread-api :thread-api/native-save-e2ee-password
|
||||
[encrypted-text]
|
||||
(<keychain-save! "logseq-encrypted-password" encrypted-text))
|
||||
(<native-save-secret! "logseq-encrypted-password" encrypted-text))
|
||||
|
||||
(def-thread-api :thread-api/native-get-e2ee-password
|
||||
[]
|
||||
(<keychain-get "logseq-encrypted-password"))
|
||||
(<native-get-secret "logseq-encrypted-password"))
|
||||
|
||||
(def-thread-api :thread-api/native-delete-e2ee-password
|
||||
[]
|
||||
(<keychain-delete! "logseq-encrypted-password"))
|
||||
(<native-delete-secret! "logseq-encrypted-password"))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[clojure.string :as string]
|
||||
[frontend.common.crypt :as crypt]
|
||||
[frontend.handler.e2ee :as e2ee-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.state :as state]
|
||||
[lambdaisland.glogi :as log]
|
||||
@@ -88,6 +89,43 @@
|
||||
private-key private-key-promise]
|
||||
(crypt/<export-private-key private-key)))
|
||||
|
||||
:native-save-e2ee-password
|
||||
(let [{:keys [key encrypted-text]} payload]
|
||||
(if-not (and (string? key) (string? encrypted-text))
|
||||
(p/rejected (ex-info "invalid native-save-e2ee-password payload"
|
||||
{:code :invalid-ui-action-payload
|
||||
:action action
|
||||
:payload payload}))
|
||||
(if-not (e2ee-handler/native-storage-supported?)
|
||||
(p/resolved {:supported? false})
|
||||
(p/let [_ (e2ee-handler/<native-save-secret! key encrypted-text)]
|
||||
{:supported? true}))))
|
||||
|
||||
:native-get-e2ee-password
|
||||
(let [{:keys [key]} payload]
|
||||
(if-not (string? key)
|
||||
(p/rejected (ex-info "invalid native-get-e2ee-password payload"
|
||||
{:code :invalid-ui-action-payload
|
||||
:action action
|
||||
:payload payload}))
|
||||
(if-not (e2ee-handler/native-storage-supported?)
|
||||
(p/resolved {:supported? false})
|
||||
(p/let [encrypted-text (e2ee-handler/<native-get-secret key)]
|
||||
{:supported? true
|
||||
:encrypted-text encrypted-text}))))
|
||||
|
||||
:native-delete-e2ee-password
|
||||
(let [{:keys [key]} payload]
|
||||
(if-not (string? key)
|
||||
(p/rejected (ex-info "invalid native-delete-e2ee-password payload"
|
||||
{:code :invalid-ui-action-payload
|
||||
:action action
|
||||
:payload payload}))
|
||||
(if-not (e2ee-handler/native-storage-supported?)
|
||||
(p/resolved {:supported? false})
|
||||
(p/let [_ (e2ee-handler/<native-delete-secret! key)]
|
||||
{:supported? true}))))
|
||||
|
||||
(p/rejected (ex-info "unsupported db-worker ui action"
|
||||
{:code :unsupported-ui-action
|
||||
:action action
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.persist-db
|
||||
"Backend of DB based graph"
|
||||
(:require [electron.ipc :as ipc]
|
||||
[frontend.config :as config]
|
||||
[frontend.db :as db]
|
||||
[frontend.db.transact :as db-transact]
|
||||
[frontend.persist-db.browser :as browser]
|
||||
@@ -29,6 +30,12 @@
|
||||
(and (not (node-runtime?))
|
||||
(util/electron?)))
|
||||
|
||||
(defn- current-db-sync-config
|
||||
[]
|
||||
{:enabled? true
|
||||
:ws-url (config/db-sync-ws-url)
|
||||
:http-base (config/db-sync-http-base)})
|
||||
|
||||
(defn- <ensure-remote!
|
||||
[repo]
|
||||
(if (or (nil? repo) (= repo @remote-repo))
|
||||
@@ -42,6 +49,9 @@
|
||||
(reset! remote-db client)
|
||||
(reset! remote-repo repo)
|
||||
(reset! state/*db-worker (:wrapped-worker client))
|
||||
(p/let [_ (state/<invoke-db-worker :thread-api/set-db-sync-config
|
||||
(current-db-sync-config))]
|
||||
nil)
|
||||
(ldb/register-transact-fn!
|
||||
(fn remote-transact!
|
||||
[repo tx-data tx-meta]
|
||||
|
||||
@@ -180,10 +180,24 @@
|
||||
(let [^js DB (.-DB ^js (.-oo1 ^js sqlite))]
|
||||
(new DB path (or mode "c")))))
|
||||
|
||||
(defn- search-param-true?
|
||||
[k]
|
||||
(let [value (.get (js/URLSearchParams. (.-search js/location)) k)]
|
||||
(or (= value "true")
|
||||
(= value "1"))))
|
||||
|
||||
(defn- owner-source
|
||||
[]
|
||||
(cond
|
||||
(search-param-true? "capacitor") :capacitor
|
||||
(search-param-true? "electron") :electron
|
||||
:else :browser))
|
||||
|
||||
(defn browser-platform
|
||||
[]
|
||||
{:env {:publishing? (string/includes? (.. js/location -href) "publishing=true")
|
||||
:runtime :browser}
|
||||
:runtime :browser
|
||||
:owner-source (owner-source)}
|
||||
:storage {:install-opfs-pool install-opfs-pool
|
||||
:list-graphs list-graphs
|
||||
:db-exists? db-exists?
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.common.graph :as common-graph]
|
||||
[promesa.core :as p]))
|
||||
[promesa.core :as p]
|
||||
["keytar" :as keytar]))
|
||||
|
||||
(defn- resolve-database-sync-ctor
|
||||
[]
|
||||
@@ -339,11 +340,43 @@
|
||||
(transit/write kv-transit-writer state))
|
||||
|
||||
(def ^:private secret-prefix "worker-secret###")
|
||||
(def ^:private keychain-service "Logseq E2EE")
|
||||
|
||||
(defn- secret-key
|
||||
[key]
|
||||
(str secret-prefix key))
|
||||
|
||||
(defn- keychain-account
|
||||
[key]
|
||||
(secret-key key))
|
||||
|
||||
(defn- <save-secret-text!
|
||||
[kv key text]
|
||||
(-> (p/let [_ (.setPassword ^js keytar keychain-service (keychain-account key) text)]
|
||||
nil)
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-worker/keychain-save-failed {:error e
|
||||
:key key})
|
||||
((:set! kv) (secret-key key) text)))))
|
||||
|
||||
(defn- <read-secret-text
|
||||
[kv key]
|
||||
(-> (p/let [secret (.getPassword ^js keytar keychain-service (keychain-account key))]
|
||||
secret)
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-worker/keychain-read-failed {:error e
|
||||
:key key})
|
||||
((:get kv) (secret-key key))))))
|
||||
|
||||
(defn- <delete-secret-text!
|
||||
[kv key]
|
||||
(-> (p/let [_ (.deletePassword ^js keytar keychain-service (keychain-account key))]
|
||||
nil)
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-worker/keychain-delete-failed {:error e
|
||||
:key key})
|
||||
((:set! kv) (secret-key key) nil)))))
|
||||
|
||||
(defn- kv-store
|
||||
[data-dir]
|
||||
(let [kv-path (node-path/join data-dir "kv-store.json")
|
||||
@@ -413,9 +446,9 @@
|
||||
:backup-db (fn [^js db path]
|
||||
(.backup db path))}
|
||||
:crypto {:save-secret-text! (fn [key text]
|
||||
((:set! kv) (secret-key key) text))
|
||||
(<save-secret-text! kv key text))
|
||||
:read-secret-text (fn [key]
|
||||
((:get kv) (secret-key key)))
|
||||
(<read-secret-text kv key))
|
||||
:delete-secret-text! (fn [key]
|
||||
((:set! kv) (secret-key key) nil))}
|
||||
(<delete-secret-text! kv key))}
|
||||
:timers {:set-interval! (fn [f ms] (js/setInterval f ms))}})))
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
(defonce ^:private *graph->aes-key (atom {}))
|
||||
(defonce ^:private *user-rsa-key-pair-inflight (atom {}))
|
||||
(defonce ^:private node-default-e2ee-password-file "~/logseq/e2ee-password")
|
||||
(defonce ^:private node-default-auth-file "~/logseq/auth.json")
|
||||
(defonce ^:private e2ee-password-secret-key "logseq-encrypted-password")
|
||||
(def ^:private invalid-transit ::invalid-transit)
|
||||
@@ -36,9 +35,14 @@
|
||||
[platform']
|
||||
(= :browser (runtime platform')))
|
||||
|
||||
(defn- e2ee-password-file-path
|
||||
[]
|
||||
node-default-e2ee-password-file)
|
||||
(defn- owner-source
|
||||
[platform']
|
||||
(get-in platform' [:env :owner-source]))
|
||||
|
||||
(defn- capacitor-runtime?
|
||||
[platform']
|
||||
(and (= :browser (runtime platform'))
|
||||
(= :capacitor (owner-source platform'))))
|
||||
|
||||
(defn- auth-file-path
|
||||
[]
|
||||
@@ -48,10 +52,10 @@
|
||||
[]
|
||||
(let [env (:env (platform/current))
|
||||
runtime' (:runtime env)
|
||||
owner-source (:owner-source env)]
|
||||
owner-source' (:owner-source env)]
|
||||
(or (= :browser runtime')
|
||||
(and (= :node runtime')
|
||||
(= :electron owner-source)))))
|
||||
(= :electron owner-source')))))
|
||||
|
||||
(defn- missing-e2ee-password-ex
|
||||
[data]
|
||||
@@ -115,12 +119,22 @@
|
||||
(<read-refresh-token-from-auth-file platform'))
|
||||
_ (ensure-refresh-token! refresh-token)
|
||||
result (crypt/<encrypt-text-by-text-password refresh-token password)
|
||||
text (ldb/write-transit-str result)]
|
||||
(if (browser-runtime? platform')
|
||||
(platform/save-secret-text! platform' e2ee-password-secret-key text)
|
||||
(platform/write-text! platform' (e2ee-password-file-path) text))))
|
||||
text (ldb/write-transit-str result)
|
||||
native-saved? (if (capacitor-runtime? platform')
|
||||
(-> (ui-request/<request :native-save-e2ee-password
|
||||
{:key e2ee-password-secret-key
|
||||
:encrypted-text text})
|
||||
(p/then (fn [resp]
|
||||
(true? (:supported? resp))))
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-sync/save-e2ee-password-native-failed {:error e})
|
||||
false)))
|
||||
false)]
|
||||
(if native-saved?
|
||||
nil
|
||||
(platform/save-secret-text! platform' e2ee-password-secret-key text))))
|
||||
|
||||
(defn- <read-browser-e2ee-password-text
|
||||
(defn- <read-platform-e2ee-password-text
|
||||
[platform']
|
||||
(-> (platform/read-secret-text platform' e2ee-password-secret-key)
|
||||
(p/catch (fn [e]
|
||||
@@ -131,11 +145,16 @@
|
||||
[refresh-token]
|
||||
(ensure-refresh-token! refresh-token)
|
||||
(p/let [platform' (platform/current)
|
||||
text (if (browser-runtime? platform')
|
||||
(<read-browser-e2ee-password-text platform')
|
||||
(-> (platform/read-text! platform' (e2ee-password-file-path))
|
||||
(p/catch (fn [_]
|
||||
nil))))]
|
||||
native-result (if (capacitor-runtime? platform')
|
||||
(-> (ui-request/<request :native-get-e2ee-password
|
||||
{:key e2ee-password-secret-key})
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-sync/read-e2ee-password-native-failed {:error e})
|
||||
{:supported? false})))
|
||||
{:supported? false})
|
||||
text (if (:supported? native-result)
|
||||
(:encrypted-text native-result)
|
||||
(<read-platform-e2ee-password-text platform'))]
|
||||
(when-not (seq text)
|
||||
(throw-missing-e2ee-password! {:reason :missing-persisted-password
|
||||
:hint "Provide --e2ee-password to persist it."}))
|
||||
@@ -152,14 +171,19 @@
|
||||
(defn- <clear-e2ee-password!
|
||||
[]
|
||||
(p/let [platform' (platform/current)
|
||||
_ (if (browser-runtime? platform')
|
||||
native-deleted? (if (capacitor-runtime? platform')
|
||||
(-> (ui-request/<request :native-delete-e2ee-password
|
||||
{:key e2ee-password-secret-key})
|
||||
(p/then (fn [resp]
|
||||
(true? (:supported? resp))))
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-sync/delete-e2ee-password-native-failed {:error e})
|
||||
false)))
|
||||
false)
|
||||
_ (when-not native-deleted?
|
||||
(-> (platform/delete-secret-text! platform' e2ee-password-secret-key)
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-sync/delete-e2ee-password-secret-failed {:error e})
|
||||
nil)))
|
||||
(-> (platform/write-text! platform' (e2ee-password-file-path) "")
|
||||
(p/catch (fn [e]
|
||||
(log/warn :db-sync/clear-e2ee-password-file-failed {:error e})
|
||||
nil))))]
|
||||
nil))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.persist-db-test
|
||||
(:require [cljs.test :refer [async deftest is use-fixtures]]
|
||||
[electron.ipc :as ipc]
|
||||
[frontend.config :as config]
|
||||
[frontend.persist-db.browser :as browser]
|
||||
[frontend.persist-db :as persist-db]
|
||||
[frontend.persist-db.protocol :as protocol]
|
||||
@@ -183,6 +184,48 @@
|
||||
(set! storage/remove original-storage-remove)
|
||||
(done)))))))
|
||||
|
||||
(deftest electron-ensure-remote-pushes-db-sync-config-on-start-test
|
||||
(async done
|
||||
(let [worker-calls (atom [])
|
||||
ensure-remote! #'persist-db/<ensure-remote!
|
||||
original-ipc ipc/ipc
|
||||
original-start! remote/start!
|
||||
original-stop! remote/stop!
|
||||
original-ws config/db-sync-ws-url
|
||||
original-http config/db-sync-http-base]
|
||||
(reset-runtime-state!)
|
||||
(set! ipc/ipc (fn [channel repo]
|
||||
(is (= "db-worker-runtime" channel))
|
||||
(p/resolved {:base-url "http://127.0.0.1:9101"
|
||||
:auth-token nil
|
||||
:repo repo})))
|
||||
(set! config/db-sync-ws-url (fn [] "wss://sync.example.test/sync/%s"))
|
||||
(set! config/db-sync-http-base (fn [] "https://sync.example.test"))
|
||||
(set! remote/start! (fn [{:keys [repo]}]
|
||||
(->FakeRemote repo
|
||||
(fn [qkw direct-pass? & args]
|
||||
(swap! worker-calls conj [qkw direct-pass? args])
|
||||
(p/resolved nil)))))
|
||||
(set! remote/stop! (fn [_] (p/resolved true)))
|
||||
(-> (p/let [_ (ensure-remote! "logseq_db_graph_a")]
|
||||
(let [set-config-call (first (filter #(= :thread-api/set-db-sync-config (first %))
|
||||
@worker-calls))]
|
||||
(is (= [:thread-api/set-db-sync-config
|
||||
false
|
||||
[{:enabled? true
|
||||
:ws-url "wss://sync.example.test/sync/%s"
|
||||
:http-base "https://sync.example.test"}]]
|
||||
set-config-call))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally (fn []
|
||||
(set! ipc/ipc original-ipc)
|
||||
(set! remote/start! original-start!)
|
||||
(set! remote/stop! original-stop!)
|
||||
(set! config/db-sync-ws-url original-ws)
|
||||
(set! config/db-sync-http-base original-http)
|
||||
(done)))))))
|
||||
|
||||
(deftest electron-list-db-without-current-repo-does-not-bootstrap-runtime
|
||||
(async done
|
||||
(let [ipc-calls (atom [])
|
||||
|
||||
@@ -138,7 +138,51 @@
|
||||
(reset! worker-state/*state state-prev)
|
||||
(done)))))))
|
||||
|
||||
(deftest save-e2ee-password-uses-file-storage-in-node-runtime-test
|
||||
(deftest save-e2ee-password-uses-native-storage-in-capacitor-runtime-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :browser
|
||||
:owner-source :capacitor}}
|
||||
state-prev @worker-state/*state
|
||||
native-calls (atom [])
|
||||
secret-calls (atom [])
|
||||
file-calls (atom [])
|
||||
encrypt-calls (atom [])]
|
||||
(reset! worker-state/*state (assoc state-prev :auth/refresh-token "refresh-from-state"))
|
||||
(-> (p/with-redefs [crypt/<encrypt-text-by-text-password (fn [refresh-token password]
|
||||
(swap! encrypt-calls conj [refresh-token password])
|
||||
{:cipher "payload"})
|
||||
platform/current (fn [] platform-map)
|
||||
ui-request/<request (fn [action payload _opts]
|
||||
(swap! native-calls conj {:action action
|
||||
:payload payload})
|
||||
(p/resolved {:supported? true}))
|
||||
platform/save-secret-text! (fn [platform' key text]
|
||||
(swap! secret-calls conj {:platform platform'
|
||||
:key key
|
||||
:text text})
|
||||
(p/resolved nil))
|
||||
platform/write-text! (fn [platform' path text]
|
||||
(swap! file-calls conj {:platform platform'
|
||||
:path path
|
||||
:text text})
|
||||
(p/resolved nil))]
|
||||
(#'sync-crypt/<save-e2ee-password "password"))
|
||||
(p/then (fn [_]
|
||||
(is (= [["refresh-from-state" "password"]] @encrypt-calls))
|
||||
(is (= 1 (count @native-calls)))
|
||||
(is (= :native-save-e2ee-password (:action (first @native-calls))))
|
||||
(is (= "logseq-encrypted-password"
|
||||
(get-in (first @native-calls) [:payload :key])))
|
||||
(is (string? (get-in (first @native-calls) [:payload :encrypted-text])))
|
||||
(is (empty? @secret-calls))
|
||||
(is (empty? @file-calls))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))))
|
||||
(p/finally (fn []
|
||||
(reset! worker-state/*state state-prev)
|
||||
(done)))))))
|
||||
|
||||
(deftest save-e2ee-password-uses-secret-storage-in-node-runtime-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :node
|
||||
:owner-source :cli}}
|
||||
@@ -169,11 +213,51 @@
|
||||
(is (= 1 (count @auth-read-calls)))
|
||||
(is (= "~/logseq/auth.json" (:path (first @auth-read-calls))))
|
||||
(is (= [["refresh-from-auth-file" "password"]] @encrypt-calls))
|
||||
(is (= 1 (count @file-calls)))
|
||||
(is (= platform-map (:platform (first @file-calls))))
|
||||
(is (= "~/logseq/e2ee-password" (:path (first @file-calls))))
|
||||
(is (string? (:text (first @file-calls))))
|
||||
(is (empty? @secret-calls))))
|
||||
(is (= 1 (count @secret-calls)))
|
||||
(is (= platform-map (:platform (first @secret-calls))))
|
||||
(is (= "logseq-encrypted-password" (:key (first @secret-calls))))
|
||||
(is (string? (:text (first @secret-calls))))
|
||||
(is (empty? @file-calls))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest save-e2ee-password-uses-secret-storage-in-electron-runtime-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :node
|
||||
:owner-source :electron}}
|
||||
secret-calls (atom [])
|
||||
file-calls (atom [])
|
||||
auth-read-calls (atom [])
|
||||
encrypt-calls (atom [])]
|
||||
(-> (p/with-redefs [crypt/<encrypt-text-by-text-password (fn [refresh-token password]
|
||||
(swap! encrypt-calls conj [refresh-token password])
|
||||
{:cipher "payload"})
|
||||
platform/current (fn [] platform-map)
|
||||
platform/read-text! (fn [platform' path]
|
||||
(swap! auth-read-calls conj {:platform platform'
|
||||
:path path})
|
||||
(p/resolved "{\"refresh-token\":\"refresh-from-auth-file\"}"))
|
||||
platform/save-secret-text! (fn [platform' key text]
|
||||
(swap! secret-calls conj {:platform platform'
|
||||
:key key
|
||||
:text text})
|
||||
(p/resolved nil))
|
||||
platform/write-text! (fn [platform' path text]
|
||||
(swap! file-calls conj {:platform platform'
|
||||
:path path
|
||||
:text text})
|
||||
(p/resolved nil))]
|
||||
(#'sync-crypt/<save-e2ee-password "password"))
|
||||
(p/then (fn [_]
|
||||
(is (= 1 (count @auth-read-calls)))
|
||||
(is (= "~/logseq/auth.json" (:path (first @auth-read-calls))))
|
||||
(is (= [["refresh-from-auth-file" "password"]] @encrypt-calls))
|
||||
(is (= 1 (count @secret-calls)))
|
||||
(is (= platform-map (:platform (first @secret-calls))))
|
||||
(is (= "logseq-encrypted-password" (:key (first @secret-calls))))
|
||||
(is (string? (:text (first @secret-calls))))
|
||||
(is (empty? @file-calls))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))))
|
||||
(p/finally done)))))
|
||||
@@ -226,6 +310,95 @@
|
||||
(is false (str e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest read-e2ee-password-uses-native-storage-in-capacitor-runtime-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :browser
|
||||
:owner-source :capacitor}}
|
||||
native-calls (atom [])
|
||||
secret-calls (atom [])
|
||||
file-calls (atom [])]
|
||||
(-> (p/with-redefs [platform/current (fn [] platform-map)
|
||||
ui-request/<request (fn [action payload _opts]
|
||||
(swap! native-calls conj {:action action
|
||||
:payload payload})
|
||||
(p/resolved {:supported? true
|
||||
:encrypted-text (ldb/write-transit-str {:cipher "payload"})}))
|
||||
platform/read-secret-text (fn [platform' key]
|
||||
(swap! secret-calls conj {:platform platform'
|
||||
:key key})
|
||||
(p/resolved (ldb/write-transit-str {:cipher "legacy"})))
|
||||
platform/read-text! (fn [platform' path]
|
||||
(swap! file-calls conj {:platform platform'
|
||||
:path path})
|
||||
(p/resolved (ldb/write-transit-str {:cipher "legacy"})))
|
||||
crypt/<decrypt-text-by-text-password (fn [_refresh-token _data]
|
||||
(p/resolved "decrypted-password"))]
|
||||
(#'sync-crypt/<read-e2ee-password "refresh-token"))
|
||||
(p/then (fn [password]
|
||||
(is (= "decrypted-password" password))
|
||||
(is (= [{:action :native-get-e2ee-password
|
||||
:payload {:key "logseq-encrypted-password"}}]
|
||||
@native-calls))
|
||||
(is (empty? @secret-calls))
|
||||
(is (empty? @file-calls))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest read-e2ee-password-uses-secret-storage-in-node-runtime-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :node
|
||||
:owner-source :cli}}
|
||||
secret-calls (atom [])
|
||||
file-calls (atom [])]
|
||||
(-> (p/with-redefs [platform/current (fn [] platform-map)
|
||||
platform/read-secret-text (fn [platform' key]
|
||||
(swap! secret-calls conj {:platform platform'
|
||||
:key key})
|
||||
(p/resolved (ldb/write-transit-str {:cipher "payload"})))
|
||||
platform/read-text! (fn [platform' path]
|
||||
(swap! file-calls conj {:platform platform'
|
||||
:path path})
|
||||
(p/resolved (ldb/write-transit-str {:cipher "legacy"})))
|
||||
crypt/<decrypt-text-by-text-password (fn [_refresh-token _data]
|
||||
(p/resolved "decrypted-password"))]
|
||||
(#'sync-crypt/<read-e2ee-password "refresh-token"))
|
||||
(p/then (fn [password]
|
||||
(is (= "decrypted-password" password))
|
||||
(is (= 1 (count @secret-calls)))
|
||||
(is (= "logseq-encrypted-password" (:key (first @secret-calls))))
|
||||
(is (empty? @file-calls))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest read-e2ee-password-uses-secret-storage-in-electron-runtime-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :node
|
||||
:owner-source :electron}}
|
||||
secret-calls (atom [])
|
||||
file-calls (atom [])]
|
||||
(-> (p/with-redefs [platform/current (fn [] platform-map)
|
||||
platform/read-secret-text (fn [platform' key]
|
||||
(swap! secret-calls conj {:platform platform'
|
||||
:key key})
|
||||
(p/resolved (ldb/write-transit-str {:cipher "payload"})))
|
||||
platform/read-text! (fn [platform' path]
|
||||
(swap! file-calls conj {:platform platform'
|
||||
:path path})
|
||||
(p/resolved (ldb/write-transit-str {:cipher "legacy"})))
|
||||
crypt/<decrypt-text-by-text-password (fn [_refresh-token _data]
|
||||
(p/resolved "decrypted-password"))]
|
||||
(#'sync-crypt/<read-e2ee-password "refresh-token"))
|
||||
(p/then (fn [password]
|
||||
(is (= "decrypted-password" password))
|
||||
(is (= 1 (count @secret-calls)))
|
||||
(is (= "logseq-encrypted-password" (:key (first @secret-calls))))
|
||||
(is (empty? @file-calls))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest read-e2ee-password-browser-missing-secret-does-not-fallback-to-file-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :browser}}
|
||||
@@ -252,9 +425,44 @@
|
||||
(keyword (ex-message e)))))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest read-e2ee-password-capacitor-missing-native-secret-does-not-fallback-to-worker-storage-test
|
||||
(async done
|
||||
(let [platform-map {:env {:runtime :browser
|
||||
:owner-source :capacitor}}
|
||||
native-read-calls (atom 0)
|
||||
secret-read-calls (atom 0)
|
||||
file-read-calls (atom 0)]
|
||||
(-> (p/with-redefs [platform/current (fn [] platform-map)
|
||||
ui-request/<request (fn [action payload _opts]
|
||||
(swap! native-read-calls inc)
|
||||
(is (= :native-get-e2ee-password action))
|
||||
(is (= {:key "logseq-encrypted-password"} payload))
|
||||
(p/resolved {:supported? true
|
||||
:encrypted-text nil}))
|
||||
platform/read-secret-text (fn [_platform' _key]
|
||||
(swap! secret-read-calls inc)
|
||||
(p/resolved (ldb/write-transit-str {:cipher "legacy"})))
|
||||
platform/read-text! (fn [_platform' _path]
|
||||
(swap! file-read-calls inc)
|
||||
(p/resolved (ldb/write-transit-str {:cipher "legacy-file"})))
|
||||
crypt/<decrypt-text-by-text-password (fn [_refresh-token _data]
|
||||
(p/rejected (ex-info "should-not-decrypt" {})))]
|
||||
(#'sync-crypt/<read-e2ee-password "refresh-token"))
|
||||
(p/then (fn [_]
|
||||
(is false "expected missing e2ee password failure")))
|
||||
(p/catch (fn [e]
|
||||
(is (= 1 @native-read-calls))
|
||||
(is (zero? @secret-read-calls))
|
||||
(is (zero? @file-read-calls))
|
||||
(is (contains? #{:db-sync/missing-e2ee-password
|
||||
:missing-e2ee-password}
|
||||
(or (:code (ex-data e))
|
||||
(keyword (ex-message e)))))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest verify-and-save-e2ee-password-verifies-before-write-test
|
||||
(async done
|
||||
(let [write-calls (atom [])
|
||||
(let [save-calls (atom [])
|
||||
decrypt-calls (atom [])]
|
||||
(-> (p/with-redefs [platform/current (fn [] {:env {:runtime :node
|
||||
:owner-source :cli}})
|
||||
@@ -269,40 +477,41 @@
|
||||
(p/resolved "{\"refresh-token\":\"refresh-token\"}"))
|
||||
crypt/<encrypt-text-by-text-password (fn [_refresh-token _password]
|
||||
{:cipher "password-payload"})
|
||||
platform/write-text! (fn [_platform' path text]
|
||||
(swap! write-calls conj {:path path
|
||||
:text text})
|
||||
(p/resolved nil))
|
||||
platform/save-secret-text! (fn [& _]
|
||||
(p/rejected (ex-info "should not use browser secret store" {})))]
|
||||
platform/save-secret-text! (fn [_platform' key text]
|
||||
(swap! save-calls conj {:key key
|
||||
:text text})
|
||||
(p/resolved nil))
|
||||
platform/write-text! (fn [& _]
|
||||
(p/rejected (ex-info "should not use node file storage" {})))]
|
||||
(#'sync-crypt/<verify-and-save-e2ee-password! "new-password"
|
||||
"encrypted-private-key"))
|
||||
(p/then (fn [_]
|
||||
(is (= [["new-password" :encrypted-private-key-payload]] @decrypt-calls))
|
||||
(is (= 1 (count @write-calls)))
|
||||
(is (= "~/logseq/e2ee-password" (:path (first @write-calls))))))
|
||||
(is (= 1 (count @save-calls)))
|
||||
(is (= "logseq-encrypted-password" (:key (first @save-calls))))
|
||||
(is (string? (:text (first @save-calls))))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest verify-and-save-e2ee-password-invalid-password-does-not-overwrite-test
|
||||
(async done
|
||||
(let [write-calls (atom 0)]
|
||||
(let [save-calls (atom 0)]
|
||||
(-> (p/with-redefs [platform/current (fn [] {:env {:runtime :node
|
||||
:owner-source :cli}})
|
||||
ldb/read-transit-str (fn [_] :encrypted-private-key-payload)
|
||||
crypt/<decrypt-private-key (fn [_password _encrypted-private-key]
|
||||
(p/rejected (ex-info "decrypt-private-key" {:code :invalid-password})))
|
||||
platform/write-text! (fn [_platform' _path _text]
|
||||
(swap! write-calls inc)
|
||||
(p/resolved nil))]
|
||||
platform/save-secret-text! (fn [_platform' _key _text]
|
||||
(swap! save-calls inc)
|
||||
(p/resolved nil))]
|
||||
(#'sync-crypt/<verify-and-save-e2ee-password! "wrong-password"
|
||||
"encrypted-private-key"))
|
||||
(p/then (fn [_]
|
||||
(is false "expected verify failure")))
|
||||
(p/catch (fn [e]
|
||||
(is (= "decrypt-private-key" (ex-message e)))
|
||||
(is (zero? @write-calls))))
|
||||
(is (zero? @save-calls))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest verify-and-save-e2ee-password-invalid-server-user-keys-shape-test
|
||||
|
||||
Reference in New Issue
Block a user