codec_test.mx raw
1 package marmot
2
3 import (
4 "testing"
5
6 "git.smesh.lol/smesh/web/common/crypto/chacha20poly1305"
7 "git.smesh.lol/smesh/web/common/helpers"
8 "git.smesh.lol/smesh/web/common/nostr"
9 )
10
11 // The codec and parser halves of the wire format. The generating functions
12 // (NewNostrGroupData, MessageToEvent, KeyPackageToEvent and everything under
13 // them) call subtle.RandomBytes, whose native build is a jsbridge panic, so
14 // they can only run under wasm; what is testable natively is what parses,
15 // encodes from a value already in hand, or decrypts.
16
17 func TestQuicVecRoundTrip(t *testing.T) {
18 for _, size := range []int32{0, 1, 63, 64, 200, 16383, 16384} {
19 data := []byte{:size}
20 for i := int32(0); i < size; i++ {
21 data[i] = byte(i)
22 }
23 blob := appendQuicVec(nil, data)
24 vec, rest, err := readQuicVec(blob)
25 if err != nil {
26 t.Fatalf("size %d: %s", size, err.Error())
27 }
28 if int32(len(vec)) != size {
29 t.Fatalf("size %d: read %d", size, len(vec))
30 }
31 for i := int32(0); i < size; i++ {
32 if vec[i] != data[i] {
33 t.Fatalf("size %d: byte %d", size, i)
34 }
35 }
36 if len(rest) != 0 {
37 t.Fatalf("size %d: %d bytes left", size, len(rest))
38 }
39 }
40
41 // Two vectors back to back: the first read leaves the second as rest.
42 blob2 := appendQuicVec(nil, []byte("one"))
43 blob2 = appendQuicVec(blob2, []byte("two"))
44 first, tail, ferr := readQuicVec(blob2)
45 if ferr != nil {
46 t.Fatal(ferr)
47 }
48 if string(first) != "one" {
49 t.Fatalf("first = %s", first)
50 }
51 second, tail2, serr := readQuicVec(tail)
52 if serr != nil {
53 t.Fatal(serr)
54 }
55 if string(second) != "two" || len(tail2) != 0 {
56 t.Fatalf("second = %s, rest %d", second, len(tail2))
57 }
58 }
59
60 func TestReadQuicVecErrors(t *testing.T) {
61 // No header at all.
62 if _, _, errA := readQuicVec(nil); errA == nil {
63 t.Fatal("an empty buffer has no varint")
64 }
65 // Header says 5 bytes, two are present.
66 if _, _, errB := readQuicVec(appendQuicVec(nil, []byte("hello"))[:3]); errB == nil {
67 t.Fatal("a short body must fail")
68 }
69 // A length that cannot fit an int32 is rejected even though the varint is
70 // well formed.
71 var hdr [8]byte
72 n, errC := EncodeVarint(uint64(0x80000000), hdr[:])
73 if errC != nil {
74 t.Fatal(errC)
75 }
76 if _, _, errD := readQuicVec(hdr[:n]); errD == nil {
77 t.Fatal("a length above int32 must fail")
78 }
79 }
80
81 func TestDMGroupIDIsOrderIndependent(t *testing.T) {
82 a := []byte{:32}
83 b := []byte{:32}
84 c := []byte{:32}
85 for i := 0; i < 32; i++ {
86 a[i] = byte(i)
87 b[i] = byte(255 - i)
88 c[i] = byte(i + 1)
89 }
90 ab := DMGroupID(a, b)
91 ba := DMGroupID(b, a)
92 if len(ab) != 32 {
93 t.Fatalf("group id length %d", len(ab))
94 }
95 if string(ab) != string(ba) {
96 t.Fatal("both peers must derive the same group id")
97 }
98 if string(ab) == string(DMGroupID(a, c)) {
99 t.Fatal("different peers must not share a group id")
100 }
101 // Same key twice is still a valid (self) DM id.
102 if string(DMGroupID(a, a)) == string(ab) {
103 t.Fatal("self DM must differ from a peer DM")
104 }
105 }
106
107 func TestBytesLess(t *testing.T) {
108 if !bytesLess([]byte{1}, []byte{2}) {
109 t.Fatal("1 < 2")
110 }
111 if bytesLess([]byte{2}, []byte{1}) {
112 t.Fatal("2 is not < 1")
113 }
114 if bytesLess([]byte{1, 2}, []byte{1, 2}) {
115 t.Fatal("equal is not less")
116 }
117 // A proper prefix sorts first.
118 if !bytesLess([]byte{1}, []byte{1, 0}) {
119 t.Fatal("a prefix sorts first")
120 }
121 if bytesLess([]byte{1, 0}, []byte{1}) {
122 t.Fatal("a longer value is not less than its prefix")
123 }
124 if !bytesLess(nil, []byte{0}) {
125 t.Fatal("empty sorts first")
126 }
127 }
128
129 func TestNostrGroupDataRoundTrip(t *testing.T) {
130 d := &NostrGroupData{
131 Version: 2,
132 Name: "dm with bob",
133 Description: "a description with spaces and \u00e9",
134 }
135 for i := 0; i < 32; i++ {
136 d.NostrGroupID[i] = byte(i)
137 }
138 adminA := []byte{:32}
139 adminB := []byte{:32}
140 for i := 0; i < 32; i++ {
141 adminA[i] = byte(0xA0 + i%16)
142 adminB[i] = byte(0xB0 + i%16)
143 }
144 d.AdminPubkeys = [][]byte{adminA, adminB}
145 d.Relays = []string{"wss://relay.one/", "wss://relay.two/"}
146
147 raw, err := d.MarshalBytes()
148 if err != nil {
149 t.Fatal(err)
150 }
151 got, uerr := UnmarshalNostrGroupData(raw)
152 if uerr != nil {
153 t.Fatal(uerr)
154 }
155 if got.Version != d.Version || got.Name != d.Name || got.Description != d.Description {
156 t.Fatalf("scalars: %d %s %s", got.Version, got.Name, got.Description)
157 }
158 if string(got.NostrGroupID[:]) != string(d.NostrGroupID[:]) {
159 t.Fatal("group id")
160 }
161 if len(got.AdminPubkeys) != 2 || string(got.AdminPubkeys[1]) != string(adminB) {
162 t.Fatalf("admins: %d", len(got.AdminPubkeys))
163 }
164 if len(got.Relays) != 2 || got.Relays[0] != "wss://relay.one/" || got.Relays[1] != "wss://relay.two/" {
165 t.Fatalf("relays: %d", len(got.Relays))
166 }
167
168 // An empty name/description/relay set still round-trips.
169 bare := &NostrGroupData{Version: 1}
170 bareRaw, berr := bare.MarshalBytes()
171 if berr != nil {
172 t.Fatal(berr)
173 }
174 bareGot, bareUerr := UnmarshalNostrGroupData(bareRaw)
175 if bareUerr != nil {
176 t.Fatal(bareUerr)
177 }
178 if bareGot.Name != "" || bareGot.Description != "" || len(bareGot.AdminPubkeys) != 0 || len(bareGot.Relays) != 0 {
179 t.Fatal("empty group data grew fields")
180 }
181 }
182
183 func TestMarshalNostrGroupDataRejectsBadAdmin(t *testing.T) {
184 d := &NostrGroupData{Version: 2, AdminPubkeys: [][]byte{[]byte{:31}}}
185 if _, err := d.MarshalBytes(); err == nil {
186 t.Fatal("a 31-byte admin pubkey must be rejected")
187 }
188 }
189
190 func TestUnmarshalNostrGroupDataErrors(t *testing.T) {
191 // Shorter than version + group id.
192 if _, err := UnmarshalNostrGroupData([]byte{:33}); err == nil {
193 t.Fatal("33 bytes is too short")
194 }
195 // A valid header followed by a truncated name vector.
196 head := []byte{:34}
197 truncated := appendQuicVec(head, []byte("name"))[:len(head)+2]
198 if _, err := UnmarshalNostrGroupData(truncated); err == nil {
199 t.Fatal("a truncated name must fail")
200 }
201
202 // Build a payload whose admin vector is not a multiple of 32.
203 var body []byte
204 body = body | head
205 body = appendQuicVec(body, []byte("n"))
206 body = appendQuicVec(body, []byte("d"))
207 body = appendQuicVec(body, []byte{1, 2, 3})
208 body = appendQuicVec(body, nil)
209 body = appendQuicVec(body, nil)
210 body = appendQuicVec(body, nil)
211 body = appendQuicVec(body, nil)
212 body = appendQuicVec(body, nil)
213 if _, err := UnmarshalNostrGroupData(body); err == nil {
214 t.Fatal("admin data of 3 bytes must be rejected")
215 }
216
217 // A relay vector whose inner length runs past the vector.
218 inner := appendQuicVec(nil, []byte("wss://relay/"))
219 relaysBroken := appendQuicVec(nil, inner)[:len(inner)]
220 var body2 []byte
221 body2 = body2 | head
222 body2 = appendQuicVec(body2, []byte("n"))
223 body2 = appendQuicVec(body2, []byte("d"))
224 body2 = appendQuicVec(body2, []byte{:32})
225 body2 = appendQuicVec(body2, relaysBroken)
226 body2 = appendQuicVec(body2, nil)
227 body2 = appendQuicVec(body2, nil)
228 body2 = appendQuicVec(body2, nil)
229 body2 = appendQuicVec(body2, nil)
230 body2 = appendQuicVec(body2, nil)
231 if _, err := UnmarshalNostrGroupData(body2); err == nil {
232 t.Fatal("a truncated relay url must fail")
233 }
234 }
235
236 // groupMessageEvent hand-builds a kind 445 event whose content is a sealed
237 // payload, so EventToMessage can be exercised without subtle.RandomBytes.
238 func groupMessageEvent(secret [32]byte, nonce [12]byte, plain []byte, gid []byte) (ev *nostr.Event) {
239 ct := chacha20poly1305.Seal(secret, nonce, plain, nil)
240 raw := []byte{:12 + len(ct)}
241 for i := 0; i < 12; i++ {
242 raw[i] = nonce[i]
243 }
244 for i := 0; i < len(ct); i++ {
245 raw[12+i] = ct[i]
246 }
247 return &nostr.Event{
248 Kind: KindGroupMessage,
249 Content: helpers.Base64Encode(raw),
250 Tags: nostr.Tags{
251 nostr.Tag{"h", helpers.HexEncode(gid)},
252 nostr.Tag{"encoding", "base64"},
253 },
254 }
255 }
256
257 func TestEventToMessageRoundTrip(t *testing.T) {
258 // The AEAD works as a dependency now: the ported sealGeneric built its
259 // ciphertext and tag through a two-slice return (the Go sliceForAppend
260 // idiom), and Moxie relocates each returned slice on its own, so the tail
261 // came back as a copy and Seal answered all zeros. The source no longer
262 // relies on that aliasing (moxie ac5b4093), so this round trip is a real
263 // assertion again.
264 var secret [32]byte
265 var nonce [12]byte
266 gid := []byte{:32}
267 for i := 0; i < 32; i++ {
268 secret[i] = byte(i + 1)
269 gid[i] = byte(0x40 + i)
270 }
271 for i := 0; i < 12; i++ {
272 nonce[i] = byte(0x80 + i)
273 }
274 plain := []byte("mls ciphertext bytes")
275 ev := groupMessageEvent(secret, nonce, plain, gid)
276
277 gotGid, gotPlain, err := EventToMessage(ev, secret[:])
278 if err != nil {
279 t.Fatal(err)
280 }
281 if string(gotGid) != string(gid) {
282 t.Fatal("group id mismatch")
283 }
284 if string(gotPlain) != string(plain) {
285 t.Fatalf("plaintext = %s", gotPlain)
286 }
287 }
288
289 func TestEventToMessageErrors(t *testing.T) {
290 var secret [32]byte
291 var nonce [12]byte
292 gid := []byte{:32}
293 for i := 0; i < 32; i++ {
294 secret[i] = byte(i + 1)
295 }
296 good := groupMessageEvent(secret, nonce, []byte("hello"), gid)
297
298 // Wrong kind.
299 wrong := &nostr.Event{Kind: KindWelcome, Content: good.Content, Tags: good.Tags}
300 if _, _, err := EventToMessage(wrong, secret[:]); err == nil {
301 t.Fatal("a non-445 event must fail")
302 }
303 // Wrong secret length.
304 if _, _, err := EventToMessage(good, secret[:31]); err == nil {
305 t.Fatal("a short secret must fail")
306 }
307 // Missing h tag.
308 noH := &nostr.Event{Kind: KindGroupMessage, Content: good.Content}
309 if _, _, err := EventToMessage(noH, secret[:]); err == nil {
310 t.Fatal("a missing h tag must fail")
311 }
312 // h tag that is not hex.
313 badH := &nostr.Event{Kind: KindGroupMessage, Content: good.Content,
314 Tags: nostr.Tags{nostr.Tag{"h", "zz"}}}
315 if _, _, err := EventToMessage(badH, secret[:]); err == nil {
316 t.Fatal("a non-hex group id must fail")
317 }
318 // Content that is not base64.
319 badB64 := &nostr.Event{Kind: KindGroupMessage, Content: "!!!", Tags: good.Tags}
320 if _, _, err := EventToMessage(badB64, secret[:]); err == nil {
321 t.Fatal("non-base64 content must fail")
322 }
323 // Content shorter than the nonce.
324 short := &nostr.Event{Kind: KindGroupMessage, Content: helpers.Base64Encode([]byte{1, 2, 3}), Tags: good.Tags}
325 if _, _, err := EventToMessage(short, secret[:]); err == nil {
326 t.Fatal("content shorter than a nonce must fail")
327 }
328 // A valid-looking event that decrypts with the wrong key.
329 var other [32]byte
330 for i := 0; i < 32; i++ {
331 other[i] = byte(0xEE - i)
332 }
333 if _, _, err := EventToMessage(good, other[:]); err == nil {
334 t.Fatal("the wrong secret must fail to open")
335 }
336 }
337
338 func TestEventToKeyPackageErrors(t *testing.T) {
339 // Wrong kind.
340 ev := &nostr.Event{Kind: KindWelcome, Content: "AAA="}
341 if _, err := EventToKeyPackage(ev); err == nil {
342 t.Fatal("a non-443 event must fail")
343 }
344 // Non-base64 content with the base64 tag.
345 bad := &nostr.Event{Kind: KindKeyPackage, Content: "!!!",
346 Tags: nostr.Tags{nostr.Tag{"encoding", "base64"}}}
347 if _, err := EventToKeyPackage(bad); err == nil {
348 t.Fatal("non-base64 content must fail")
349 }
350 // Base64 that is not a key package.
351 garbage := &nostr.Event{Kind: KindKeyPackage, Content: helpers.Base64Encode([]byte("not a key package")),
352 Tags: nostr.Tags{nostr.Tag{"encoding", "base64"}}}
353 if _, err := EventToKeyPackage(garbage); err == nil {
354 t.Fatal("garbage must not parse as a key package")
355 }
356 }
357
358 func TestRumorToWelcomeErrors(t *testing.T) {
359 ev := &nostr.Event{Kind: KindGroupMessage, Content: "AAA="}
360 if _, err := RumorToWelcome(ev); err == nil {
361 t.Fatal("a non-444 event must fail")
362 }
363 // Base64 decode failure with the base64 tag present.
364 bad := &nostr.Event{Kind: KindWelcome, Content: "!!!",
365 Tags: nostr.Tags{nostr.Tag{"encoding", "base64"}}}
366 if _, err := RumorToWelcome(bad); err == nil {
367 t.Fatal("non-base64 content must fail")
368 }
369 // Base64 that is not a Welcome.
370 garbage := &nostr.Event{Kind: KindWelcome, Content: helpers.Base64Encode([]byte("nope")),
371 Tags: nostr.Tags{nostr.Tag{"encoding", "base64"}}}
372 if _, err := RumorToWelcome(garbage); err == nil {
373 t.Fatal("garbage must not parse as a welcome")
374 }
375 }
376
377 func TestUnmarshalGroupStateErrors(t *testing.T) {
378 // Nothing at all.
379 if _, err := UnmarshalGroupState(nil); err == nil {
380 t.Fatal("empty input must fail")
381 }
382 // A length prefix longer than the rest.
383 if _, err := UnmarshalGroupState([]byte{0x00, 0x08, 0x01}); err == nil {
384 t.Fatal("a short group blob must fail")
385 }
386 // A complete prefix wrapping garbage group bytes.
387 inner := []byte("garbage group bytes")
388 var raw []byte
389 raw = push(raw, byte(len(inner)>>8), byte(len(inner)))
390 raw = raw | inner
391 if _, err := UnmarshalGroupState(raw); err == nil {
392 t.Fatal("garbage group bytes must fail")
393 }
394 }
395