feat!: File Sync (#5355)

- file sync for electron/ios/android
- age encryption of both file content and file path
- massive UI enhancement
- corresponding CI tasks

Co-authored-by: llcc <lzhes43@gmail.com>
Co-authored-by: rcmerci <rcmerci@gmail.com>
Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
Co-authored-by: Andelf <andelf@gmail.com>
Co-authored-by: Gabriel Horner <gabriel@logseq.com>
This commit is contained in:
Charlie
2022-09-01 00:31:49 +08:00
committed by GitHub
parent 252fec4dd6
commit 01d879c18e
111 changed files with 6507 additions and 1950 deletions

View File

@@ -1,21 +1,24 @@
(ns frontend.components.encryption
(:require [clojure.string :as string]
[frontend.context.i18n :refer [t]]
[frontend.encrypt :as e]
[frontend.encrypt :as encrypt]
[frontend.handler.metadata :as metadata-handler]
[frontend.handler.notification :as notification]
[frontend.fs.sync :as sync]
[frontend.state :as state]
[frontend.ui :as ui]
[frontend.util :as util]
[frontend.config :as config]
[promesa.core :as p]
[cljs.core.async :as async]
[rum.core :as rum]))
(rum/defcs encryption-dialog-inner <
(rum/local false ::reveal-secret-phrase?)
[state repo-url close-fn]
(let [reveal-secret-phrase? (get state ::reveal-secret-phrase?)
public-key (e/get-public-key repo-url)
private-key (e/get-secret-key repo-url)]
public-key (encrypt/get-public-key repo-url)
private-key (encrypt/get-secret-key repo-url)]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
@@ -48,58 +51,227 @@
(fn [close-fn]
(encryption-dialog-inner repo-url close-fn)))
(rum/defcs input-password-inner <
(rum/defcs ^:large-vars/cleanup-todo input-password-inner < rum/reactive
(rum/local "" ::password)
(rum/local "" ::password-confirm)
[state repo-url close-fn]
(let [password (get state ::password)
password-confirm (get state ::password-confirm)]
[:div
[:div.sm:flex.sm:items-start
(rum/local "" ::pw-confirm)
(rum/local false ::pw-confirm-focused?)
{:will-mount (fn [state]
;; try to close tour tips
(some->> (state/sub :file-sync/jstour-inst)
(.complete))
state)}
[state repo-url close-fn {:keys [type GraphName GraphUUID init-graph-keys after-input-password]}]
(let [*password (get state ::password)
*pw-confirm (get state ::pw-confirm)
*pw-confirm-focused? (get state ::pw-confirm-focused?)
*input-ref-0 (rum/create-ref)
*input-ref-1 (rum/create-ref)
remote-pw? (= type :input-pwd-remote)
loading? (state/sub [:ui/loading? :set-graph-password])
pw-strength (when (and init-graph-keys
(not (string/blank? @*password)))
(util/check-password-strength @*password))
can-submit? #(if init-graph-keys
(and (>= (count @*password) 6)
(>= (:id pw-strength) 1))
true)
set-remote-graph-pwd-result (state/sub [:file-sync/set-remote-graph-password-result])
submit-handler
(fn []
(let [value @*password]
(cond
(string/blank? value)
nil
(and init-graph-keys (not= @*password @*pw-confirm))
(notification/show! "The passwords are not matched." :error)
:else
(case type
:local
(p/let [keys (encrypt/generate-key-pair-and-save! repo-url)
db-encrypted-secret (encrypt/encrypt-with-passphrase value keys)]
(metadata-handler/set-db-encrypted-secret! db-encrypted-secret)
(close-fn true))
(:create-pwd-remote :input-pwd-remote)
(do
(state/set-state! [:ui/loading? :set-graph-password] true)
(state/set-state! [:file-sync/set-remote-graph-password-result] {})
(async/go
(let [persist-r (async/<! (sync/encrypt+persist-pwd! @*password GraphUUID))]
(if (instance? js/Error persist-r)
(js/console.error persist-r)
(when (fn? after-input-password)
(async/<! (after-input-password))
;; TODO: it's better if based on sync state
(when init-graph-keys
(js/setTimeout #(state/pub-event! [:file-sync/maybe-onboarding-show :sync-learn]) 10000)))))))))))
cancel-handler
(fn []
(state/set-state! [:file-sync/set-remote-graph-password-result] {})
(close-fn))
enter-handler
(fn [^js e]
(when-let [^js input (and e (= 13 (.-which e)) (.-target e))]
(when-not (string/blank? (.-value input))
(let [input-0? (= (util/safe-lower-case (.-placeholder input)) "password")]
(if init-graph-keys
;; setup mode
(if input-0?
(.select (rum/deref *input-ref-1))
(submit-handler))
;; unlock mode
(submit-handler))))))]
[:div.encryption-password.max-w-2xl.-mb-2
[:div.cp__file-sync-related-normal-modal
[:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "lock-access")]]
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium.font-bold
"Enter a password"]]]
[:h1#modal-headline.text-2xl.font-bold.text-center
(if init-graph-keys
(if remote-pw?
"Secure this remote graph!"
"Encrypt this graph")
(if remote-pw?
"Unlock this remote graph!"
"Decrypt this graph"))]]
(ui/admonition
:warning
[:div.opacity-70
"Choose a strong and hard to guess password.\nIf you lose your password, all the data can't be decrypted!! Please make sure you remember the password you have set, or you can keep a secure backup of the password."])
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:type "password"
:placeholder "Password"
:auto-focus true
:on-change (fn [e]
(reset! password (util/evalue e)))}]
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:type "password"
:placeholder "Re-enter the password"
:on-change (fn [e]
(reset! password-confirm (util/evalue e)))}]
;; decrypt remote graph with one password
(when (and remote-pw? (not init-graph-keys))
[:<>
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
[:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
{:type "button"
:on-click (fn []
(let [value @password]
(cond
(string/blank? value)
nil
[:div.folder-tip.flex.flex-col.items-center
[:h3
[:span.flex.space-x-2.leading-none.pb-1
(ui/icon "cloud-lock")
[:span GraphName]
[:span.scale-75 (ui/icon "arrow-right")]
[:span (ui/icon "folder")]]]
[:h4.px-2.-mb-1.5 (config/get-string-repo-dir repo-url)]]
(not= @password @password-confirm)
(notification/show! "The passwords are not matched." :error)
[:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.mt-2.flex.items-center
(if-let [display-str (:fail set-remote-graph-pwd-result)]
[:<>
[:span.scale-125.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
[:span.text-red-600 display-str]]
[:<>
[:span.scale-125.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})]
[:span "Please enter the password for this graph to continue syncing."]])]])
:else
(p/let [keys (e/generate-key-pair-and-save! repo-url)
db-encrypted-secret (e/encrypt-with-passphrase value keys)]
(metadata-handler/set-db-encrypted-secret! db-encrypted-secret)
(close-fn true)))))}
"Submit"]]]]))
;; secure this remote graph
(when (and remote-pw? init-graph-keys)
(let [pattern-ok? #(>= (count @*password) 6)]
[:<>
[:h2.text-center.opacity-70.text-sm.py-2
"Each graph you want to synchronize via Logseq needs its own password for end-to-end encryption."]
[:div.input-hints.text-sm.py-2.px-3.rounded.mb-3.mt-4.flex.items-center
(if (or (not (string/blank? @*password))
(not (string/blank? @*pw-confirm)))
(if (or (not (pattern-ok?))
(not= @*password @*pw-confirm))
[:span.scale-125.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
[:span.scale-125.pr-1.text-green-600 (ui/icon "circle-check" {:class "text-md mr-1"})])
[:span.scale-125.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})])
(if (not (string/blank? @*password))
(if-not (pattern-ok?)
[:span "Password can't be less than 6 characters"]
(if (not (string/blank? @*pw-confirm))
(if (not= @*pw-confirm @*password)
[:span "Password fields are not matching!"]
[:span "Password fields are matching!"])
[:span "Enter your chosen password again!"]))
[:span "Choose a strong and hard to guess password!"])
]
;; password strength checker
(when-not (string/blank? @*password)
[:<>
[:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.-mt-1.5.flex.items-center.space-x-3
(let [included-set (set (:contains pw-strength))]
(for [i ["lowercase" "uppercase" "number" "symbol"]
:let [included? (contains? included-set i)]]
[:span.strength-item
{:key i
:class (when included? "included")}
(ui/icon (if included? "check" "x") {:class "mr-1"})
[:span.capitalize i]
]))]
[:div.input-pw-strength
[:div.indicator.flex
(for [i (range 4)
:let [title (get ["Too weak" "Weak" "Medium" "Strong"] i)]]
[:i {:key i
:title title
:class (when (>= (int (:id pw-strength)) i) "active")} i])]]])]))
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:type "password"
:ref *input-ref-0
:placeholder "Password"
:auto-focus true
:disabled loading?
:on-key-up enter-handler
:on-change (fn [^js e]
(reset! *password (util/evalue e))
(when (:fail set-remote-graph-pwd-result)
(state/set-state! [:file-sync/set-remote-graph-password-result] {})))}]
(when init-graph-keys
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:type "password"
:ref *input-ref-1
:placeholder "Re-enter the password"
:on-focus #(reset! *pw-confirm-focused? true)
:on-blur #(reset! *pw-confirm-focused? false)
:disabled loading?
:on-key-up enter-handler
:on-change (fn [^js e]
(reset! *pw-confirm (util/evalue e)))}])
(when init-graph-keys
[:div.init-remote-pw-tips.space-x-4.pt-2.hidden.sm:flex
[:div.flex-1.flex.items-center
[:span.px-3.scale-125 (ui/icon "key")]
[:p.dark:text-gray-100
[:span "Please make sure you "]
"remember the password you have set, "
[:span "and we recommend you "]
"keep a secure backup "
[:span "of the password."]]]
[:div.flex-1.flex.items-center
[:span.px-3.scale-125 (ui/icon "lock")]
[:p.dark:text-gray-100
"If you lose your password, all of your data in the cloud cant be decrypted. "
[:span "You will still be able to access the local version of your graph."]]]])]
[:div.mt-5.sm:mt-4.flex.justify-center.sm:justify-end.space-x-3
(ui/button (t :cancel) :background "gray" :disabled loading? :class "opacity-60" :on-click cancel-handler)
(ui/button [:span.inline-flex.items-center.leading-none
[:span (t :submit)]
(when loading?
[:span.ml-1 (ui/loading "" {:class "w-4 h-4"})])]
:disabled (or (not (can-submit?)) loading?)
:on-click submit-handler)]]))
(defn input-password
[repo-url close-fn]
(fn [_close-fn]
(input-password-inner repo-url close-fn)))
([repo-url close-fn] (input-password repo-url close-fn {:type :local}))
([repo-url close-fn opts]
(fn [_close-fn]
(let [close-fn' (if (fn? close-fn)
#(do (close-fn %)
(_close-fn))
_close-fn)]
(input-password-inner repo-url close-fn' opts)))))
(rum/defcs encryption-setup-dialog-inner
[state repo-url close-fn]
@@ -114,7 +286,9 @@
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
{:type "button"
:on-click (fn []
(state/set-modal! (input-password repo-url close-fn)))}
(state/set-modal!
(input-password repo-url close-fn)
{:center? true :close-btn? false}))}
(t :yes)]]
[:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
@@ -142,9 +316,9 @@
(when-not (string/blank? value) ; TODO: length or other checks
(let [repo (state/get-current-repo)]
(p/do!
(-> (e/decrypt-with-passphrase value db-encrypted-secret)
(-> (encrypt/decrypt-with-passphrase value db-encrypted-secret)
(p/then (fn [keys]
(e/save-key-pair! repo keys)
(encrypt/save-key-pair! repo keys)
(close-fn true)
(state/set-state! :encryption/graph-parsing? false)))
(p/catch #(notification/show! "The password is not matched." :warning true))