encryption.cljs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. (ns frontend.components.encryption
  2. (:require [clojure.string :as string]
  3. [frontend.context.i18n :refer [t]]
  4. [frontend.encrypt :as encrypt]
  5. [frontend.handler.metadata :as metadata-handler]
  6. [frontend.handler.notification :as notification]
  7. [frontend.fs.sync :as sync]
  8. [frontend.state :as state]
  9. [frontend.ui :as ui]
  10. [frontend.util :as util]
  11. [frontend.config :as config]
  12. [promesa.core :as p]
  13. [cljs.core.async :as async]
  14. [rum.core :as rum]))
  15. (rum/defcs encryption-dialog-inner <
  16. (rum/local false ::reveal-secret-phrase?)
  17. [state repo-url close-fn]
  18. (let [reveal-secret-phrase? (get state ::reveal-secret-phrase?)
  19. public-key (encrypt/get-public-key repo-url)
  20. private-key (encrypt/get-secret-key repo-url)]
  21. [:div
  22. [:div.sm:flex.sm:items-start
  23. [:div.mt-3.text-center.sm:mt-0.sm:text-left
  24. [:h3#modal-headline.text-lg.leading-6.font-medium
  25. "This graph is encrypted with " [:a {:href "https://age-encryption.org/" :target "_blank" :rel "noopener"} "age-encryption.org/v1"]]]]
  26. [:div.mt-1
  27. [:div.max-w-2xl.rounded-md.shadow-sm.sm:max-w-xl
  28. [:div.cursor-pointer.block.w-full.rounded-sm.p-2
  29. {:on-click (fn []
  30. (when (not @reveal-secret-phrase?)
  31. (reset! reveal-secret-phrase? true)))}
  32. [:div.font-medium "Public Key:"]
  33. [:div.font-mono.select-all.break-all public-key]
  34. (if @reveal-secret-phrase?
  35. [:div
  36. [:div.mt-1.font-medium "Private Key:"]
  37. [:div.font-mono.select-all.break-all private-key]]
  38. [:div.underline "click to view the private key"])]]]
  39. [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
  40. [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
  41. [: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
  42. {:type "button"
  43. :on-click close-fn}
  44. (t :close)]]]]))
  45. (defn encryption-dialog
  46. [repo-url]
  47. (fn [close-fn]
  48. (encryption-dialog-inner repo-url close-fn)))
  49. (rum/defc show-password-cp
  50. [*show-password?]
  51. [:div.flex.flex-row.items-center
  52. [:label.px-1 {:for "show-password"}
  53. (ui/checkbox {:checked? @*show-password?
  54. :on-change (fn [e]
  55. (reset! *show-password? (util/echecked? e)))
  56. :id "show-password"})
  57. [:span.text-sm.ml-1.opacity-80.select-none.px-1 "Show password"]]])
  58. (rum/defcs ^:large-vars/cleanup-todo input-password-inner < rum/reactive
  59. (rum/local "" ::password)
  60. (rum/local "" ::pw-confirm)
  61. (rum/local false ::pw-confirm-focused?)
  62. (rum/local false ::show-password?)
  63. {:will-mount (fn [state]
  64. ;; try to close tour tips
  65. (some->> (state/sub :file-sync/jstour-inst)
  66. (.complete))
  67. state)}
  68. [state repo-url close-fn {:keys [type GraphName GraphUUID init-graph-keys after-input-password]}]
  69. (let [*password (get state ::password)
  70. *pw-confirm (get state ::pw-confirm)
  71. *pw-confirm-focused? (get state ::pw-confirm-focused?)
  72. *show-password? (get state ::show-password?)
  73. *input-ref-0 (rum/create-ref)
  74. *input-ref-1 (rum/create-ref)
  75. remote-pw? (= type :input-pwd-remote)
  76. loading? (state/sub [:ui/loading? :set-graph-password])
  77. pw-strength (when (and init-graph-keys
  78. (not (string/blank? @*password)))
  79. (util/check-password-strength @*password))
  80. can-submit? #(if init-graph-keys
  81. (and (>= (count @*password) 6)
  82. (>= (:id pw-strength) 1))
  83. true)
  84. set-remote-graph-pwd-result (state/sub [:file-sync/set-remote-graph-password-result])
  85. submit-handler
  86. (fn []
  87. (let [value @*password]
  88. (cond
  89. (string/blank? value)
  90. nil
  91. (and init-graph-keys (not= @*password @*pw-confirm))
  92. (notification/show! "The passwords are not matched." :error)
  93. :else
  94. (case type
  95. :local
  96. (p/let [keys (encrypt/generate-key-pair-and-save! repo-url)
  97. db-encrypted-secret (encrypt/encrypt-with-passphrase value keys)]
  98. (metadata-handler/set-db-encrypted-secret! db-encrypted-secret)
  99. (close-fn true))
  100. (:create-pwd-remote :input-pwd-remote)
  101. (do
  102. (state/set-state! [:ui/loading? :set-graph-password] true)
  103. (state/set-state! [:file-sync/set-remote-graph-password-result] {})
  104. (async/go
  105. (let [persist-r (async/<! (sync/encrypt+persist-pwd! @*password GraphUUID))]
  106. (if (instance? js/Error persist-r)
  107. (js/console.error persist-r)
  108. (when (fn? after-input-password)
  109. (async/<! (after-input-password))
  110. ;; TODO: it's better if based on sync state
  111. (when init-graph-keys
  112. (js/setTimeout #(state/pub-event! [:file-sync/maybe-onboarding-show :sync-learn]) 10000)))))))))))
  113. cancel-handler
  114. (fn []
  115. (state/set-state! [:file-sync/set-remote-graph-password-result] {})
  116. (close-fn))
  117. enter-handler
  118. (fn [^js e]
  119. (when-let [^js input (and e (= 13 (.-which e)) (.-target e))]
  120. (when-not (string/blank? (.-value input))
  121. (let [input-0? (= (util/safe-lower-case (.-placeholder input)) "password")]
  122. (if init-graph-keys
  123. ;; setup mode
  124. (if input-0?
  125. (.select (rum/deref *input-ref-1))
  126. (submit-handler))
  127. ;; unlock mode
  128. (submit-handler))))))]
  129. [:div.encryption-password.max-w-2xl.-mb-2
  130. [:div.cp__file-sync-related-normal-modal
  131. [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "lock-access" {:size 28})]]
  132. [:div.mt-3.text-center.sm:mt-0.sm:text-left
  133. [:h1#modal-headline.text-2xl.font-bold.text-center
  134. (if init-graph-keys
  135. (if remote-pw?
  136. "Secure this remote graph!"
  137. "Encrypt this graph")
  138. (if remote-pw?
  139. "Unlock this remote graph!"
  140. "Decrypt this graph"))]]
  141. ;; decrypt remote graph with one password
  142. (when (and remote-pw? (not init-graph-keys))
  143. [:<>
  144. [:div.folder-tip.flex.flex-col.items-center
  145. [:h3
  146. [:span.flex.space-x-2.leading-none.pb-1
  147. (ui/icon "cloud-lock" {:size 20})
  148. [:span GraphName]
  149. [:span.scale-75 (ui/icon "arrow-right")]
  150. [:span (ui/icon "folder")]]]
  151. [:h4.px-2.-mb-1.5 (config/get-string-repo-dir repo-url)]]
  152. [:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.mt-2.flex.items-center
  153. (if-let [display-str (:fail set-remote-graph-pwd-result)]
  154. [:<>
  155. [:span.flex.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
  156. [:span.text-red-600 display-str]]
  157. [:<>
  158. [:span.flex.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})]
  159. [:span "Please enter the password for this graph to continue syncing."]])]])
  160. ;; secure this remote graph
  161. (when (and remote-pw? init-graph-keys)
  162. (let [pattern-ok? #(>= (count @*password) 6)]
  163. [:<>
  164. [:h2.text-center.opacity-70.text-sm.py-2
  165. "Each graph you want to synchronize via Logseq needs its own password for end-to-end encryption."]
  166. [:div.input-hints.text-sm.py-2.px-3.rounded.mb-3.mt-4.flex.items-center
  167. (if (or (not (string/blank? @*password))
  168. (not (string/blank? @*pw-confirm)))
  169. (if (or (not (pattern-ok?))
  170. (not= @*password @*pw-confirm))
  171. [:span.flex.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
  172. [:span.flex.pr-1.text-green-600 (ui/icon "circle-check" {:class "text-md mr-1"})])
  173. [:span.flex.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})])
  174. (if (not (string/blank? @*password))
  175. (if-not (pattern-ok?)
  176. [:span "Password can't be less than 6 characters"]
  177. (if (not (string/blank? @*pw-confirm))
  178. (if (not= @*pw-confirm @*password)
  179. [:span "Password fields are not matching!"]
  180. [:span "Password fields are matching!"])
  181. [:span "Enter your chosen password again!"]))
  182. [:span "Choose a strong and hard to guess password!"])
  183. ]
  184. ;; password strength checker
  185. (when-not (string/blank? @*password)
  186. [:<>
  187. [:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.-mt-1.5.flex.items-center.space-x-3
  188. (let [included-set (set (:contains pw-strength))]
  189. (for [i ["lowercase" "uppercase" "number" "symbol"]
  190. :let [included? (contains? included-set i)]]
  191. [:span.strength-item
  192. {:key i
  193. :class (when included? "included")}
  194. (ui/icon (if included? "check" "x") {:class "mr-1"})
  195. [:span.capitalize i]
  196. ]))]
  197. [:div.input-pw-strength
  198. [:div.indicator.flex
  199. (for [i (range 4)
  200. :let [title (get ["Too weak" "Weak" "Medium" "Strong"] i)]]
  201. [:i {:key i
  202. :title title
  203. :class (when (>= (int (:id pw-strength)) i) "active")} i])]]])]))
  204. [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
  205. {:type (if @*show-password? "text" "password")
  206. :ref *input-ref-0
  207. :placeholder "Password"
  208. :auto-focus true
  209. :disabled loading?
  210. :on-key-up enter-handler
  211. :on-change (fn [^js e]
  212. (reset! *password (util/evalue e))
  213. (when (:fail set-remote-graph-pwd-result)
  214. (state/set-state! [:file-sync/set-remote-graph-password-result] {})))}]
  215. (when init-graph-keys
  216. [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
  217. {:type (if @*show-password? "text" "password")
  218. :ref *input-ref-1
  219. :placeholder "Re-enter the password"
  220. :on-focus #(reset! *pw-confirm-focused? true)
  221. :on-blur #(reset! *pw-confirm-focused? false)
  222. :disabled loading?
  223. :on-key-up enter-handler
  224. :on-change (fn [^js e]
  225. (reset! *pw-confirm (util/evalue e)))}])
  226. (show-password-cp *show-password?)
  227. (when init-graph-keys
  228. [:div.init-remote-pw-tips.space-x-4.pt-2.hidden.sm:flex
  229. [:div.flex-1.flex.items-center
  230. [:span.px-3.flex (ui/icon "key")]
  231. [:p.dark:text-gray-100
  232. [:span "Please make sure you "]
  233. "remember the password you have set, "
  234. [:span "and we recommend you "]
  235. "keep a secure backup "
  236. [:span "of the password."]]]
  237. [:div.flex-1.flex.items-center
  238. [:span.px-3.flex (ui/icon "lock")]
  239. [:p.dark:text-gray-100
  240. "If you lose your password, all of your data in the cloud can’t be decrypted. "
  241. [:span "You will still be able to access the local version of your graph."]]]])]
  242. [:div.mt-5.sm:mt-4.flex.justify-center.sm:justify-end.space-x-3
  243. (ui/button (t :cancel) :background "gray" :disabled loading? :class "opacity-60" :on-click cancel-handler)
  244. (ui/button [:span.inline-flex.items-center.leading-none
  245. [:span (t :submit)]
  246. (when loading?
  247. [:span.ml-1 (ui/loading "" {:class "w-4 h-4"})])]
  248. :disabled (or (not (can-submit?)) loading?)
  249. :on-click submit-handler)]]))
  250. (defn input-password
  251. ([repo-url close-fn] (input-password repo-url close-fn {:type :local}))
  252. ([repo-url close-fn opts]
  253. (fn [close-fn']
  254. (let [close-fn' (if (fn? close-fn)
  255. #(do (close-fn %)
  256. (close-fn'))
  257. close-fn')]
  258. (input-password-inner repo-url close-fn' opts)))))
  259. (rum/defcs encryption-setup-dialog-inner
  260. [state repo-url close-fn]
  261. [:div
  262. [:div.sm:flex.sm:items-start
  263. [:div.mt-3.text-center.sm:mt-0.sm:text-left
  264. [:h3#modal-headline.text-lg.leading-6.font-medium
  265. "Do you want to create an encrypted graph?"]]]
  266. [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
  267. [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
  268. [: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
  269. {:type "button"
  270. :on-click (fn []
  271. (state/set-modal!
  272. (input-password repo-url close-fn)
  273. {:center? true :close-btn? false}))}
  274. (t :yes)]]
  275. [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
  276. [: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
  277. {:type "button"
  278. :on-click (fn [] (close-fn false))}
  279. (t :no)]]]])
  280. (defn encryption-setup-dialog
  281. [repo-url close-fn]
  282. (fn [close-modal-fn]
  283. (let [close-fn (fn [encrypted?]
  284. (close-fn encrypted?)
  285. (close-modal-fn))]
  286. (encryption-setup-dialog-inner repo-url close-fn))))
  287. (rum/defcs encryption-input-secret-inner <
  288. (rum/local "" ::secret)
  289. (rum/local false ::loading)
  290. (rum/local false ::show-password?)
  291. [state _repo-url db-encrypted-secret close-fn]
  292. (let [secret (::secret state)
  293. loading (::loading state)
  294. *show-password? (::show-password? state)
  295. on-click-fn (fn []
  296. (reset! loading true)
  297. (let [value @secret]
  298. (when-not (string/blank? value) ; TODO: length or other checks
  299. (let [repo (state/get-current-repo)]
  300. (p/do!
  301. (-> (encrypt/decrypt-with-passphrase value db-encrypted-secret)
  302. (p/then (fn [keys]
  303. (encrypt/save-key-pair! repo keys)
  304. (close-fn true)
  305. (state/set-state! :encryption/graph-parsing? false)))
  306. (p/catch #(notification/show! "The password is not matched." :warning true))
  307. (p/finally #(reset! loading false))))))))]
  308. [:div
  309. [:div.sm:flex.sm:items-start
  310. [:div.mt-3.text-center.sm:mt-0.sm:text-left
  311. [:h3#modal-headline.text-lg.leading-6.font-medium
  312. "Enter your password"]]]
  313. [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
  314. {:type (if @*show-password? "text" "password")
  315. :auto-focus true
  316. :on-change (fn [e]
  317. (reset! secret (util/evalue e)))
  318. :on-key-down (fn [e]
  319. (when (= (.-key e) "Enter")
  320. (on-click-fn)))}]
  321. (show-password-cp *show-password?)
  322. [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
  323. [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
  324. [: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
  325. {:type "button"
  326. :on-click on-click-fn}
  327. (if @loading (ui/loading "Decrypting") "Decrypt")]]]]))
  328. (defn encryption-input-secret-dialog
  329. [repo-url db-encrypted-secret close-fn]
  330. (fn [close-modal-fn]
  331. (let [close-fn (fn [encrypted?]
  332. (close-fn encrypted?)
  333. (close-modal-fn))]
  334. (encryption-input-secret-inner repo-url db-encrypted-secret close-fn))))