p2p-cljs / test / ird / p2p / vectors_test.cljs
1,086 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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
(ns ird.p2p.vectors-test
  "The interop gate with the Swift SDK: every golden vector of
  Tests/IrdP2PCryptoTests/Vectors (copied verbatim) must reproduce byte for
  byte โ€” derivations, sealer IVs, JSON/binary/at-rest envelopes โ€” and every
  tamper case must open to nil, never throw. Identity signatures are
  verified rather than compared (Ed25519 may be randomized)."
  (:require [cljs.test :refer [deftest is testing]]
            [cljs.core.async :refer [<!]]
            [ird.p2p.crypto :as crypto]
            [ird.p2p.envelope :as env]
            [ird.p2p.identity :as identity]
            [ird.p2p.impl.bytes :as bytes]
            [ird.p2p.impl.json :as json]
            [ird.p2p.test-util :as tu :refer [deftest-async]]))

(def peer-a "00000000000000000000000000000001")

(deftest base64-alphabets
  (doseq [{:keys [bytes b64 b64url]} (:b64 (tu/vector-file "room-crypto"))]
    (is (= b64 (bytes/bytes->b64 bytes)))
    (is (= b64url (bytes/b64->b64url b64)))
    (is (= b64 (bytes/b64url->b64 b64url)))
    (is (= bytes (bytes/b64->bytes b64)))))

(deftest too-short-secret
  (let [{:keys [secret error]} (:tooShort (tu/vector-file "room-crypto"))
        e (try (crypto/check-secret secret) nil (catch :default e e))]
    (is (some? e))
    (is (= error (ex-message e)))))

(deftest-async derivations-match-reference
  {:timeout-ms 30000}
  (doseq [d (:derivations (tu/vector-file "room-crypto"))]
    (let [{:keys [secret appSalt]} d
          rc (<! (crypto/room-crypto {:secret secret :app-salt appSalt :peer-id peer-a}))]
      (is (= (:roomId d) (:room-id rc)) (str "roomId " appSalt))
      (is (= 43 (count (:room-id rc))))
      (is (= (:roomId d) (<! (crypto/room-id secret appSalt))))
      (is (= (:roomTopic d) (<! (crypto/room-topic secret appSalt))))
      (is (= (:dbName d) (<! (crypto/db-name secret appSalt))))
      (is (= (:mailboxRoomId d) (<! (crypto/mailbox-room-id secret appSalt))))
      (is (= 32 (bytes/b64url-byte-length (<! (crypto/sig-key rc peer-a)))))
      (is (= 32 (bytes/b64url-byte-length (<! (crypto/msg-key rc peer-a))))))))

(deftest sealer-ivs-are-epoch-then-big-endian-counter
  (let [ivs (:sealerIvs (tu/vector-file "room-crypto"))
        s (crypto/make-sealer (subs (first ivs) 0 8))]
    (is (= ivs
           (second (reduce (fn [[s acc] _] (let [[s' iv] (crypto/next-iv s)] [s' (conj acc (bytes/b64->hex iv))]))
                           [s []] ivs))))))

(deftest-async envelopes-are-byte-exact-and-open
  (let [e (:envelopes (tu/vector-file "room-crypto"))
        {:keys [secret appSalt from to epoch]} e
        sender (<! (crypto/room-crypto {:secret secret :app-salt appSalt :peer-id from :epoch epoch}))
        recipient (<! (crypto/room-crypto {:secret secret :app-salt appSalt :peer-id to}))
        {:keys [signalHello signalOffer msg binary]} e
        sealed-hello (<! (env/seal-signal sender (:plaintext signalHello) to))
        sealed-offer (<! (env/seal-signal sender (:plaintext signalOffer) to))
        sealed-msg (<! (env/seal-message sender (:plaintext msg)))
        sealed-bin (<! (env/seal-message-binary sender (bytes/bytes->b64 (:plaintext binary))))]
    (is (= (json/stringify (:envelope signalHello)) (env/envelope->json sealed-hello)))
    (is (= (:envelope signalHello) sealed-hello))
    (is (= (json/stringify (:envelope signalOffer)) (env/envelope->json sealed-offer)))
    (is (= (json/stringify (:envelope msg)) (env/envelope->json sealed-msg)))
    (is (= (:sealedHex binary) (bytes/b64->hex sealed-bin)))
    (is (= (:plaintext signalHello) (<! (env/open-signal recipient (:envelope signalHello) from))))
    (is (= (:plaintext signalOffer) (<! (env/open-signal recipient (:envelope signalOffer) from))))
    (is (= (:plaintext msg) (<! (env/open-message recipient (:envelope msg) from))))
    (is (= (:plaintext binary) (bytes/b64->bytes (<! (env/open-message-binary recipient (bytes/hex->b64 (:sealedHex binary)) from)))))
    ;; opened plaintext keeps the sender's key order
    (is (= (json/stringify (:plaintext signalHello))
           (json/stringify (<! (env/open-signal recipient (:envelope signalHello) from)))))
    ;; the envelope wire text has exactly the three keys in order
    (is (re-matches #"\{\"v\":1,\"iv\":\"[^\"]+\",\"ct\":\"[^\"]+\"\}" (env/envelope->json sealed-hello)))))

(deftest-async at-rest-matches-reference
  (let [v (tu/vector-file "room-crypto")
        e (:envelopes v)
        r (:atRest v)
        rc (<! (crypto/room-crypto {:secret (:secret e) :app-salt (:appSalt e) :peer-id (:from e)}))
        mbx (:mbx r)
        pt (bytes/utf8->b64 (json/stringify (:plaintext mbx)))]
    (is (= (:sealedHex mbx) (bytes/b64->hex (<! (env/mailbox-seal rc pt (bytes/hex->b64 (:ivHex mbx)))))))
    (is (= pt (<! (env/mailbox-open rc (bytes/hex->b64 (:sealedHex mbx))))))
    (let [entry (:entry r)]
      (is (= (:sealedHex entry)
             (bytes/b64->hex (<! (env/at-rest-seal rc "log-entry" "entry" (bytes/bytes->b64 (:plaintextBytes entry))
                                                   (bytes/hex->b64 (:ivHex entry))))))))
    ;; random-IV path round-trips and never repeats an IV
    (let [s1 (<! (env/mailbox-seal rc pt)) s2 (<! (env/mailbox-seal rc pt))]
      (is (not= s1 s2))
      (is (= pt (<! (env/mailbox-open rc s1))))
      (is (= pt (<! (env/mailbox-open rc s2)))))))

(deftest-async tamper-table-all-open-to-nil
  (let [v (tu/vector-file "room-crypto")
        e (:envelopes v)
        cases (:tamper v)]
    (is (= 14 (count cases)))
    (doseq [[name c] cases]
      (let [opener (or (:by c) (:to e))
            from (or (:from c) (:from e))
            rc (<! (crypto/room-crypto {:secret (or (:secret c) (:secret e))
                                        :app-salt (or (:appSalt c) (:appSalt e))
                                        :peer-id opener}))
            result (case (:open c)
                     "signal" (<! (env/open-signal rc (:env c) from))
                     "msg" (<! (env/open-message rc (:env c) from))
                     "binary" (<! (env/open-message-binary rc (bytes/hex->b64 (:sealedHex c)) from))
                     "atRest" (if (= "entry" (:aadSuffix c))
                                (<! (env/at-rest-open rc "log-entry" "entry" (bytes/hex->b64 (:sealedHex c))))
                                (<! (env/mailbox-open rc (bytes/hex->b64 (:sealedHex c)))))
                     ::unknown)]
        (is (nil? result) (str "tamper case " (clojure.core/name name) " must drop"))))))

(deftest-async fresh-room-crypto-uses-random-epochs
  (let [secret (crypto/random-secret) p (crypto/random-peer-id)
        x (<! (crypto/room-crypto {:secret secret :app-salt "t" :peer-id p}))
        y (<! (crypto/room-crypto {:secret secret :app-salt "t" :peer-id p}))]
    (is (not= (:iv (<! (env/seal-message x {:kind "ping"}))) (:iv (<! (env/seal-message y {:kind "ping"})))))))

(deftest key-cache-is-bounded
  (let [rc {:secret (crypto/random-secret) :app-salt "t" :keys (atom {})}]
    (doseq [i (range 600)] (swap! (:keys rc) (fn [m] (assoc (if (>= (count m) crypto/key-cache-limit) {} m) (str "k" i) "x"))))
    (is (<= (count @(:keys rc)) crypto/key-cache-limit))))

;; ---- identity --------------------------------------------------------------------------

(deftest-async identity-cases-match-reference
  {:timeout-ms 30000}
  (doseq [c (:cases (tu/vector-file "identity"))]
    (let [id (<! (identity/from-seed (:seed c)))]
      (is (not (tu/error? id)) (:name c))
      (is (= (:seed c) (:seed id)))
      (is (= (:publicKeyB64url c) (:public-key id)) (:name c))
      (is (= (get-in c [:fingerprint :emoji]) (get-in id [:fingerprint :emoji])) (:name c))
      (is (= (get-in c [:fingerprint :hex]) (get-in id [:fingerprint :hex])))
      (doseq [a (:assertions c)]
        (let [{:keys [appSalt roomId peerId sigB64]} a]
          (is (= (:fingerprint id) (<! (identity/verify-assertion appSalt roomId peerId (:public-key id) sigB64))) (str (:name c) " " appSalt))
          (let [ours (<! (identity/assert-session id appSalt roomId peerId))]
            (is (= 64 (bytes/byte-length ours)))
            (is (some? (<! (identity/verify-assertion appSalt roomId peerId (:public-key id) ours)))))))
      (doseq [r (:rawSignatures c)]
        (let [data (bytes/utf8->b64 (:dataUtf8 r))]
          (is (true? (<! (identity/verify-raw (:public-key id) (:sigB64url r) data))))
          (is (true? (<! (identity/verify-raw (:public-key id) (<! (identity/sign-raw id data)) data)))))))))

(deftest-async verify-assertion-table
  (doseq [c (:verifyAssertion (tu/vector-file "identity"))]
    (let [[salt room peer pub sig] (:args c)
          r (<! (identity/verify-assertion salt room peer pub sig))]
      (is (= (:ok c) (some? r)) (:desc c)))))

(deftest-async assert-round-trips-through-public-api
  (let [id (<! (identity/generate))
        room (<! (crypto/room-id (crypto/random-secret) "x/v1"))
        p (crypto/random-peer-id)
        sig (<! (identity/assert-session id "x/v1" room p))]
    (is (= 64 (bytes/byte-length sig)))
    (is (= (:fingerprint id) (<! (identity/verify-assertion "x/v1" room p (:public-key id) sig))))
    (is (nil? (<! (identity/verify-assertion "y/v1" room p (:public-key id) sig))))))

(deftest-async seed-validation
  (is (tu/error? (<! (identity/from-seed "AAAA"))))
  (is (tu/error? (<! (identity/from-seed "***"))))
  (let [g (<! (identity/generate {:name "Ana"}))]
    (is (= 43 (count (:seed g))))
    (is (= "Ana" (:name g)))
    (is (= (:public-key g) (:public-key (<! (identity/from-seed (:seed g))))))))

(deftest emoji-table-is-intact
  (is (= 64 (count identity/emoji-table)))
  (is (= 64 (count (set identity/emoji-table))))
  ;; the variation-selector entries, copied from PROTOCOL.md bytes
  (is (= "โ„๏ธ" (nth identity/emoji-table 22)))
  (is (= "๐Ÿ—๏ธ" (nth identity/emoji-table 50)))
  (is (= "๐Ÿ–๏ธ" (nth identity/emoji-table 56)))
  (is (= "โœ‚๏ธ" (nth identity/emoji-table 58)))
  (is (= "๐Ÿ›ก๏ธ" (nth identity/emoji-table 61)))
  (is (= "โš™๏ธ" (nth identity/emoji-table 62)))
  ;; the table in PROTOCOL.md is the table in the code, byte for byte
  (let [proto (tu/read-text "PROTOCOL.md")
        block (second (re-find #"(?s)```\n(๐Ÿข[^`]*)```" proto))
        entries (vec (remove clojure.string/blank? (clojure.string/split block #"[ \n]+")))]
    (is (= identity/emoji-table entries))))

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