extension.go raw
1 package marmot
2
3 import (
4 "crypto/rand"
5 "encoding/binary"
6 "fmt"
7
8 "github.com/emersion/go-mls"
9 )
10
11 // NostrGroupData holds the Marmot group metadata carried in the MLS
12 // group context extension 0xf2ee. Version 2 is current.
13 type NostrGroupData struct {
14 Version uint16
15 NostrGroupID [32]byte
16 Name string
17 Description string
18 AdminPubkeys [][]byte // 32-byte x-only pubkeys
19 Relays []string // WebSocket relay URLs
20 }
21
22 // NewNostrGroupData creates a NostrGroupData with a random group ID.
23 func NewNostrGroupData(name string, adminPub []byte, relays []string) (*NostrGroupData, error) {
24 var groupID [32]byte
25 if _, err := rand.Read(groupID[:]); err != nil {
26 return nil, fmt.Errorf("generate group id: %w", err)
27 }
28 return &NostrGroupData{
29 Version: 2,
30 NostrGroupID: groupID,
31 Name: name,
32 AdminPubkeys: [][]byte{adminPub},
33 Relays: relays,
34 }, nil
35 }
36
37 // MarshalExtension serializes to the TLS wire format expected by MIP-01.
38 func (d *NostrGroupData) MarshalExtension() (mls.Extension, error) {
39 data, err := d.marshalBytes()
40 if err != nil {
41 return mls.Extension{}, err
42 }
43 return mls.NewExtension(mls.ExtensionTypeNostrGroupData, data), nil
44 }
45
46 func (d *NostrGroupData) marshalBytes() ([]byte, error) {
47 var buf []byte
48
49 // version (uint16 big-endian)
50 buf = binary.BigEndian.AppendUint16(buf, d.Version)
51
52 // nostr_group_id (fixed 32 bytes)
53 buf = append(buf, d.NostrGroupID[:]...)
54
55 // name (QUIC varint length prefix + UTF-8)
56 buf = appendQUICVec(buf, []byte(d.Name))
57
58 // description (QUIC varint length prefix + UTF-8)
59 buf = appendQUICVec(buf, []byte(d.Description))
60
61 // admin_pubkeys (QUIC varint length prefix + concatenated 32-byte keys)
62 adminData := make([]byte, 0, 32*len(d.AdminPubkeys))
63 for _, pk := range d.AdminPubkeys {
64 if len(pk) != 32 {
65 return nil, fmt.Errorf("admin pubkey must be 32 bytes, got %d", len(pk))
66 }
67 adminData = append(adminData, pk...)
68 }
69 buf = appendQUICVec(buf, adminData)
70
71 // relays (QUIC varint outer length + individually prefixed URLs)
72 var relayBuf []byte
73 for _, r := range d.Relays {
74 relayBuf = appendQUICVec(relayBuf, []byte(r))
75 }
76 buf = appendQUICVec(buf, relayBuf)
77
78 // image_hash, image_key, image_nonce, image_upload_key (all empty)
79 buf = appendQUICVec(buf, nil)
80 buf = appendQUICVec(buf, nil)
81 buf = appendQUICVec(buf, nil)
82 buf = appendQUICVec(buf, nil)
83
84 return buf, nil
85 }
86
87 // UnmarshalNostrGroupData parses the TLS extension data.
88 func UnmarshalNostrGroupData(data []byte) (*NostrGroupData, error) {
89 if len(data) < 34 {
90 return nil, fmt.Errorf("extension data too short: %d bytes", len(data))
91 }
92
93 d := &NostrGroupData{}
94 d.Version = binary.BigEndian.Uint16(data[0:2])
95 copy(d.NostrGroupID[:], data[2:34])
96
97 rest := data[34:]
98 var err error
99 var b []byte
100
101 // name
102 b, rest, err = readQUICVec(rest)
103 if err != nil {
104 return nil, fmt.Errorf("read name: %w", err)
105 }
106 d.Name = string(b)
107
108 // description
109 b, rest, err = readQUICVec(rest)
110 if err != nil {
111 return nil, fmt.Errorf("read description: %w", err)
112 }
113 d.Description = string(b)
114
115 // admin_pubkeys
116 b, rest, err = readQUICVec(rest)
117 if err != nil {
118 return nil, fmt.Errorf("read admin_pubkeys: %w", err)
119 }
120 if len(b)%32 != 0 {
121 return nil, fmt.Errorf("admin_pubkeys length %d not multiple of 32", len(b))
122 }
123 for i := 0; i < len(b); i += 32 {
124 pk := make([]byte, 32)
125 copy(pk, b[i:i+32])
126 d.AdminPubkeys = append(d.AdminPubkeys, pk)
127 }
128
129 // relays
130 b, rest, err = readQUICVec(rest)
131 if err != nil {
132 return nil, fmt.Errorf("read relays: %w", err)
133 }
134 for len(b) > 0 {
135 var url []byte
136 url, b, err = readQUICVec(b)
137 if err != nil {
138 return nil, fmt.Errorf("read relay url: %w", err)
139 }
140 d.Relays = append(d.Relays, string(url))
141 }
142
143 // Skip image fields (we don't use them for DMs)
144 _ = rest
145
146 return d, nil
147 }
148
149 // appendQUICVec appends a QUIC-style variable-length integer prefix followed
150 // by the data bytes. RFC 9000 Section 16.
151 func appendQUICVec(dst, data []byte) []byte {
152 n := uint64(len(data))
153 if n <= 63 {
154 dst = append(dst, byte(n))
155 } else if n <= 16383 {
156 dst = append(dst, byte(0x40|n>>8), byte(n))
157 } else if n <= 1073741823 {
158 dst = append(dst, byte(0x80|n>>24), byte(n>>16), byte(n>>8), byte(n))
159 } else {
160 dst = append(dst,
161 byte(0xc0|n>>56), byte(n>>48), byte(n>>40), byte(n>>32),
162 byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
163 }
164 dst = append(dst, data...)
165 return dst
166 }
167
168 // readQUICVec reads a QUIC varint-prefixed byte vector.
169 func readQUICVec(data []byte) (vec, rest []byte, err error) {
170 if len(data) == 0 {
171 return nil, nil, fmt.Errorf("unexpected end of data")
172 }
173
174 prefix := data[0] >> 6
175 var length uint64
176
177 switch prefix {
178 case 0:
179 length = uint64(data[0] & 0x3f)
180 data = data[1:]
181 case 1:
182 if len(data) < 2 {
183 return nil, nil, fmt.Errorf("truncated 2-byte varint")
184 }
185 length = uint64(data[0]&0x3f)<<8 | uint64(data[1])
186 data = data[2:]
187 case 2:
188 if len(data) < 4 {
189 return nil, nil, fmt.Errorf("truncated 4-byte varint")
190 }
191 length = uint64(data[0]&0x3f)<<24 | uint64(data[1])<<16 | uint64(data[2])<<8 | uint64(data[3])
192 data = data[4:]
193 case 3:
194 if len(data) < 8 {
195 return nil, nil, fmt.Errorf("truncated 8-byte varint")
196 }
197 length = uint64(data[0]&0x3f)<<56 | uint64(data[1])<<48 | uint64(data[2])<<40 | uint64(data[3])<<32 |
198 uint64(data[4])<<24 | uint64(data[5])<<16 | uint64(data[6])<<8 | uint64(data[7])
199 data = data[8:]
200 }
201
202 if uint64(len(data)) < length {
203 return nil, nil, fmt.Errorf("data too short: need %d, have %d", length, len(data))
204 }
205
206 return data[:length], data[length:], nil
207 }
208