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