Browse Source

enhance(rtc,e2ee,wip): add frontend.common.crypt, frontend.worker.rtc.crypt

rcmerci 3 days ago
parent
commit
ad0229ff96

+ 2 - 0
prompts/review.md

@@ -23,3 +23,5 @@ You're Clojure(script) expert, you're responsible to check those common errors:
   - e.g. `["65.9" {:properties [:logseq.property.embedding/hnsw-label-updated-at]}]`
 
 - If common keywords are added or modified, make corresponding changes in their definitions.
+
+- A function that returns a promise, and its function name starts with "<".

+ 172 - 0
src/main/frontend/common/crypt.cljs

@@ -0,0 +1,172 @@
+(ns frontend.common.crypt
+  (:require [promesa.core :as p]))
+
+(defonce subtle (.. js/crypto -subtle))
+
+(defn <generate-rsa-key-pair
+  "Generates a new RSA public/private key pair.
+  Return
+  {:publicKey #object [CryptoKey [object CryptoKey]],
+   :privateKey #object [CryptoKey [object CryptoKey]]}"
+  []
+  (p/let [r (.generateKey subtle
+                          #js {:name "RSA-OAEP"
+                               :modulusLength 4096
+                               :publicExponent (js/Uint8Array. [1 0 1])
+                               :hash "SHA-256"}
+                          true
+                          #js ["encrypt" "decrypt"])]
+    (js->clj r :keywordize-keys true)))
+
+(defn <generate-aes-key
+  "Generates a new AES-GCM-256 key."
+  []
+  (.generateKey subtle
+                #js {:name "AES-GCM"
+                     :length 256}
+                true
+                #js ["encrypt" "decrypt"]))
+
+(defn <encrypt-private-key
+  "Encrypts a private key with a password."
+  [password private-key]
+  (p/let [salt (js/crypto.getRandomValues (js/Uint8Array. 16))
+          iv (js/crypto.getRandomValues (js/Uint8Array. 12))
+          password-key (.importKey subtle "raw"
+                                   (.encode (js/TextEncoder.) password)
+                                   "PBKDF2"
+                                   false
+                                   #js ["deriveKey"])
+          derived-key (.deriveKey subtle
+                                  #js {:name "PBKDF2"
+                                       :salt salt
+                                       :iterations 100000
+                                       :hash "SHA-256"}
+                                  password-key
+                                  #js {:name "AES-GCM" :length 256}
+                                  true
+                                  #js ["encrypt" "decrypt"])
+          exported-private-key (.exportKey subtle "pkcs8" private-key)
+          encrypted-private-key (.encrypt subtle
+                                          #js {:name "AES-GCM" :iv iv}
+                                          derived-key
+                                          exported-private-key)]
+    [salt iv encrypted-private-key]))
+
+(defn <decrypt-private-key
+  "Decrypts a private key with a password."
+  [password encrypted-key-data]
+  (p/let [[salt-data iv-data encrypted-private-key-data] encrypted-key-data
+          salt (js/Uint8Array. salt-data)
+          iv (js/Uint8Array. iv-data)
+          encrypted-private-key (js/Uint8Array. encrypted-private-key-data)
+          password-key (.importKey subtle "raw"
+                                   (.encode (js/TextEncoder.) password)
+                                   "PBKDF2"
+                                   false
+                                   #js ["deriveKey"])
+          derived-key (.deriveKey subtle
+                                  #js {:name "PBKDF2"
+                                       :salt salt
+                                       :iterations 100000
+                                       :hash "SHA-256"}
+                                  password-key
+                                  #js {:name "AES-GCM" :length 256}
+                                  true
+                                  #js ["encrypt" "decrypt"])
+          decrypted-private-key-data (.decrypt subtle
+                                               #js {:name "AES-GCM" :iv iv}
+                                               derived-key
+                                               encrypted-private-key)
+          private-key (.importKey subtle "pkcs8"
+                                  decrypted-private-key-data
+                                  #js {:name "RSA-OAEP" :hash "SHA-256"}
+                                  true
+                                  #js ["decrypt"])]
+    private-key))
+
+(defn <encrypt-aes-key
+  "Encrypts an AES key with a public key."
+  [public-key aes-key]
+  (p/let [exported-aes-key (.exportKey subtle "raw" aes-key)]
+    (.encrypt subtle
+              #js {:name "RSA-OAEP"}
+              public-key
+              exported-aes-key)))
+
+(defn <decrypt-aes-key
+  "Decrypts an AES key with a private key."
+  [private-key encrypted-aes-key-data]
+  (p/let [encrypted-aes-key (js/Uint8Array. encrypted-aes-key-data)
+          decrypted-key-data (.decrypt subtle
+                                       #js {:name "RSA-OAEP"}
+                                       private-key
+                                       encrypted-aes-key)]
+    (.importKey subtle
+                "raw"
+                decrypted-key-data
+                "AES-GCM"
+                true
+                #js ["encrypt" "decrypt"])))
+
+(defn <encrypt-text
+  "Encrypts text with an AES key."
+  [aes-key text]
+  (p/let [iv (js/crypto.getRandomValues (js/Uint8Array. 12))
+          encoded-text (.encode (js/TextEncoder.) text)
+          encrypted-data (.encrypt subtle
+                                   #js {:name "AES-GCM" :iv iv}
+                                   aes-key
+                                   encoded-text)]
+    [iv encrypted-data]))
+
+(defn <decrypt-text
+  "Decrypts text with an AES key."
+  [aes-key encrypted-text-data]
+  (p/let [[iv-data encrypted-data-from-db] encrypted-text-data
+          iv (js/Uint8Array. iv-data)
+          encrypted-data (js/Uint8Array. encrypted-data-from-db)
+          decrypted-data (.decrypt subtle
+                                   #js {:name "AES-GCM" :iv iv}
+                                   aes-key
+                                   encrypted-data)
+          decoded-text (.decode (js/TextDecoder.) decrypted-data)]
+    decoded-text))
+
+(comment
+  (let [array-buffers-equal?
+        (fn [^js/ArrayBuffer buf1 ^js/ArrayBuffer buf2]
+          (if (not= (.-byteLength buf1) (.-byteLength buf2))
+            false
+            (let [arr1 (js/Uint8Array. buf1)
+                  arr2 (js/Uint8Array. buf2)]
+              (= (vec arr1) (vec arr2)))))]
+    (p/let [rsa-key-pair (<generate-rsa-key-pair)
+            aes-key (<generate-aes-key)
+            public-key (:publicKey rsa-key-pair)
+            private-key (:privateKey rsa-key-pair)
+            encrypted-aes-key (<encrypt-aes-key public-key aes-key)
+            decrypted-aes-key (<decrypt-aes-key private-key encrypted-aes-key)
+            password "my-secret-password"
+            encrypted-private-key (<encrypt-private-key password private-key)
+            decrypted-private-key (<decrypt-private-key password encrypted-private-key)
+            ;; Export keys to compare their raw values
+            exported-original-aes (.exportKey subtle "raw" aes-key)
+            exported-decrypted-aes (.exportKey subtle "raw" decrypted-aes-key)
+            exported-original-private (.exportKey subtle "pkcs8" private-key)
+            exported-decrypted-private (.exportKey subtle "pkcs8" decrypted-private-key)
+            ;; Test text encryption
+            original-text "This is a secret message."
+            encrypted-text-data (<encrypt-text aes-key original-text)
+            decrypted-text (<decrypt-text aes-key encrypted-text-data)]
+      (js/console.log "Original AES key:" aes-key)
+      (js/console.log "Decrypted AES key:" decrypted-aes-key)
+      (js/console.log "Original private key:" private-key)
+      (js/console.log "Decrypted private key:" decrypted-private-key)
+      (let [aes-match? (array-buffers-equal? exported-original-aes exported-decrypted-aes)
+            private-key-match? (array-buffers-equal? exported-original-private exported-decrypted-private)]
+        (js/console.log "AES keys match:" aes-match?)
+        (js/console.log "Private keys match:" private-key-match?))
+      (js/console.log "Original text:" original-text)
+      (js/console.log "Decrypted text:" decrypted-text)
+      (js/console.log "Texts match:" (= original-text decrypted-text)))))

+ 2 - 1
src/main/frontend/worker/crypt.cljs

@@ -1,5 +1,6 @@
 (ns frontend.worker.crypt
-  "Fns to en/decrypt some block attrs"
+  "Deprecated ns. Remove later.
+  Fns to en/decrypt some block attrs"
   (:require [datascript.core :as d]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.worker.state :as worker-state]

+ 193 - 0
src/main/frontend/worker/rtc/crypt.cljs

@@ -0,0 +1,193 @@
+(ns frontend.worker.rtc.crypt
+  "rtc e2ee related.
+  Each user has an RSA key pair.
+  Each graph has an AES key.
+  Server stores the encrypted AES key, public key, and encrypted private key."
+  (:require ["/frontend/idbkv" :as idb-keyval]
+            [frontend.common.crypt :as crypt]
+            [frontend.common.missionary :as c.m]
+            [frontend.worker.rtc.ws-util :as ws-util]
+            [lambdaisland.glogi :as log]
+            [missionary.core :as m]
+            [promesa.core :as p]))
+
+(defonce ^:private store (delay (idb-keyval/newStore "localforage" "keyvaluepairs" 2)))
+
+(defn- <get-item
+  [k]
+  (assert (and k @store))
+  (p/let [r (idb-keyval/get k @store)]
+    (js->clj r :keywordize-keys true)))
+
+(defn- <set-item!
+  [k value]
+  (assert (and k @store))
+  (idb-keyval/set k value @store))
+
+(defn- <remove-item!
+  [k]
+  (idb-keyval/del k @store))
+
+(defn- graph-encrypted-aes-key-idb-key
+  [repo]
+  (assert (some? repo))
+  (str "rtc-encrypted-aes-key###" repo))
+
+(defn- user-rsa-key-pair-idb-key
+  [user-uuid]
+  (assert (some? user-uuid))
+  (str "user-rsa-key-pair###" user-uuid))
+
+(defn task--upload-graph-encrypted-aes-key
+  "Uploads the encrypted AES key for a graph to the server."
+  [token graph-uuid encrypted-aes-key]
+  (m/sp
+    (let [{:keys [get-ws-create-task]} (ws-util/gen-get-ws-create-map--memoized (ws-util/get-ws-url token))
+          response (m/? (ws-util/send&recv get-ws-create-task
+                                           {:action "upload-graph-encrypted-aes-key"
+                                            :graph-uuid graph-uuid
+                                            :encrypted-aes-key encrypted-aes-key}))]
+      (when-not (:success response)
+        (log/error :msg "Failed to upload graph encrypted AES key" :response response)))))
+
+(defn task--upload-user-rsa-key-pair
+  "Uploads the user's RSA key pair to the server."
+  [token user-id public-key encrypted-private-key]
+  (m/sp
+    (let [{:keys [get-ws-create-task]} (ws-util/gen-get-ws-create-map--memoized (ws-util/get-ws-url token))
+          response (m/? (ws-util/send&recv get-ws-create-task
+                                           {:action                "upload-user-rsa-key-pair"
+                                            :user-id               user-id
+                                            :public-key            public-key
+                                            :encrypted-private-key encrypted-private-key}))]
+      (when-not (:success response)
+        (log/error :msg "Failed to upload user RSA key pair" :response response)))))
+
+(defn task--fetch-user-rsa-key-pair
+  "Fetches the user's RSA key pair, from indexeddb or server."
+  [token user-id password]
+  (m/sp
+    (let [key-pair (c.m/<? (<get-item (user-rsa-key-pair-idb-key user-id)))]
+      (if key-pair
+        (let [private-key (c.m/<? (crypt/<decrypt-private-key password (:encrypted-private-key key-pair)))]
+          {:public-key (:public-key key-pair)
+           :private-key private-key})
+        (let [{:keys [get-ws-create-task]} (ws-util/gen-get-ws-create-map--memoized (ws-util/get-ws-url token))
+              response (m/? (ws-util/send&recv get-ws-create-task
+                                               {:action "fetch-user-rsa-key-pair"
+                                                :user-id user-id}))]
+          (if (:success response)
+            (let [retrieved-key-pair (:body response)]
+              (c.m/<? (<set-item! (user-rsa-key-pair-idb-key user-id) retrieved-key-pair))
+              (let [private-key (c.m/<? (crypt/<decrypt-private-key password (:encrypted-private-key retrieved-key-pair)))]
+                {:public-key (:public-key retrieved-key-pair)
+                 :private-key private-key}))
+            (log/error :msg "Failed to fetch user RSA key pair" :response response)))))))
+
+(defn task--fetch-graph-aes-key
+  "Fetches the AES key for a graph, from indexeddb or server."
+  [token graph-uuid private-key]
+  (m/sp
+    (let [encrypted-aes-key (c.m/<? (<get-item (graph-encrypted-aes-key-idb-key graph-uuid)))]
+      (if encrypted-aes-key
+        (c.m/<? (crypt/<decrypt-aes-key private-key encrypted-aes-key))
+        (let [{:keys [get-ws-create-task]} (ws-util/gen-get-ws-create-map--memoized (ws-util/get-ws-url token))
+              response (m/? (ws-util/send&recv get-ws-create-task
+                                               {:action "fetch-graph-aes-key"
+                                                :graph-uuid graph-uuid}))]
+          (if (:success response)
+            (let [fetched-encrypted-aes-key (:body response)]
+              (c.m/<? (<set-item! (graph-encrypted-aes-key-idb-key graph-uuid) fetched-encrypted-aes-key))
+              (c.m/<? (crypt/<decrypt-aes-key private-key fetched-encrypted-aes-key)))
+            (log/error :msg "Failed to fetch graph AES key" :response response)))))))
+
+(comment
+  (do
+    (defn- array-buffers-equal?
+      [buf1 buf2]
+      (if (not= (.-byteLength buf1) (.-byteLength buf2))
+        false
+        (let [arr1 (js/Uint8Array. buf1)
+              arr2 (js/Uint8Array. buf2)]
+          (= (vec arr1) (vec arr2)))))
+
+    (def user-id "test-user-uuid")
+    (def graph-uuid "test-graph-uuid")
+    (def password "test-password")
+    (def token "test-token")
+
+    ;; Prepare keys
+    (def prepare-keys-task
+      (m/sp
+        (let [rsa-key-pair          (c.m/<? (crypt/<generate-rsa-key-pair))
+              public-key            (:publicKey rsa-key-pair)
+              private-key           (:privateKey rsa-key-pair)
+              encrypted-private-key (c.m/<? (crypt/<encrypt-private-key password private-key))
+              aes-key               (c.m/<? (crypt/<generate-aes-key))
+              encrypted-aes-key     (c.m/<? (crypt/<encrypt-aes-key public-key aes-key))]
+          {:public-key            public-key
+           :private-key           private-key
+           :encrypted-private-key encrypted-private-key
+           :aes-key               aes-key
+           :encrypted-aes-key     encrypted-aes-key})))
+
+    ;; Run test
+    (def cancel
+      (c.m/run-task*
+       (m/sp
+         (prn "--- Start testing crypt.cljs ---")
+         (let [{:keys [public-key private-key encrypted-private-key aes-key encrypted-aes-key]} (m/? prepare-keys-task)]
+
+           (prn "1. Test fetch from local storage")
+           (prn "   Clean local storage first")
+           (c.m/<? (<remove-item! (user-rsa-key-pair-idb-key user-id)))
+           (c.m/<? (<remove-item! (graph-encrypted-aes-key-idb-key graph-uuid)))
+
+           (prn "   Set items to local storage")
+           (c.m/<? (<set-item! (user-rsa-key-pair-idb-key user-id)
+                               (clj->js
+                                {:public-key            public-key
+                                 :encrypted-private-key encrypted-private-key})))
+           (c.m/<? (<set-item! (graph-encrypted-aes-key-idb-key graph-uuid) encrypted-aes-key))
+
+           (prn "   Fetch user rsa key pair from local storage")
+           (let [fetched-key-pair (m/? (task--fetch-user-rsa-key-pair token user-id password))
+                 exported-public-key (c.m/<? (.exportKey crypt/subtle "spki" public-key))
+                 exported-fetched-public-key (c.m/<? (.exportKey crypt/subtle "spki" (:public-key fetched-key-pair)))]
+             (assert (array-buffers-equal? exported-public-key exported-fetched-public-key))
+             (prn "   Fetched user rsa key pair successfully"))
+
+           (prn "   Fetch graph aes key from local storage")
+           (let [fetched-aes-key (m/? (task--fetch-graph-aes-key token graph-uuid private-key))
+                 exported-aes-key (c.m/<? (.exportKey crypt/subtle "raw" aes-key))
+                 exported-fetched-aes-key (c.m/<? (.exportKey crypt/subtle "raw" fetched-aes-key))]
+             (assert (array-buffers-equal? exported-aes-key exported-fetched-aes-key))
+             (prn "   Fetched graph aes key successfully"))
+
+           (comment
+             ;; skip, server api not implemented yet
+             (prn "2. Test fetch from server")
+             (prn "   Clean local storage first")
+             (c.m/<? (<remove-item! (user-rsa-key-pair-idb-key user-id)))
+             (c.m/<? (<remove-item! (graph-encrypted-aes-key-idb-key graph-uuid)))
+
+             (prn "   Upload keys to server")
+             (m/? (task--upload-user-rsa-key-pair token user-id public-key encrypted-private-key))
+             (m/? (task--upload-graph-encrypted-aes-key token graph-uuid encrypted-aes-key))
+             (prn "   Upload complete")
+
+             (prn "   Fetch user rsa key pair from server")
+             (let [fetched-key-pair (m/? (task--fetch-user-rsa-key-pair token user-id password))
+                   exported-public-key (c.m/<? (.exportKey crypt/subtle "spki" public-key))
+                   exported-fetched-public-key (c.m/<? (.exportKey crypt/subtle "spki" (:public-key fetched-key-pair)))]
+               (assert (array-buffers-equal? exported-public-key exported-fetched-public-key))
+               (prn "   Fetched user rsa key pair successfully"))
+
+             (prn "   Fetch graph aes key from server")
+             (let [fetched-aes-key (m/? (task--fetch-graph-aes-key token graph-uuid private-key))
+                   exported-aes-key (c.m/<? (.exportKey crypt/subtle "raw" aes-key))
+                   exported-fetched-aes-key (c.m/<? (.exportKey crypt/subtle "raw" fetched-aes-key))]
+               (assert (array-buffers-equal? exported-aes-key exported-fetched-aes-key))
+               (prn "   Fetched graph aes key successfully")))
+
+           (prn "--- Test finished ---")))))))

+ 2 - 1
src/main/frontend/worker/rtc/encrypt.cljs

@@ -1,5 +1,6 @@
 (ns frontend.worker.rtc.encrypt
-  "rtc e2ee related"
+  "Deprecated ns.
+  rtc e2ee related"
   (:require ["/frontend/idbkv" :as idb-keyval]
             [frontend.common.thread-api :as thread-api :refer [def-thread-api]]
             [logseq.db :as ldb]