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