fix(e2ee): use native secret storage and init remote sync config

This commit is contained in:
Tienson Qin
2026-04-21 12:00:38 +08:00
parent ffe75c786f
commit e5ea919e3c
9 changed files with 451 additions and 49 deletions

View File

@@ -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)

View File

@@ -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"))

View File

@@ -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

View File

@@ -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]

View File

@@ -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?

View File

@@ -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))}})))

View File

@@ -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))

View File

@@ -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 [])

View File

@@ -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