p2p-cljs / src / ird / p2p / impl / webcrypto.cljs
664 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
(ns ird.p2p.impl.webcrypto
  "The cryptographic primitives of PROTOCOL.md on Node's WebCrypto
  (`crypto.subtle`): HKDF-SHA256, AES-256-GCM, Ed25519 and SHA-256, plus
  secure randomness. Every function takes and returns strings — keys and
  identifiers in URL-safe base64, IVs/ciphertexts in standard base64 —
  and the asynchronous ones deliver on a promise-chan (an error value only
  for a local programming error such as a malformed key; a failed
  authentication delivers nil, which callers treat as \"drop\").

  Internal interop namespace: the only place AES/HKDF/Ed25519 are called."
  (:require [cljs.core.async :as async :refer [go <!]]
            [ird.p2p.impl.bytes :as bytes]
            [ird.p2p.impl.node :as node]))

(def ^:private webcrypto
  (delay (or (and (exists? js/globalThis) (.-crypto js/globalThis))
             (.-webcrypto (js/require "node:crypto")))))

(defn- subtle [] (.-subtle @webcrypto))

(defn- pc
  "Promise → channel that delivers the resolution, or nil on rejection."
  [p]
  (let [c (async/promise-chan)]
    (.then p
           (fn [v] (if (nil? v) (async/close! c) (async/put! c v)))
           (fn [_] (async/close! c)))
    c))

(defn- u8 [b64] (bytes/b64->u8 b64))
(defn- utf8 [s] (.encode (js/TextEncoder.) s))

;; ---- HKDF -----------------------------------------------------------------

(defn hkdf
  "HKDF-SHA256 of IKM (URL-safe base64 bytes) with SALT and INFO (strings,
  taken as UTF-8), 32 output bytes as URL-safe base64. Channel; nil when
  the ikm is malformed."
  [ikm-b64url salt info]
  (go
    (if-let [ikm (bytes/b64url->b64 ikm-b64url)]
      (let [key (<! (pc (.importKey (subtle) "raw" (u8 ikm) "HKDF" false #js ["deriveBits"])))]
        (when key
          (when-let [bits (<! (pc (.deriveBits (subtle)
                                               #js {:name "HKDF" :hash "SHA-256" :salt (utf8 salt) :info (utf8 info)}
                                               key 256)))]
            (bytes/b64->b64url (bytes/buffer->b64 bits)))))
      nil)))

;; ---- AES-256-GCM ------------------------------------------------------------

(def ^:private key-cache (atom {}))
(def ^:private key-cache-limit 512)

(defn- aes-key
  "Import KEY-B64URL (32 bytes) as an AES-GCM CryptoKey, cached (bounded)."
  [key-b64url]
  (go
    (or (get @key-cache key-b64url)
        (when-let [raw (bytes/b64url->b64 key-b64url)]
          (when-let [k (<! (pc (.importKey (subtle) "raw" (u8 raw) #js {:name "AES-GCM"} false #js ["encrypt" "decrypt"])))]
            (swap! key-cache (fn [m] (let [m (if (>= (count m) key-cache-limit) {} m)] (assoc m key-b64url k))))
            k)))))

(defn aes-gcm-seal
  "AES-256-GCM: ciphertext‖tag (standard base64) of PT-B64 under KEY with
  IV-B64 (12 bytes) and AAD (string, UTF-8). Channel; nil on a local error."
  [key-b64url iv-b64 aad pt-b64]
  (go
    (when-let [k (<! (aes-key key-b64url))]
      (when-let [ct (<! (pc (.encrypt (subtle) #js {:name "AES-GCM" :iv (u8 iv-b64) :additionalData (utf8 aad)} k (u8 pt-b64))))]
        (bytes/buffer->b64 ct)))))

(defn aes-gcm-open
  "AES-256-GCM decrypt: plaintext (standard base64) of CT-B64 (ciphertext‖tag)
  under KEY, IV-B64 and AAD. Channel; nil on any failure (drop)."
  [key-b64url iv-b64 aad ct-b64]
  (go
    (when (and (bytes/valid-b64? iv-b64) (bytes/valid-b64? ct-b64)
               (= 12 (bytes/byte-length iv-b64)) (>= (bytes/byte-length ct-b64) 16))
      (when-let [k (<! (aes-key key-b64url))]
        (when-let [pt (<! (pc (.decrypt (subtle) #js {:name "AES-GCM" :iv (u8 iv-b64) :additionalData (utf8 aad)} k (u8 ct-b64))))]
          (bytes/buffer->b64 pt))))))

;; ---- SHA-256 ----------------------------------------------------------------

(defn sha256-hex
  "SHA-256 of B64 (standard base64 bytes) as lowercase hex. Synchronous
  (node:crypto's hash), so fingerprints need no channel."
  [b64]
  (let [h (.createHash (js/require "node:crypto") "sha256")]
    (.update h (bytes/b64->buffer b64))
    (.digest h "hex")))

;; ---- Ed25519 ----------------------------------------------------------------

(def ^:private pkcs8-prefix-b64 (bytes/hex->b64 "302e020100300506032b657004220420"))

(defn- private-key
  "Import a 32-byte seed (URL-safe base64) as an Ed25519 signing key."
  [seed-b64url]
  (go
    (when-let [seed (bytes/b64url->b64 seed-b64url)]
      (when (= 32 (bytes/byte-length seed))
        (<! (pc (.importKey (subtle) "pkcs8" (u8 (bytes/concat-b64 pkcs8-prefix-b64 seed))
                            #js {:name "Ed25519"} true #js ["sign"])))))))

(defn ed25519-public-key
  "The public key (URL-safe base64, 32 bytes) of SEED-B64URL. Channel; nil
  when the seed is malformed."
  [seed-b64url]
  (go
    (when-let [k (<! (private-key seed-b64url))]
      (when-let [jwk (<! (pc (.exportKey (subtle) "jwk" k)))]
        (.-x jwk)))))

(defn ed25519-sign
  "Ed25519 signature (standard base64, 64 bytes) of DATA-B64 under the seed.
  Channel; nil when the seed is malformed."
  [seed-b64url data-b64]
  (go
    (when-let [k (<! (private-key seed-b64url))]
      (when-let [sig (<! (pc (.sign (subtle) #js {:name "Ed25519"} k (u8 data-b64))))]
        (bytes/buffer->b64 sig)))))

(defn ed25519-verify
  "True when SIG-B64 (standard base64) is a valid Ed25519 signature of
  DATA-B64 under PUB-B64URL. Channel; false on any malformed input."
  [pub-b64url sig-b64 data-b64]
  (go
    (boolean
     (when-let [pub (bytes/b64url->b64 pub-b64url)]
       (when (and (= 32 (bytes/byte-length pub)) (bytes/valid-b64? sig-b64) (= 64 (bytes/byte-length sig-b64)))
         (when-let [k (<! (pc (.importKey (subtle) "raw" (u8 pub) #js {:name "Ed25519"} false #js ["verify"])))]
           (<! (pc (.verify (subtle) #js {:name "Ed25519"} k (u8 sig-b64) (u8 data-b64))))))))))

(defn random-b64
  "N random bytes as standard base64."
  [n]
  (bytes/random-b64 n))

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-cljs.git