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
188
189
190
191
192
193
194
195
196
197
198 | (ns ird.p2p.envelope-test
"Unit tests of the codec layer: envelope strictness, inner messages,
sealer counters and epochs, the replay window, chunking bounds and the
reassembly limits."
(:require [cljs.test :refer [deftest is testing]]
[cljs.core.async :refer [<!]]
[ird.p2p.crypto :as crypto]
[ird.p2p.envelope :as env]
[ird.p2p.impl.bytes :as bytes]
[ird.p2p.impl.json :as json]
[ird.p2p.test-util :as tu :refer [deftest-async]]))
(deftest json-stringify-matches-javascript
(is (= "{\"v\":1,\"iv\":\"a\",\"ct\":\"b\"}" (json/stringify {:v 1 :iv "a" :ct "b"})))
(is (= "{\"kind\":\"msg\",\"payload\":{\"t\":\"chat\",\"text\":\"salut — ăîșț\",\"n\":[1,2.5,null,true]}}"
(json/stringify {:kind "msg" :payload {:t "chat" :text "salut — ăîșț" :n [1 2.5 nil true]}})))
(is (= "\"x\\\"y\\n\"" (json/stringify "x\"y\n")))
(is (= {:a 1 :b [1 2]} (json/parse "{\"a\":1,\"b\":[1,2]}")))
(is (nil? (json/parse "{nope")))
(is (nil? (json/parse nil))))
(deftest envelope-strictness
(is (= {:v 1 :iv "AAAA" :ct "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"} (env/parse-envelope {:v 1 :iv "AAAA" :ct "AAAAAAAAAAAAAAAAAAAAAAAAAAAA" :extra 1})))
(is (nil? (env/parse-envelope {:v 2 :iv "AAAA" :ct "AAAA"})))
(is (nil? (env/parse-envelope {:v "1" :iv "AAAA" :ct "AAAA"})))
(is (nil? (env/parse-envelope {:v 1 :iv 5 :ct "AAAA"})))
(is (nil? (env/parse-envelope {:v 1 :iv "AA*A" :ct "AAAA"})))
(is (nil? (env/parse-envelope "junk")))
(is (nil? (env/json->envelope "not json")))
(is (= {:v 1 :iv "AAAA" :ct "AAAA"} (env/json->envelope "{\"v\":1,\"iv\":\"AAAA\",\"ct\":\"AAAA\"}")))
(is (= "{\"v\":1,\"iv\":\"AAAA\",\"ct\":\"AAAA\"}" (env/envelope->json {:v 1 :iv "AAAA" :ct "AAAA"}))))
(deftest inner-messages
(is (= {:kind "hello"} (env/hello nil)))
(is (= "{\"kind\":\"hello\",\"id\":{\"pub\":\"p\",\"sig\":\"s\",\"name\":\"ana\"}}"
(json/stringify (env/hello {:pub "p" :sig "s" :name "ana"}))))
(is (= "{\"kind\":\"hello\",\"id\":{\"pub\":\"p\",\"sig\":\"s\",\"name\":\"ana\"}}"
(json/stringify (env/hello {:pub "p" :sig "s" :name "ana" :hue 200 :glyph "🐢"})))
"hue/glyph never reach the wire, even if handed to the builder")
(is (= "{\"kind\":\"hello\",\"id\":{\"pub\":\"p\",\"sig\":\"s\"}}"
(json/stringify (env/hello {:pub "p" :sig "s" :name nil})))
"a nil name is omitted, not null")
(is (= "{\"kind\":\"ice\",\"candidate\":null,\"sdpMid\":null,\"sdpMLineIndex\":null}" (json/stringify (env/ice nil nil nil))))
(is (= "{\"kind\":\"chunk\",\"id\":\"x\",\"i\":0,\"n\":2,\"enc\":\"bin\",\"d\":\"AQ==\"}" (json/stringify (env/chunk-message "x" 0 2 "bin" "AQ=="))))
(is (= {:kind :hello :id {:pub "p" :sig "s" :name nil}} (env/parse-inner {:kind "hello" :id {:pub "p" :sig "s"}})))
(is (= {:kind :hello :id {:pub "p" :sig "s" :name "ana"}} (env/parse-inner {:kind "hello" :id {:pub "p" :sig "s" :name "ana" :hue 200 :glyph "🐢"}}))
"a Swift-style block's hue/glyph are dropped silently")
(is (= {:kind :hello :id nil} (env/parse-inner {:kind "hello" :id {:pub "p"}})))
(is (= {:kind :offer :sdp "v=0"} (env/parse-inner {:kind "offer" :sdp "v=0" :extra true})))
(is (nil? (env/parse-inner {:kind "offer"})))
(is (= {:kind :ice :candidate "c" :sdp-mid "0" :sdp-mline-index 0} (env/parse-inner {:kind "ice" :candidate "c" :sdpMid "0" :sdpMLineIndex 0})))
(is (= {:kind :ice :candidate nil :sdp-mid nil :sdp-mline-index nil} (env/parse-inner {:kind "ice" :candidate nil})))
(is (= {:kind :msg :payload {:a 1}} (env/parse-inner {:kind "msg" :payload {:a 1}})))
(is (= {:kind :msg :payload nil} (env/parse-inner {:kind "msg" :payload nil})))
(is (nil? (env/parse-inner {:kind "msg"})))
(is (= {:kind :bin :data "AQID"} (env/parse-inner {:kind "bin" :d "AQID"})))
(is (nil? (env/parse-inner {:kind "bin" :d "***"})))
(is (= {:kind :chunk :id "x" :index 1 :count 2 :enc :json :data "AQ=="} (env/parse-inner {:kind "chunk" :id "x" :i 1 :n 2 :enc "json" :d "AQ=="})))
(is (nil? (env/parse-inner {:kind "chunk" :id "x" :i 2 :n 2 :enc "json" :d "AQ=="})))
(is (nil? (env/parse-inner {:kind "chunk" :id "x" :i 0 :n 2 :enc "gzip" :d "AQ=="})))
(is (= {:kind :unknown :name "future"} (env/parse-inner {:kind "future" :x 1})))
(is (= {:kind :ping} (env/parse-inner {:kind "ping"})))
(is (nil? (env/parse-inner {:nokind 1})))
(is (nil? (env/parse-inner "x"))))
(deftest sealer-counter-and-epoch
(let [s (crypto/make-sealer "0a0b0c0d")
[s1 iv1] (crypto/next-iv s)
[s2 iv2] (crypto/next-iv s1)]
(is (= "0a0b0c0d0000000000000000" (bytes/b64->hex iv1)))
(is (= "0a0b0c0d0000000000000001" (bytes/b64->hex iv2)))
(is (= 2 (:counter s2))))
(let [a (crypto/make-sealer) b (crypto/make-sealer)]
(is (= 8 (count (:epoch a))))
(is (not= (:epoch a) (:epoch b)) "epochs are random"))
(is (thrown? :default (crypto/make-sealer "abc")))
(let [at (atom (crypto/make-sealer "ffffffff"))]
(is (= "ffffffff0000000000000000" (bytes/b64->hex (crypto/take-iv! at))))
(is (= "ffffffff0000000000000001" (bytes/b64->hex (crypto/take-iv! at))))))
(defn- iv [epoch counter] (bytes/hex->b64 (str epoch (bytes/int->hex counter 16))))
(deftest replay-window
(let [accept (fn [w i] (crypto/replay-accept w i))
w (crypto/make-replay-window)
[w ok1] (accept w (iv "01010101" 0))
[w ok2] (accept w (iv "01010101" 1))
[w dup] (accept w (iv "01010101" 1))
[w ok3] (accept w (iv "01010101" 100))
[w in-window] (accept w (iv "01010101" 50))
[w again] (accept w (iv "01010101" 50))
[w edge] (accept w (iv "01010101" 37))
[w too-old] (accept w (iv "01010101" 36))
[w far-old] (accept w (iv "01010101" 2))]
(is (and ok1 ok2 ok3 in-window edge))
(is (not dup) "a replay is rejected")
(is (not again) "a replay inside the window is rejected")
(is (not too-old) "64 behind is rejected")
(is (not far-old) "far behind is rejected")
;; out-of-order within the window passes
(let [[w a] (accept w (iv "01010101" 99)) [w b] (accept w (iv "01010101" 98))]
(is (and a b))
;; a new epoch retires the old one
(let [[w e2] (accept w (iv "02020202" 0)) [w back] (accept w (iv "01010101" 500))]
(is e2)
(is (not back) "a retired epoch never comes back")
(is (= ["01010101"] (:retired w)))
(let [[w' ok] (accept w (iv "02020202" 1))]
(is ok)
(is (= 1 (:highest w')))))))
(testing "malformed IVs are rejected"
(is (false? (second (crypto/replay-accept (crypto/make-replay-window) "AAAA"))))
(is (false? (second (crypto/replay-accept (crypto/make-replay-window) 5)))))
(testing "retired epochs are bounded"
(let [w (reduce (fn [w i] (first (crypto/replay-accept w (iv (bytes/int->hex i 8) 0)))) (crypto/make-replay-window) (range 20))]
(is (= crypto/retired-epochs-kept (count (:retired w)))))))
(deftest chunking-bounds
(is (= 8192 env/data-channel-slice-size))
(is (= 32768 (env/relay-slice-size 65536)))
(is (= (quot (* 100 (- 4096 512)) 185) (env/relay-slice-size 4096)))
(is (= 1937 (env/relay-slice-size 4096)))
(is (= 1024 (env/relay-slice-size 600)) "never below 1 KiB")
(is (= 32768 (env/relay-slice-size 1000000)) "capped at 32 KiB")
(let [data (bytes/bytes->b64 (range 200))
chunks (env/split data "bin" 64)]
(is (= 4 (count chunks)))
(is (= [0 1 2 3] (map :i chunks)))
(is (every? #(= 4 (:n %)) chunks))
(is (= 1 (count (set (map :id chunks)))))
(is (= 11 (count (:id (first chunks)))) "8 bytes as URL-safe base64")
(is (= data (apply bytes/concat-b64 (map :d chunks)))))
(is (= 1 (count (env/split "" "json" 64))) "an empty message is one chunk"))
(defn- chunk-of [id i n data] {:kind :chunk :id id :index i :count n :enc :bin :data data})
(deftest reassembly-limits
(let [d (bytes/bytes->b64 [1 2 3])
asm (env/make-assembler 10)]
(testing "completes in order and out of order"
(let [[a r1] (env/assemble asm (chunk-of "x" 1 2 d) 0)
[a r2] (env/assemble a (chunk-of "x" 0 2 d) 0)]
(is (nil? r1))
(is (= {:enc :bin :data (bytes/concat-b64 d d)} r2))
(is (empty? (:partials a)))
(is (zero? (:total a)))))
(testing "byte budget"
(let [[a r1] (env/assemble asm (chunk-of "x" 0 3 d) 0)
[a r2] (env/assemble a (chunk-of "x" 1 3 d) 0)
[a r3] (env/assemble a (chunk-of "x" 2 3 d) 0)]
(is (nil? r1)) (is (nil? r2))
(is (= {:enc :bin :data (bytes/concat-b64 d d d)} r3) "9 bytes fit a 10-byte budget"))
(let [[a r1] (env/assemble (env/make-assembler 8) (chunk-of "x" 0 3 d) 0)
[a r2] (env/assemble a (chunk-of "x" 1 3 d) 0)
[a r3] (env/assemble a (chunk-of "x" 2 3 d) 0)]
(is (nil? r1)) (is (nil? r2))
(is (= :dropped r3) "the ninth byte exceeds an 8-byte budget")
(is (empty? (:partials a)) "the assembly is discarded")
(is (zero? (:total a)))))
(testing "eight concurrent ids"
(let [a (reduce (fn [a i] (first (env/assemble a (chunk-of (str "id" i) 0 2 "AA==") 0))) (env/make-assembler 1000) (range 8))
[a r] (env/assemble a (chunk-of "id9" 0 2 "AA==") 0)]
(is (= 8 (count (:partials a))))
(is (= :dropped r))
(let [[_ r] (env/assemble a (chunk-of "id3" 1 2 "AA==") 0)]
(is (map? r) "an existing assembly still completes"))))
(testing "inconsistent chunks drop the assembly"
(let [[a _] (env/assemble (env/make-assembler 1000) (chunk-of "x" 0 3 d) 0)
[a r] (env/assemble a (chunk-of "x" 0 3 d) 0)]
(is (= :dropped r) "a duplicate index")
(is (empty? (:partials a))))
(let [[a _] (env/assemble (env/make-assembler 1000) (chunk-of "x" 0 3 d) 0)
[_ r] (env/assemble a (chunk-of "x" 1 4 d) 0)]
(is (= :dropped r) "a different count")))
(testing "60 s age"
(let [[a _] (env/assemble (env/make-assembler 1000) (chunk-of "x" 0 2 d) 0)
[a r] (env/assemble a (chunk-of "y" 0 2 d) 60001)]
(is (nil? r))
(is (= ["y"] (keys (:partials a))) "the stale assembly was swept")))))
(deftest-async chunked-message-round-trips-through-envelopes
(let [secret (crypto/random-secret)
a (<! (crypto/room-crypto {:secret secret :app-salt "t" :peer-id "00000000000000000000000000000001"}))
b (<! (crypto/room-crypto {:secret secret :app-salt "t" :peer-id "0f1e2d3c4b5a69788796a5b4c3d2e1f0"}))
big (bytes/bytes->b64 (map #(mod % 251) (range 100000)))
chunks (env/split big "bin" (env/relay-slice-size 65536))
sealed (loop [cs chunks acc []]
(if-let [c (first cs)]
(recur (rest cs) (conj acc (<! (env/seal-signal a c (:peer-id b)))))
acc))
opened (loop [ss sealed acc []]
(if-let [s (first ss)]
(recur (rest ss) (conj acc (env/parse-inner (<! (env/open-signal b s (:peer-id a))))))
acc))
[_ result] (reduce (fn [[asm _] c] (env/assemble asm c 0)) [(env/make-assembler) nil] opened)]
(is (= 4 (count chunks)))
(is (every? #(<= (bytes/utf8-byte-length (env/envelope->json %)) 65536) sealed) "every relayed frame stays under the cap")
(is (= {:enc :bin :data big} result))))
|