feat: add encryption back

This commit is contained in:
Tienson Qin
2022-05-09 20:33:59 +08:00
parent 17a4f06058
commit 83569141fc
23 changed files with 566 additions and 60 deletions

View File

@@ -52,6 +52,7 @@ const portal = new MagicPortal(worker);
<script defer src="./js/lsplugin.core.js"></script>
<script defer src="./js/main.js"></script>
<script defer src="./js/code-editor.js"></script>
<script defer src="./js/age-encryption.js"></script>
<script defer src="./js/excalidraw.js"></script>
</body>
</html>

View File

@@ -12,12 +12,12 @@
:js-options {:ignore-asset-requires true} ;; handle `require(xxx.css)`
:modules {:main
{:init-fn frontend.core/init}
;; :graph
;; {:entries [frontend.extensions.graph.force]
;; :depends-on #{:main}}
:code-editor
{:entries [frontend.extensions.code]
:depends-on #{:main}}
:age-encryption
{:entries [frontend.extensions.age-encryption]
:depends-on #{:main}}
:excalidraw
{:entries [frontend.extensions.excalidraw]
:depends-on #{:main}}}
@@ -69,12 +69,12 @@
:js-options {:ignore-asset-requires true}
:modules {:main
{:init-fn frontend.publishing/init}
;; :graph
;; {:entries [frontend.extensions.graph.force]
;; :depends-on #{:main}}
:code-editor
{:entries [frontend.extensions.code]
:depends-on #{:main}}
:age-encryption
{:entries [frontend.extensions.age-encryption]
:depends-on #{:main}}
:excalidraw
{:entries [frontend.extensions.excalidraw]
:depends-on #{:main}}}

View File

@@ -127,7 +127,7 @@
;; TODO: ugly, replace with ls-files and filter with ".map"
_ (p/all (map (fn [file]
(. fs removeSync (path/join static-dir "js" (str file ".map"))))
["main.js" "code-editor.js" "excalidraw.js"]))]
["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))]
(. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")})))))))
(defn setup-app-manager!

View File

@@ -0,0 +1,176 @@
(ns frontend.components.encryption
(:require [clojure.string :as string]
[frontend.context.i18n :refer [t]]
[frontend.encrypt :as e]
[frontend.handler.metadata :as metadata-handler]
[frontend.handler.notification :as notification]
[frontend.state :as state]
[frontend.ui :as ui]
[frontend.util :as util]
[promesa.core :as p]
[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)]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium
"This graph is encrypted with " [:a {:href "https://age-encryption.org/" :target "_blank" :rel "noopener"} "age-encryption.org/v1"]]]]
[:div.mt-1
[:div.max-w-2xl.rounded-md.shadow-sm.sm:max-w-xl
[:div.cursor-pointer.block.w-full.rounded-sm.p-2
{:on-click (fn []
(when (not @reveal-secret-phrase?)
(reset! reveal-secret-phrase? true)))}
[:div.font-medium "Public Key:"]
[:div.font-mono.select-all.break-all public-key]
(if @reveal-secret-phrase?
[:div
[:div.mt-1.font-medium "Private Key:"]
[:div.font-mono.select-all.break-all private-key]]
[:div.underline "click to view the private key"])]]]
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
[: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
{:type "button"
:on-click close-fn}
(t :close)]]]]))
(defn encryption-dialog
[repo-url]
(fn [close-fn]
(encryption-dialog-inner repo-url close-fn)))
(rum/defcs input-password-inner <
(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
[: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"]]]
(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)))}]
[: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
(not= @password @password-confirm)
(notification/show! "The passwords are not matched." :error)
: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"]]]]))
(defn input-password
[repo-url close-fn]
(fn [_close-fn]
(input-password-inner repo-url close-fn)))
(rum/defcs encryption-setup-dialog-inner
[state repo-url close-fn]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium
"Do you want to create an encrypted graph?"]]]
[: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 []
(state/set-modal! (input-password repo-url close-fn)))}
(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
{:type "button"
:on-click (fn [] (close-fn false))}
(t :no)]]]])
(defn encryption-setup-dialog
[repo-url close-fn]
(fn [close-modal-fn]
(let [close-fn (fn [encrypted?]
(close-fn encrypted?)
(close-modal-fn))]
(encryption-setup-dialog-inner repo-url close-fn))))
(rum/defcs encryption-input-secret-inner <
(rum/local "" ::secret)
(rum/local false ::loading)
[state _repo-url db-encrypted-secret close-fn]
(let [secret (::secret state)
loading (::loading state)]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium
"Enter your password"]]]
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:type "password"
:auto-focus true
:on-change (fn [e]
(reset! secret (util/evalue e)))}]
[: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 []
(reset! loading true)
(let [value @secret]
(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)
(p/then (fn [keys]
(e/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))
(p/finally #(reset! loading false))))))))}
(if @loading (ui/loading "Decrypting") "Decrypt")]]]]))
(defn encryption-input-secret-dialog
[repo-url db-encrypted-secret close-fn]
(fn [close-modal-fn]
(let [close-fn (fn [encrypted?]
(close-fn encrypted?)
(close-modal-fn))]
(encryption-input-secret-inner repo-url db-encrypted-secret close-fn))))

View File

@@ -17,7 +17,9 @@
[frontend.text :as text]
[promesa.core :as p]
[electron.ipc :as ipc]
[goog.object :as gobj]))
[goog.object :as gobj]
[frontend.components.encryption :as encryption]
[frontend.encrypt :as e]))
(rum/defc add-repo
[args]
@@ -60,6 +62,11 @@
:href url}
(db/get-repo-path url)])
[:div.controls
(when (e/encrypted-db? url)
[:a.control {:title "Show encryption information about this graph"
:on-click (fn []
(state/set-modal! (encryption/encryption-dialog url)))}
"🔐"])
[:a.text-gray-400.ml-4.font-medium.text-sm
{:title "No worries, unlink this graph will clear its cache only, it does not remove your files on the disk."
:on-click (fn []

View File

@@ -390,6 +390,14 @@
;; (let [value (not enable-block-timestamps?)]
;; (config-handler/set-config! :feature/enable-block-timestamps? value)))))
(defn encryption-row [t enable-encryption?]
(toggle "enable_encryption"
(t :settings-page/enable-encryption)
enable-encryption?
#(let [value (not enable-encryption?)]
(config-handler/set-config! :feature/enable-encryption? value))
[:div.text-sm.opacity-50 "⚠️ This feature is experimental"]))
(rum/defc keyboard-shortcuts-row [t]
(row-with-button-action
{:left-label (t :settings-page/customize-shortcuts)
@@ -544,6 +552,7 @@
preferred-workflow (state/get-preferred-workflow)
enable-timetracking? (state/enable-timetracking?)
enable-journals? (state/enable-journals? current-repo)
enable-encryption? (state/enable-encryption? current-repo)
enable-all-pages-public? (state/all-pages-public?)
logical-outdenting? (state/logical-outdenting?)
enable-tooltip? (state/enable-tooltip?)
@@ -578,6 +587,7 @@
:on-key-press (fn [e]
(when (= "Enter" (util/ekey e))
(update-home-page e)))}]]]])
(encryption-row t enable-encryption?)
(enable-all-pages-public-row t enable-all-pages-public?)
(zotero-settings-row t)
(auto-push-row t current-repo enable-git-auto-push?)]))

View File

@@ -276,6 +276,7 @@
(def config-file "config.edn")
(def custom-css-file "custom.css")
(def custom-js-file "custom.js")
(def metadata-file "metadata.edn")
(def pages-metadata-file "pages-metadata.edn")
(def config-default-content (rc/inline "config.edn"))
@@ -368,6 +369,13 @@
(when repo
(get-file-path repo (str app-name "/" config-file)))))
(defn get-metadata-path
([]
(get-metadata-path (state/get-current-repo)))
([repo]
(when repo
(get-file-path repo (str app-name "/" metadata-file)))))
(defn get-pages-metadata-path
([]
(get-pages-metadata-path (state/get-current-repo)))

View File

@@ -8,6 +8,8 @@
:ast/version {}
:db/type {}
:db/ident {:db/unique :db.unique/identity}
:db/encrypted? {}
:db/encryption-keys {}
:recent/pages {}

View File

@@ -162,6 +162,7 @@
:settings-page/enable-tooltip "Tooltips"
:settings-page/enable-journals "Journals"
:settings-page/enable-all-pages-public "All pages public when publishing"
:settings-page/enable-encryption "Encryption"
:settings-page/customize-shortcuts "Keyboard shortcuts"
:settings-page/shortcut-settings "Customize shortcuts"
:settings-page/home-default-page "Set the default home page"
@@ -585,6 +586,7 @@
:settings-page/customize-shortcuts "Tastaturbefehle"
:settings-page/disable-sentry "Nutzungs- und Diagnostik-Daten an Logseq senden"
:settings-page/edit-custom-css "custom.css bearbeiten"
:settings-page/enable-encryption "Verschlüsselung"
:settings-page/enable-shortcut-tooltip "Tooltips für Verknüpfungen aktivieren"
:settings-page/enable-tooltip "Tooltips"
:settings-page/shortcut-settings "Verknüpfungen anpassen"
@@ -895,6 +897,7 @@
:settings-page/enable-tooltip "开启提示框"
:settings-page/enable-journals "开启日记"
:settings-page/enable-all-pages-public "发布所有页面"
:settings-page/enable-encryption "激活加密功能"
:settings-page/customize-shortcuts "自定义快捷键"
:settings-page/shortcut-settings "快捷键设置"
:settings-page/home-default-page "设置首页默认页面"
@@ -1476,6 +1479,7 @@
:settings-page/enable-timetracking "Habilitar rastreo de tiempo"
:settings-page/enable-tooltip "Habilitar descripción emergente"
:settings-page/enable-journals "Habilitar diarios"
:settings-page/enable-encryption "Habilitar función de cifrado"
:settings-page/home-default-page "Establecer página de inicio"
:settings-page/enable-block-time "Habilitar marcas temporales de bloque"
:settings-page/clear-cache "Limpiar caché"
@@ -1706,6 +1710,7 @@
:settings-page/preferred-workflow "Foretrukket arbeidslflyt"
:settings-page/enable-shortcut-tooltip "Skru på tooltip for snarveier"
:settings-page/enable-timetracking "Aktiver tidssporing"
:settings-page/enable-encryption "Aktiver kryptering"
:settings-page/enable-tooltip "Aktiver verktøytips"
:settings-page/enable-journals "Aktiver dagbøker"
:settings-page/enable-all-pages-public "Aktiver alle sider som offentlige ved publisering"
@@ -1994,6 +1999,7 @@
:settings-page/enable-tooltip "Ativar dicas de ferramentas"
:settings-page/enable-journals "Ativar diários"
:settings-page/enable-all-pages-public "Ativar todas as páginas públicas ao publicar"
:settings-page/enable-encryption "Ativar funcionalidade de criptografia"
:settings-page/customize-shortcuts "Atalhos de teclado"
:settings-page/shortcut-settings "Personalizar atalhos"
:settings-page/home-default-page "Definir a página inicial padrão"
@@ -2315,6 +2321,7 @@
:settings-page/enable-tooltip "Dicas de atalhos"
:settings-page/enable-journals "Diários"
:settings-page/enable-all-pages-public "Todas as páginas públicas ao publicar"
:settings-page/enable-encryption "Encriptação"
:settings-page/customize-shortcuts "Atalhos de teclado"
:settings-page/shortcut-settings "Personalizar atalhos"
:settings-page/home-default-page "Definir a página inicial predefinida"
@@ -2610,6 +2617,7 @@
:settings-page/enable-shortcut-tooltip "Всплывающие подсказки горячих клавиш"
:settings-page/enable-journals "Включить Дневники"
:settings-page/enable-all-pages-public "Все страницы общедоступны при публикации"
:settings-page/enable-encryption "Функции шифрования"
:settings-page/customize-shortcuts "Горячие клавиши"
:settings-page/shortcut-settings "Настроить горячие клавиши"
:settings-page/home-default-page "Установить домашнюю страницу по умолчанию"
@@ -2897,6 +2905,7 @@
:settings-page/enable-tooltip "ツールチップ"
:settings-page/enable-journals "日誌"
:settings-page/enable-all-pages-public "パブリッシュ時には全てのページを公開する"
:settings-page/enable-encryption "暗号化"
:settings-page/customize-shortcuts "キーボードショートカット"
:settings-page/shortcut-settings "ショートカットをカスタマイズ"
:settings-page/home-default-page "デフォルトのホームページを設定"

View File

@@ -0,0 +1,104 @@
(ns frontend.encrypt
(:require [frontend.utf8 :as utf8]
[frontend.db.utils :as db-utils]
[frontend.db :as db]
[promesa.core :as p]
[frontend.state :as state]
[clojure.string :as str]
[cljs.reader :as reader]
[shadow.loader :as loader]
[lambdaisland.glogi :as log]))
(defonce age-pem-header-line "-----BEGIN AGE ENCRYPTED FILE-----")
(defonce age-version-line "age-encryption.org/v1")
(defn content-encrypted?
[content]
(when content
(or (str/starts-with? content age-pem-header-line)
(str/starts-with? content age-version-line))))
(defn encrypted-db?
[repo-url]
(db-utils/get-key-value repo-url :db/encrypted?))
(defn get-key-pair
[repo-url]
(db-utils/get-key-value repo-url :db/encryption-keys))
(defn save-key-pair!
[repo-url keys]
(let [keys (if (string? keys) (reader/read-string keys) keys)]
(db/set-key-value repo-url :db/encryption-keys keys)
(db/set-key-value repo-url :db/encrypted? true)))
(defn generate-key-pair
[]
(p/let [_ (loader/load :age-encryption)
lazy-keygen (resolve 'frontend.extensions.age-encryption/keygen)
js-keys (lazy-keygen)]
(array-seq js-keys)))
(defn generate-key-pair-and-save!
[repo-url]
(when-not (get-key-pair repo-url)
(p/let [keys (generate-key-pair)]
(save-key-pair! repo-url keys)
(pr-str keys))))
(defn get-public-key
[repo-url]
(second (get-key-pair repo-url)))
(defn get-secret-key
[repo-url]
(first (get-key-pair repo-url)))
(defn encrypt
([content]
(encrypt (state/get-current-repo) content))
([repo-url content]
(cond
(encrypted-db? repo-url)
(p/let [_ (loader/load :age-encryption)
lazy-encrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/encrypt-with-x25519)
content (utf8/encode content)
public-key (get-public-key repo-url)
encrypted (lazy-encrypt-with-x25519 public-key content true)]
(utf8/decode encrypted))
:else
(p/resolved content))))
(defn decrypt
([content]
(decrypt (state/get-current-repo) content))
([repo-url content]
(cond
(and (encrypted-db? repo-url)
(content-encrypted? content))
(let [content (utf8/encode content)]
(if-let [secret-key (get-secret-key repo-url)]
(p/let [_ (loader/load :age-encryption)
lazy-decrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/decrypt-with-x25519)
decrypted (lazy-decrypt-with-x25519 secret-key content)]
(utf8/decode decrypted))
(log/error :encrypt/empty-secret-key (str "Can't find the secret key for repo: " repo-url))))
:else
(p/resolved content))))
(defn encrypt-with-passphrase
[passphrase content]
(p/let [_ (loader/load :age-encryption)
lazy-encrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/encrypt-with-user-passphrase)
content (utf8/encode content)
encrypted (@lazy-encrypt-with-user-passphrase passphrase content true)]
(utf8/decode encrypted)))
;; ;; TODO: What if decryption failed
(defn decrypt-with-passphrase
[passphrase content]
(p/let [_ (loader/load :age-encryption)
lazy-decrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/decrypt-with-user-passphrase)
content (utf8/encode content)
decrypted (lazy-decrypt-with-user-passphrase passphrase content)]
(utf8/decode decrypted)))

View File

@@ -0,0 +1,23 @@
(ns frontend.extensions.age-encryption
(:require ["regenerator-runtime/runtime"] ;; required for async npm module
["@kanru/rage-wasm" :as rage]))
(defn keygen
[]
(rage/keygen))
(defn encrypt-with-x25519
[public-key content armor]
(rage/encrypt_with_x25519 public-key content armor))
(defn decrypt-with-x25519
[secret-key content]
(rage/decrypt_with_x25519 secret-key content))
(defn encrypt-with-user-passphrase
[passphrase content armor]
(rage/encrypt_with_user_passphrase passphrase content armor))
(defn decrypt-with-user-passphrase
[passphrase content]
(rage/decrypt_with_user_passphrase passphrase content))

View File

@@ -11,7 +11,8 @@
[lambdaisland.glogi :as log]
[promesa.core :as p]
[frontend.db :as db]
[clojure.string :as string]))
[clojure.string :as string]
[frontend.encrypt :as encrypt]))
(defonce nfs-record (nfs/->Nfs))
(defonce bfs-record (bfs/->Bfs))
@@ -74,17 +75,20 @@
(defn write-file!
[repo dir path content opts]
(when content
(->
(p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)]
(when (= bfs-record (get-fs dir))
(db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.))))
(p/catch (fn [error]
(log/error :file/write-failed {:dir dir
:path path
:error error})
;; Disable this temporarily
;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
)))))
(let [fs-record (get-fs dir)]
(p/let [md-or-org? (contains? #{"md" "markdown" "org"} (util/get-file-ext path))
content (if-not md-or-org? content (encrypt/encrypt content))]
(->
(p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)]
(when (= bfs-record fs-record)
(db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.))))
(p/catch (fn [error]
(log/error :file/write-failed {:dir dir
:path path
:error error})
;; Disable this temporarily
;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
)))))))
(defn read-file
([dir path]

View File

@@ -9,7 +9,8 @@
[promesa.core :as p]
[rum.core :as rum]
[frontend.state :as state]
[frontend.db :as db]))
[frontend.db :as db]
[frontend.encrypt :as encrypt]))
(when (mobile-util/native-ios?)
(defn iOS-ensure-documents!
@@ -101,7 +102,10 @@
(defn- contents-matched?
[disk-content db-content]
(when (and (string? disk-content) (string? db-content))
(p/resolved (= (string/trim disk-content) (string/trim db-content)))))
(if (encrypt/encrypted-db? (state/get-current-repo))
(p/let [decrypted-content (encrypt/decrypt disk-content)]
(= (string/trim decrypted-content) (string/trim db-content)))
(p/resolved (= (string/trim disk-content) (string/trim db-content))))))
(defn- write-file-impl!
[_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
@@ -136,7 +140,8 @@
(not (contains? #{"excalidraw" "edn" "css"} ext))
(not (string/includes? path "/.recycle/"))
(zero? pending-writes))
(state/pub-event! [:file/not-matched-from-disk path disk-content content])
(p/let [disk-content (encrypt/decrypt disk-content)]
(state/pub-event! [:file/not-matched-from-disk path disk-content content]))
:else
(->
@@ -144,7 +149,10 @@
:data content
:encoding (.-UTF8 Encoding)
:recursive true}))]
(db/set-file-content! repo (js/decodeURI path) content)
(p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
(encrypt/decrypt content)
content)]
(db/set-file-content! repo (js/decodeURI path) content))
(when ok-handler
(ok-handler repo path result))
result)

View File

@@ -10,7 +10,8 @@
[frontend.config :as config]
[frontend.state :as state]
[frontend.handler.notification :as notification]
["/frontend/utils" :as utils]))
["/frontend/utils" :as utils]
[frontend.encrypt :as encrypt]))
;; We need to cache the file handles in the memory so that
;; the browser will not keep asking permissions.
@@ -57,7 +58,10 @@
(defn- contents-matched?
[disk-content db-content]
(when (and (string? disk-content) (string? db-content))
(p/resolved (= (string/trim disk-content) (string/trim db-content)))))
(if (encrypt/encrypted-db? (state/get-current-repo))
(p/let [decrypted-content (encrypt/decrypt disk-content)]
(= (string/trim decrypted-content) (string/trim db-content)))
(p/resolved (= (string/trim disk-content) (string/trim db-content))))))
(defrecord ^:large-vars/cleanup-todo Nfs []
protocol/Fs
@@ -171,12 +175,16 @@
(not (contains? #{"excalidraw" "edn" "css"} ext))
(not (string/includes? path "/.recycle/"))
(zero? pending-writes))
(state/pub-event! [:file/not-matched-from-disk path local-content content])
(p/let [local-content (encrypt/decrypt local-content)]
(state/pub-event! [:file/not-matched-from-disk path local-content content]))
(p/let [_ (verify-permission repo file-handle true)
_ (utils/writeFile file-handle content)
file (.getFile file-handle)]
(when file
(db/set-file-content! repo path content)
(p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
(encrypt/decrypt content)
content)]
(db/set-file-content! repo path content))
(nfs-saved-handler repo path file))))))
(p/catch (fn [e]
(js/console.error e))))

View File

@@ -8,7 +8,8 @@
[frontend.util :as util]
[goog.object :as gobj]
[lambdaisland.glogi :as log]
[promesa.core :as p]))
[promesa.core :as p]
[frontend.encrypt :as encrypt]))
(defn concat-path
[dir path]
@@ -27,7 +28,10 @@
(defn- contents-matched?
[disk-content db-content]
(when (and (string? disk-content) (string? db-content))
(p/resolved (= (string/trim disk-content) (string/trim db-content)))))
(if (encrypt/encrypted-db? (state/get-current-repo))
(p/let [decrypted-content (encrypt/decrypt disk-content)]
(= (string/trim decrypted-content) (string/trim db-content)))
(p/resolved (= (string/trim disk-content) (string/trim db-content))))))
(defn- write-file-impl!
[this repo dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
@@ -58,14 +62,18 @@
(not (contains? #{"excalidraw" "edn" "css"} ext))
(not (string/includes? path "/.recycle/"))
(zero? pending-writes))
(state/pub-event! [:file/not-matched-from-disk path disk-content content])
(p/let [disk-content (encrypt/decrypt disk-content)]
(state/pub-event! [:file/not-matched-from-disk path disk-content content]))
:else
(->
(p/let [result (ipc/ipc "writeFile" repo path content)
mtime (gobj/get result "mtime")]
(db/set-file-last-modified-at! repo path mtime)
(db/set-file-content! repo path content)
(p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
(encrypt/decrypt content)
content)]
(db/set-file-content! repo path content))
(when ok-handler
(ok-handler repo path result))
result)

View File

@@ -13,7 +13,8 @@
[lambdaisland.glogi :as log]
[electron.ipc :as ipc]
[promesa.core :as p]
[frontend.state :as state]))
[frontend.state :as state]
[frontend.encrypt :as encrypt]))
;; all IPC paths must be normalized! (via gp-util/path-normalize)
@@ -47,7 +48,9 @@
pages-metadata-path (config/get-pages-metadata-path)
{:keys [mtime]} stat
db-content (or (db/get-file repo path) "")]
(when (or content (= type "unlink"))
(when (and (or content (= type "unlink"))
(not (encrypt/content-encrypted? content))
(not (:encryption/graph-parsing? @state/state)))
(cond
(and (= "add" type)
(not= (string/trim content) (string/trim db-content))

View File

@@ -80,6 +80,14 @@
(state/set-config! repo-url config)
config)))
(defn read-metadata!
[content]
(try
(reader/read-string content)
(catch :default e
(log/error :parse/metadata-failed e)
{})))
(defn get-page-default-properties
[page-name]
{:title page-name

View File

@@ -38,7 +38,9 @@
[frontend.fs :as fs]
[clojure.string :as string]
[frontend.util.persist-var :as persist-var]
[frontend.fs.sync :as sync]))
[frontend.fs.sync :as sync]
[frontend.components.encryption :as encryption]
[frontend.encrypt :as encrypt]))
;; TODO: should we move all events here?
@@ -233,7 +235,8 @@
{:label "diff__cp"}))))
(defmethod handle :modal/display-file-version [[_ path content hash]]
(state/set-modal! #(git-component/file-specific-version path hash content)))
(p/let [content (when content (encrypt/decrypt content))]
(state/set-modal! #(git-component/file-specific-version path hash content))))
(defmethod handle :graph/ready [[_ repo]]
(search-handler/rebuild-indices-when-stale! repo)
@@ -352,6 +355,18 @@
(defmethod handle :rebuild-slash-commands-list [[_]]
(page-handler/rebuild-slash-commands-list!))
;; encryption
(defmethod handle :modal/encryption-setup-dialog [[_ repo-url close-fn]]
(state/set-modal!
(encryption/encryption-setup-dialog repo-url close-fn)))
(defmethod handle :modal/encryption-input-secret-dialog [[_ repo-url db-encrypted-secret close-fn]]
(state/set-modal!
(encryption/encryption-input-secret-dialog
repo-url
db-encrypted-secret
close-fn)))
(defn run!
[]
(let [chan (state/get-events-chan)]

View File

@@ -16,7 +16,7 @@
[frontend.handler.ui :as ui-handler]
[frontend.state :as state]
[frontend.util :as util]
[logseq.graph-parser.util :as gp-util]
[logseq.graph-parser.util :as gp-util]
[lambdaisland.glogi :as log]
[promesa.core :as p]
[frontend.mobile.util :as mobile]
@@ -272,6 +272,17 @@
(when-let [dir (config/get-repo-dir repo)]
(fs/watch-dir! dir))))
(defn create-metadata-file
[repo-url encrypted?]
(let [repo-dir (config/get-repo-dir repo-url)
path (str config/app-name "/" config/metadata-file)
file-path (str "/" path)
default-content (if encrypted? "{:db/encrypted? true}" "{}")]
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
(when-not file-exists?
(reset-file! repo-url path default-content)))))
(defn create-pages-metadata-file
[repo-url]
(let [repo-dir (config/get-repo-dir repo-url)

View File

@@ -1,12 +1,44 @@
(ns frontend.handler.metadata
(:require [cljs.pprint]
(:require [cljs.reader :as reader]
[cljs.pprint]
[clojure.string :as string]
[datascript.db :as ddb]
[frontend.config :as config]
[frontend.db :as db]
[frontend.fs :as fs]
[frontend.handler.common :as common-handler]
[frontend.handler.file :as file-handler]
[frontend.state :as state]
[promesa.core :as p]))
(def default-metadata-str "{}")
(defn set-metadata!
[k v]
(when-let [repo (state/get-current-repo)]
(let [encrypted? (= k :db/encrypted-secret)
path (config/get-metadata-path)
file-content (db/get-file path)]
(p/let [_ (file-handler/create-metadata-file repo false)]
(let [metadata-str (or file-content default-metadata-str)
metadata (try
(reader/read-string metadata-str)
(catch js/Error e
(println "Parsing metadata.edn failed: ")
(js/console.dir e)
{}))
new-metadata (cond
(= k :block/properties)
(update metadata :block/properties v) ; v should be a function
:else
(let [ks (if (vector? k) k [k])]
(assoc-in metadata ks v)))
new-metadata (if encrypted?
(assoc new-metadata :db/encrypted? true)
new-metadata)
new-content (pr-str new-metadata)]
(file-handler/set-file-content! repo path new-content))))))
(defn set-pages-metadata!
[repo]
(let [path (config/get-pages-metadata-path repo)
@@ -23,3 +55,31 @@
path
new-content
{})))))
(defn set-db-encrypted-secret!
[encrypted-secret]
(when-not (string/blank? encrypted-secret)
(set-metadata! :db/encrypted-secret encrypted-secret)))
(defn- handler-properties!
[all-properties properties-tx]
(reduce
(fn [acc datom]
(let [v (:v datom)
id (or (get v :id)
(get v :title))]
(if id
(let [added? (ddb/datom-added datom)
remove-all-properties? (and (not added?)
;; only id
(= 1 (count v)))]
(if remove-all-properties?
(dissoc acc id)
(assoc acc id v)))
acc)))
all-properties
properties-tx))
(defn update-properties!
[properties-tx]
(set-metadata! :block/properties #(handler-properties! % properties-tx)))

View File

@@ -25,7 +25,8 @@
[logseq.graph-parser.util :as gp-util]
[electron.ipc :as ipc]
[clojure.set :as set]
[clojure.core.async :as async]))
[clojure.core.async :as async]
[frontend.encrypt :as encrypt]))
;; Project settings should be checked in two situations:
;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
@@ -128,16 +129,19 @@
(ui-handler/re-render-root!)))))))
(defn create-default-files!
[repo-url]
(spec/validate :repos/url repo-url)
(let [repo-dir (config/get-repo-dir repo-url)]
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name "/" config/recycle-dir))
_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory)))
_ (create-config-file-if-not-exists repo-url)
_ (create-contents-file repo-url)
_ (create-custom-theme repo-url)]
(state/pub-event! [:page/create-today-journal repo-url]))))
([repo-url]
(create-default-files! repo-url false))
([repo-url encrypted?]
(spec/validate :repos/url repo-url)
(let [repo-dir (config/get-repo-dir repo-url)]
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name "/" config/recycle-dir))
_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory)))
_ (file-handler/create-metadata-file repo-url encrypted?)
_ (create-config-file-if-not-exists repo-url)
_ (create-contents-file repo-url)
_ (create-custom-theme repo-url)]
(state/pub-event! [:page/create-today-journal repo-url])))))
(defn- load-pages-metadata!
"force?: if set true, skip the metadata timestamp range check"
@@ -197,18 +201,21 @@
(update m :finished inc))))
(defn- after-parse
[repo-url files file-paths re-render? re-render-opts opts graph-added-chan]
[repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan]
(load-pages-metadata! repo-url file-paths files true)
(when (:new-graph? opts)
(create-default-files! repo-url))
(if (and (not db-encrypted?) (state/enable-encryption? repo-url))
(state/pub-event! [:modal/encryption-setup-dialog repo-url
#(create-default-files! repo-url %)])
(create-default-files! repo-url db-encrypted?)))
(when re-render?
(ui-handler/re-render-root! re-render-opts))
(state/pub-event! [:graph/added repo-url opts])
(state/reset-parsing-state!)
(async/offer! graph-added-chan true))
(defn- parse-files-and-create-default-files!
[repo-url files delete-files delete-blocks file-paths re-render? re-render-opts opts]
(defn- parse-files-and-create-default-files-inner!
[repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts]
(let [support-files (filter
(fn [file]
(let [format (format/get-format (:file/path file))]
@@ -234,16 +241,27 @@
(do
(doseq [file support-files']
(parse-and-load-file! repo-url file new-graph?))
(after-parse repo-url files file-paths re-render? re-render-opts opts graph-added-chan))
(after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan))
(async/go-loop []
(if-let [file (async/<! chan)]
(do
(parse-and-load-file! repo-url file new-graph?)
(async/<! (async/timeout 10))
(recur))
(after-parse repo-url files file-paths re-render? re-render-opts opts graph-added-chan))))
(after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan))))
graph-added-chan))
(defn- parse-files-and-create-default-files!
[repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts]
(if db-encrypted?
(p/let [files (p/all
(map (fn [file]
(p/let [content (encrypt/decrypt (:file/content file))]
(assoc file :file/content content)))
files))]
(parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts))
(parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts)))
(defn- update-parsing-state!
[repo-url]
(state/set-loading-files! repo-url false))
@@ -253,8 +271,21 @@
:or {re-render? true}}]
(update-parsing-state! repo-url)
(let [file-paths (map :file/path files)]
(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths re-render? re-render-opts opts)))
(let [file-paths (map :file/path files)
metadata-file (config/get-metadata-path)
metadata-content (some #(when (= (:file/path %) metadata-file)
(:file/content %)) files)
metadata (when metadata-content
(common-handler/read-metadata! metadata-content))
db-encrypted? (:db/encrypted? metadata)
db-encrypted-secret (if db-encrypted? (:db/encrypted-secret metadata) nil)]
(if db-encrypted?
(let [close-fn #(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts)]
(state/set-state! :encryption/graph-parsing? true)
(state/pub-event! [:modal/encryption-input-secret-dialog repo-url
db-encrypted-secret
close-fn]))
(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts))))
(defn load-repo-to-db!
[repo-url {:keys [diffs nfs-files refresh? new-graph? empty-graph?]}]

View File

@@ -22,7 +22,8 @@
[frontend.mobile.util :as mobile-util]
[logseq.graph-parser.util :as gp-util]
[logseq.graph-parser.config :as gp-config]
[clojure.core.async :as async]))
[clojure.core.async :as async]
[frontend.encrypt :as encrypt]))
(defn remove-ignore-files
[files]
@@ -168,7 +169,8 @@
(-> (p/all (map (fn [file]
(p/let [content (if nfs?
(.text (:file/file file))
(:file/content file))]
(:file/content file))
content (encrypt/decrypt content)]
(assoc file :file/content content))) markup-files))
(p/then (fn [result]
(let [files (map #(dissoc % :file/file) result)]
@@ -247,7 +249,8 @@
(when-let [file (get-file-f path new-files)]
(p/let [content (if nfs?
(.text (:file/file file))
(:file/content file))]
(:file/content file))
content (encrypt/decrypt content)]
(assoc file :file/content content)))) added-or-modified))
(p/then (fn [result]
(let [files (map #(dissoc % :file/file :file/handle) result)

View File

@@ -216,6 +216,8 @@
:file-sync/sync-state nil
:file-sync/sync-uploading-files nil
:file-sync/sync-downloading-files nil
:encryption/graph-parsing? false
})))
;; block uuid -> {content(String) -> ast}
@@ -1674,3 +1676,8 @@
(update-state! [:graph/parsing-state (get-current-repo)]
(if (fn? m) m
(fn [old-value] (merge old-value m)))))
(defn enable-encryption?
[repo]
(:feature/enable-encryption?
(get (sub-config) repo)))