crypt.cljs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. (ns frontend.common.crypt
  2. (:require [logseq.db :as ldb]
  3. [promesa.core :as p]))
  4. (defonce subtle (.. js/crypto -subtle))
  5. (defn <generate-rsa-key-pair
  6. "Generates a new RSA public/private key pair.
  7. Return
  8. {:publicKey #object [CryptoKey [object CryptoKey]],
  9. :privateKey #object [CryptoKey [object CryptoKey]]}"
  10. []
  11. (p/let [r (.generateKey subtle
  12. #js {:name "RSA-OAEP"
  13. :modulusLength 4096
  14. :publicExponent (js/Uint8Array. [1 0 1])
  15. :hash "SHA-256"}
  16. true
  17. #js ["encrypt" "decrypt"])]
  18. (js->clj r :keywordize-keys true)))
  19. (defn <generate-aes-key
  20. "Generates a new AES-GCM-256 key."
  21. []
  22. (.generateKey subtle
  23. #js {:name "AES-GCM"
  24. :length 256}
  25. true
  26. #js ["encrypt" "decrypt"]))
  27. (defn <encrypt-private-key
  28. "Encrypts a private key with a password."
  29. [password private-key]
  30. (p/let [salt (js/crypto.getRandomValues (js/Uint8Array. 16))
  31. iv (js/crypto.getRandomValues (js/Uint8Array. 12))
  32. password-key (.importKey subtle "raw"
  33. (.encode (js/TextEncoder.) password)
  34. "PBKDF2"
  35. false
  36. #js ["deriveKey"])
  37. derived-key (.deriveKey subtle
  38. #js {:name "PBKDF2"
  39. :salt salt
  40. :iterations 100000
  41. :hash "SHA-256"}
  42. password-key
  43. #js {:name "AES-GCM" :length 256}
  44. true
  45. #js ["encrypt" "decrypt"])
  46. exported-private-key (.exportKey subtle "pkcs8" private-key)
  47. encrypted-private-key (.encrypt subtle
  48. #js {:name "AES-GCM" :iv iv}
  49. derived-key
  50. exported-private-key)]
  51. [salt iv (js/Uint8Array. encrypted-private-key)]))
  52. (defn <decrypt-private-key
  53. "Decrypts a private key with a password."
  54. [password encrypted-key-data]
  55. (p/let [[salt-data iv-data encrypted-private-key-data] encrypted-key-data
  56. salt (js/Uint8Array. salt-data)
  57. iv (js/Uint8Array. iv-data)
  58. encrypted-private-key (js/Uint8Array. encrypted-private-key-data)
  59. password-key (.importKey subtle "raw"
  60. (.encode (js/TextEncoder.) password)
  61. "PBKDF2"
  62. false
  63. #js ["deriveKey"])
  64. derived-key (.deriveKey subtle
  65. #js {:name "PBKDF2"
  66. :salt salt
  67. :iterations 100000
  68. :hash "SHA-256"}
  69. password-key
  70. #js {:name "AES-GCM" :length 256}
  71. true
  72. #js ["encrypt" "decrypt"])
  73. decrypted-private-key-data (.decrypt subtle
  74. #js {:name "AES-GCM" :iv iv}
  75. derived-key
  76. encrypted-private-key)
  77. private-key (.importKey subtle "pkcs8"
  78. decrypted-private-key-data
  79. #js {:name "RSA-OAEP" :hash "SHA-256"}
  80. true
  81. #js ["decrypt"])]
  82. private-key))
  83. (defn <encrypt-aes-key
  84. "Encrypts an AES key with a public key."
  85. [public-key aes-key]
  86. (p/let [exported-aes-key (.exportKey subtle "raw" aes-key)
  87. encrypted-aes-key (.encrypt subtle
  88. #js {:name "RSA-OAEP"}
  89. public-key
  90. exported-aes-key)]
  91. (js/Uint8Array. encrypted-aes-key)))
  92. (defn <decrypt-aes-key
  93. "Decrypts an AES key with a private key."
  94. [private-key encrypted-aes-key-data]
  95. (p/let [encrypted-aes-key (js/Uint8Array. encrypted-aes-key-data)
  96. decrypted-key-data (.decrypt subtle
  97. #js {:name "RSA-OAEP"}
  98. private-key
  99. encrypted-aes-key)]
  100. (.importKey subtle
  101. "raw"
  102. decrypted-key-data
  103. "AES-GCM"
  104. true
  105. #js ["encrypt" "decrypt"])))
  106. (defn <encrypt-text
  107. "Encrypts text with an AES key."
  108. [aes-key text]
  109. (p/let [iv (js/crypto.getRandomValues (js/Uint8Array. 12))
  110. encoded-text (.encode (js/TextEncoder.) text)
  111. encrypted-data (.encrypt subtle
  112. #js {:name "AES-GCM" :iv iv}
  113. aes-key
  114. encoded-text)]
  115. [iv (js/Uint8Array. encrypted-data)]))
  116. (defn <decrypt-text
  117. "Decrypts text with an AES key."
  118. [aes-key encrypted-text-data]
  119. (p/let [[iv-data encrypted-data-from-db] encrypted-text-data
  120. iv (js/Uint8Array. iv-data)
  121. encrypted-data (js/Uint8Array. encrypted-data-from-db)
  122. decrypted-data (.decrypt subtle
  123. #js {:name "AES-GCM" :iv iv}
  124. aes-key
  125. encrypted-data)
  126. decoded-text (.decode (js/TextDecoder.) decrypted-data)]
  127. decoded-text))
  128. (defn <decrypt-text-if-encrypted
  129. "return nil if not a encrypted-package"
  130. [aes-key maybe-encrypted-package]
  131. (when (and (vector? maybe-encrypted-package)
  132. (<= 2 (count maybe-encrypted-package)))
  133. (<decrypt-text aes-key maybe-encrypted-package)))
  134. (defn <encrypt-map
  135. [aes-key encrypt-attr-set m]
  136. (assert (map? m))
  137. (reduce
  138. (fn [map-p encrypt-attr]
  139. (p/let [m map-p]
  140. (if-let [v (get m encrypt-attr)]
  141. (p/let [v' (p/chain (<encrypt-text aes-key v) ldb/write-transit-str)]
  142. (assoc m encrypt-attr v'))
  143. m)))
  144. (p/promise m) encrypt-attr-set))
  145. (defn <encrypt-av-coll
  146. "see also `rtc-schema/av-schema`"
  147. [aes-key encrypt-attr-set av-coll]
  148. (p/all
  149. (mapv
  150. (fn [[a v & others]]
  151. (p/let [v' (if (and (contains? encrypt-attr-set a)
  152. (string? v))
  153. (p/chain (<encrypt-text aes-key v) ldb/write-transit-str)
  154. v)]
  155. (apply conj [a v'] others)))
  156. av-coll)))
  157. (defn <decrypt-map
  158. [aes-key encrypt-attr-set m]
  159. (assert (map? m))
  160. (reduce
  161. (fn [map-p encrypt-attr]
  162. (p/let [m map-p]
  163. (if-let [v (get m encrypt-attr)]
  164. (if (string? v)
  165. (p/let [v' (<decrypt-text-if-encrypted aes-key (ldb/read-transit-str v))]
  166. (if v'
  167. (assoc m encrypt-attr v')
  168. m))
  169. m)
  170. m)))
  171. (p/promise m) encrypt-attr-set))
  172. (comment
  173. (let [array-buffers-equal?
  174. (fn [^js/ArrayBuffer buf1 ^js/ArrayBuffer buf2]
  175. (if (not= (.-byteLength buf1) (.-byteLength buf2))
  176. false
  177. (let [arr1 (js/Uint8Array. buf1)
  178. arr2 (js/Uint8Array. buf2)]
  179. (= (vec arr1) (vec arr2)))))]
  180. (p/let [rsa-key-pair (<generate-rsa-key-pair)
  181. aes-key (<generate-aes-key)
  182. public-key (:publicKey rsa-key-pair)
  183. private-key (:privateKey rsa-key-pair)
  184. encrypted-aes-key (<encrypt-aes-key public-key aes-key)
  185. decrypted-aes-key (<decrypt-aes-key private-key encrypted-aes-key)
  186. password "my-secret-password"
  187. encrypted-private-key (<encrypt-private-key password private-key)
  188. decrypted-private-key (<decrypt-private-key password encrypted-private-key)
  189. ;; Export keys to compare their raw values
  190. exported-original-aes (.exportKey subtle "raw" aes-key)
  191. exported-decrypted-aes (.exportKey subtle "raw" decrypted-aes-key)
  192. exported-original-private (.exportKey subtle "pkcs8" private-key)
  193. exported-decrypted-private (.exportKey subtle "pkcs8" decrypted-private-key)
  194. ;; Test text encryption
  195. original-text "This is a secret message."
  196. encrypted-text-data (<encrypt-text aes-key original-text)
  197. decrypted-text (<decrypt-text aes-key encrypted-text-data)]
  198. (js/console.log "Original AES key:" aes-key)
  199. (js/console.log "Decrypted AES key:" decrypted-aes-key)
  200. (js/console.log "Original private key:" private-key)
  201. (js/console.log "Decrypted private key:" decrypted-private-key)
  202. (let [aes-match? (array-buffers-equal? exported-original-aes exported-decrypted-aes)
  203. private-key-match? (array-buffers-equal? exported-original-private exported-decrypted-private)]
  204. (js/console.log "AES keys match:" aes-match?)
  205. (js/console.log "Private keys match:" private-key-match?))
  206. (js/console.log "Original text:" original-text)
  207. (js/console.log "Decrypted text:" decrypted-text)
  208. (js/console.log "Texts match:" (= original-text decrypted-text)))))