package marmot import ( "testing" "git.smesh.lol/nostr/pkg/crypto/chacha20poly1305" "git.smesh.lol/musiquay/web/common/helpers" "git.smesh.lol/nostr/pkg/core" ) // The codec and parser halves of the wire format. The generating functions // (NewNostrGroupData, MessageToEvent, KeyPackageToEvent and everything under // them) call subtle.RandomBytes, whose native build is a jsbridge panic, so // they can only run under wasm; what is testable natively is what parses, // encodes from a value already in hand, or decrypts. func TestQuicVecRoundTrip(t *testing.T) { for _, size := range []int32{0, 1, 63, 64, 200, 16383, 16384} { data := []byte{:size} for i := int32(0); i < size; i++ { data[i] = byte(i) } blob := appendQuicVec(nil, data) vec, rest, err := readQuicVec(blob) if err != nil { t.Fatalf("size %d: %s", size, err.Error()) } if int32(len(vec)) != size { t.Fatalf("size %d: read %d", size, len(vec)) } for i := int32(0); i < size; i++ { if vec[i] != data[i] { t.Fatalf("size %d: byte %d", size, i) } } if len(rest) != 0 { t.Fatalf("size %d: %d bytes left", size, len(rest)) } } // Two vectors back to back: the first read leaves the second as rest. blob2 := appendQuicVec(nil, []byte("one")) blob2 = appendQuicVec(blob2, []byte("two")) first, tail, ferr := readQuicVec(blob2) if ferr != nil { t.Fatal(ferr) } if string(first) != "one" { t.Fatalf("first = %s", first) } second, tail2, serr := readQuicVec(tail) if serr != nil { t.Fatal(serr) } if string(second) != "two" || len(tail2) != 0 { t.Fatalf("second = %s, rest %d", second, len(tail2)) } } func TestReadQuicVecErrors(t *testing.T) { // No header at all. if _, _, errA := readQuicVec(nil); errA == nil { t.Fatal("an empty buffer has no varint") } // Header says 5 bytes, two are present. if _, _, errB := readQuicVec(appendQuicVec(nil, []byte("hello"))[:3]); errB == nil { t.Fatal("a short body must fail") } // A length that cannot fit an int32 is rejected even though the varint is // well formed. var hdr [8]byte n, errC := EncodeVarint(uint64(0x80000000), hdr[:]) if errC != nil { t.Fatal(errC) } if _, _, errD := readQuicVec(hdr[:n]); errD == nil { t.Fatal("a length above int32 must fail") } } func TestDMGroupIDIsOrderIndependent(t *testing.T) { a := []byte{:32} b := []byte{:32} c := []byte{:32} for i := 0; i < 32; i++ { a[i] = byte(i) b[i] = byte(255 - i) c[i] = byte(i + 1) } ab := DMGroupID(a, b) ba := DMGroupID(b, a) if len(ab) != 32 { t.Fatalf("group id length %d", len(ab)) } if string(ab) != string(ba) { t.Fatal("both peers must derive the same group id") } if string(ab) == string(DMGroupID(a, c)) { t.Fatal("different peers must not share a group id") } // Same key twice is still a valid (self) DM id. if string(DMGroupID(a, a)) == string(ab) { t.Fatal("self DM must differ from a peer DM") } } func TestBytesLess(t *testing.T) { if !bytesLess([]byte{1}, []byte{2}) { t.Fatal("1 < 2") } if bytesLess([]byte{2}, []byte{1}) { t.Fatal("2 is not < 1") } if bytesLess([]byte{1, 2}, []byte{1, 2}) { t.Fatal("equal is not less") } // A proper prefix sorts first. if !bytesLess([]byte{1}, []byte{1, 0}) { t.Fatal("a prefix sorts first") } if bytesLess([]byte{1, 0}, []byte{1}) { t.Fatal("a longer value is not less than its prefix") } if !bytesLess(nil, []byte{0}) { t.Fatal("empty sorts first") } } func TestNostrGroupDataRoundTrip(t *testing.T) { d := &NostrGroupData{ Version: 2, Name: "dm with bob", Description: "a description with spaces and \u00e9", } for i := 0; i < 32; i++ { d.NostrGroupID[i] = byte(i) } adminA := []byte{:32} adminB := []byte{:32} for i := 0; i < 32; i++ { adminA[i] = byte(0xA0 + i%16) adminB[i] = byte(0xB0 + i%16) } d.AdminPubkeys = [][]byte{adminA, adminB} d.Relays = []string{"wss://relay.one/", "wss://relay.two/"} raw, err := d.MarshalBytes() if err != nil { t.Fatal(err) } got, uerr := UnmarshalNostrGroupData(raw) if uerr != nil { t.Fatal(uerr) } if got.Version != d.Version || got.Name != d.Name || got.Description != d.Description { t.Fatalf("scalars: %d %s %s", got.Version, got.Name, got.Description) } if string(got.NostrGroupID[:]) != string(d.NostrGroupID[:]) { t.Fatal("group id") } if len(got.AdminPubkeys) != 2 || string(got.AdminPubkeys[1]) != string(adminB) { t.Fatalf("admins: %d", len(got.AdminPubkeys)) } if len(got.Relays) != 2 || got.Relays[0] != "wss://relay.one/" || got.Relays[1] != "wss://relay.two/" { t.Fatalf("relays: %d", len(got.Relays)) } // An empty name/description/relay set still round-trips. bare := &NostrGroupData{Version: 1} bareRaw, berr := bare.MarshalBytes() if berr != nil { t.Fatal(berr) } bareGot, bareUerr := UnmarshalNostrGroupData(bareRaw) if bareUerr != nil { t.Fatal(bareUerr) } if bareGot.Name != "" || bareGot.Description != "" || len(bareGot.AdminPubkeys) != 0 || len(bareGot.Relays) != 0 { t.Fatal("empty group data grew fields") } } func TestMarshalNostrGroupDataRejectsBadAdmin(t *testing.T) { d := &NostrGroupData{Version: 2, AdminPubkeys: [][]byte{[]byte{:31}}} if _, err := d.MarshalBytes(); err == nil { t.Fatal("a 31-byte admin pubkey must be rejected") } } func TestUnmarshalNostrGroupDataErrors(t *testing.T) { // Shorter than version + group id. if _, err := UnmarshalNostrGroupData([]byte{:33}); err == nil { t.Fatal("33 bytes is too short") } // A valid header followed by a truncated name vector. head := []byte{:34} truncated := appendQuicVec(head, []byte("name"))[:len(head)+2] if _, err := UnmarshalNostrGroupData(truncated); err == nil { t.Fatal("a truncated name must fail") } // Build a payload whose admin vector is not a multiple of 32. var body []byte body = body | head body = appendQuicVec(body, []byte("n")) body = appendQuicVec(body, []byte("d")) body = appendQuicVec(body, []byte{1, 2, 3}) body = appendQuicVec(body, nil) body = appendQuicVec(body, nil) body = appendQuicVec(body, nil) body = appendQuicVec(body, nil) body = appendQuicVec(body, nil) if _, err := UnmarshalNostrGroupData(body); err == nil { t.Fatal("admin data of 3 bytes must be rejected") } // A relay vector whose inner length runs past the vector. inner := appendQuicVec(nil, []byte("wss://relay/")) relaysBroken := appendQuicVec(nil, inner)[:len(inner)] var body2 []byte body2 = body2 | head body2 = appendQuicVec(body2, []byte("n")) body2 = appendQuicVec(body2, []byte("d")) body2 = appendQuicVec(body2, []byte{:32}) body2 = appendQuicVec(body2, relaysBroken) body2 = appendQuicVec(body2, nil) body2 = appendQuicVec(body2, nil) body2 = appendQuicVec(body2, nil) body2 = appendQuicVec(body2, nil) body2 = appendQuicVec(body2, nil) if _, err := UnmarshalNostrGroupData(body2); err == nil { t.Fatal("a truncated relay url must fail") } } // groupMessageEvent hand-builds a kind 445 event whose content is a sealed // payload, so EventToMessage can be exercised without subtle.RandomBytes. func groupMessageEvent(secret [32]byte, nonce [12]byte, plain []byte, gid []byte) (ev *nostr.Event) { ct := chacha20poly1305.Seal(secret, nonce, plain, nil) raw := []byte{:12 + len(ct)} for i := 0; i < 12; i++ { raw[i] = nonce[i] } for i := 0; i < len(ct); i++ { raw[12+i] = ct[i] } return &nostr.Event{ Kind: KindGroupMessage, Content: helpers.Base64Encode(raw), Tags: [][]string{ []string{"h", helpers.HexEncode(gid)}, []string{"encoding", "base64"}, }, } } func TestEventToMessageRoundTrip(t *testing.T) { // The AEAD works as a dependency now: the ported sealGeneric built its // ciphertext and tag through a two-slice return (the Go sliceForAppend // idiom), and Moxie relocates each returned slice on its own, so the tail // came back as a copy and Seal answered all zeros. The source no longer // relies on that aliasing (moxie ac5b4093), so this round trip is a real // assertion again. var secret [32]byte var nonce [12]byte gid := []byte{:32} for i := 0; i < 32; i++ { secret[i] = byte(i + 1) gid[i] = byte(0x40 + i) } for i := 0; i < 12; i++ { nonce[i] = byte(0x80 + i) } plain := []byte("mls ciphertext bytes") ev := groupMessageEvent(secret, nonce, plain, gid) gotGid, gotPlain, err := EventToMessage(ev, secret[:]) if err != nil { t.Fatal(err) } if string(gotGid) != string(gid) { t.Fatal("group id mismatch") } if string(gotPlain) != string(plain) { t.Fatalf("plaintext = %s", gotPlain) } } func TestEventToMessageErrors(t *testing.T) { var secret [32]byte var nonce [12]byte gid := []byte{:32} for i := 0; i < 32; i++ { secret[i] = byte(i + 1) } good := groupMessageEvent(secret, nonce, []byte("hello"), gid) // Wrong kind. wrong := &nostr.Event{Kind: KindWelcome, Content: good.Content, Tags: good.Tags} if _, _, err := EventToMessage(wrong, secret[:]); err == nil { t.Fatal("a non-445 event must fail") } // Wrong secret length. if _, _, err := EventToMessage(good, secret[:31]); err == nil { t.Fatal("a short secret must fail") } // Missing h tag. noH := &nostr.Event{Kind: KindGroupMessage, Content: good.Content} if _, _, err := EventToMessage(noH, secret[:]); err == nil { t.Fatal("a missing h tag must fail") } // h tag that is not hex. badH := &nostr.Event{Kind: KindGroupMessage, Content: good.Content, Tags: [][]string{[]string{"h", "zz"}}} if _, _, err := EventToMessage(badH, secret[:]); err == nil { t.Fatal("a non-hex group id must fail") } // Content that is not base64. badB64 := &nostr.Event{Kind: KindGroupMessage, Content: "!!!", Tags: good.Tags} if _, _, err := EventToMessage(badB64, secret[:]); err == nil { t.Fatal("non-base64 content must fail") } // Content shorter than the nonce. short := &nostr.Event{Kind: KindGroupMessage, Content: helpers.Base64Encode([]byte{1, 2, 3}), Tags: good.Tags} if _, _, err := EventToMessage(short, secret[:]); err == nil { t.Fatal("content shorter than a nonce must fail") } // A valid-looking event that decrypts with the wrong key. var other [32]byte for i := 0; i < 32; i++ { other[i] = byte(0xEE - i) } if _, _, err := EventToMessage(good, other[:]); err == nil { t.Fatal("the wrong secret must fail to open") } } func TestEventToKeyPackageErrors(t *testing.T) { // Wrong kind. ev := &nostr.Event{Kind: KindWelcome, Content: "AAA="} if _, err := EventToKeyPackage(ev); err == nil { t.Fatal("a non-443 event must fail") } // Non-base64 content with the base64 tag. bad := &nostr.Event{Kind: KindKeyPackage, Content: "!!!", Tags: [][]string{[]string{"encoding", "base64"}}} if _, err := EventToKeyPackage(bad); err == nil { t.Fatal("non-base64 content must fail") } // Base64 that is not a key package. garbage := &nostr.Event{Kind: KindKeyPackage, Content: helpers.Base64Encode([]byte("not a key package")), Tags: [][]string{[]string{"encoding", "base64"}}} if _, err := EventToKeyPackage(garbage); err == nil { t.Fatal("garbage must not parse as a key package") } } func TestRumorToWelcomeErrors(t *testing.T) { ev := &nostr.Event{Kind: KindGroupMessage, Content: "AAA="} if _, err := RumorToWelcome(ev); err == nil { t.Fatal("a non-444 event must fail") } // Base64 decode failure with the base64 tag present. bad := &nostr.Event{Kind: KindWelcome, Content: "!!!", Tags: [][]string{[]string{"encoding", "base64"}}} if _, err := RumorToWelcome(bad); err == nil { t.Fatal("non-base64 content must fail") } // Base64 that is not a Welcome. garbage := &nostr.Event{Kind: KindWelcome, Content: helpers.Base64Encode([]byte("nope")), Tags: [][]string{[]string{"encoding", "base64"}}} if _, err := RumorToWelcome(garbage); err == nil { t.Fatal("garbage must not parse as a welcome") } } func TestUnmarshalGroupStateErrors(t *testing.T) { // Nothing at all. if _, err := UnmarshalGroupState(nil); err == nil { t.Fatal("empty input must fail") } // A length prefix longer than the rest. if _, err := UnmarshalGroupState([]byte{0x00, 0x08, 0x01}); err == nil { t.Fatal("a short group blob must fail") } // A complete prefix wrapping garbage group bytes. inner := []byte("garbage group bytes") var raw []byte raw = push(raw, byte(len(inner)>>8), byte(len(inner))) raw = raw | inner if _, err := UnmarshalGroupState(raw); err == nil { t.Fatal("garbage group bytes must fail") } }