Bladeren bron

feat: add encryption back

Tienson Qin 3 jaren geleden
bovenliggende
commit
83569141fc

+ 1 - 0
resources/index.html

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

+ 6 - 6
shadow-cljs.edn

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

+ 1 - 1
src/electron/electron/core.cljs

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

+ 176 - 0
src/main/frontend/components/encryption.cljs

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

+ 8 - 1
src/main/frontend/components/repo.cljs

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

+ 10 - 0
src/main/frontend/components/settings.cljs

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

+ 8 - 0
src/main/frontend/config.cljs

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

+ 2 - 0
src/main/frontend/db_schema.cljs

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

+ 9 - 0
src/main/frontend/dicts.cljc

@@ -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 "デフォルトのホームページを設定"

+ 104 - 0
src/main/frontend/encrypt.cljs

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

+ 23 - 0
src/main/frontend/extensions/age_encryption.cljs

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

+ 16 - 12
src/main/frontend/fs.cljs

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

+ 12 - 4
src/main/frontend/fs/capacitor_fs.cljs

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

+ 12 - 4
src/main/frontend/fs/nfs.cljs

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

+ 12 - 4
src/main/frontend/fs/node.cljs

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

+ 5 - 2
src/main/frontend/fs/watcher_handler.cljs

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

+ 8 - 0
src/main/frontend/handler/common.cljs

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

+ 17 - 2
src/main/frontend/handler/events.cljs

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

+ 12 - 1
src/main/frontend/handler/file.cljs

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

+ 61 - 1
src/main/frontend/handler/metadata.cljs

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

+ 50 - 19
src/main/frontend/handler/repo.cljs

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

+ 6 - 3
src/main/frontend/handler/web/nfs.cljs

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

+ 7 - 0
src/main/frontend/state.cljs

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