ephemeral_wire.go raw

   1  package crypto
   2  
   3  import (
   4  	"encoding/binary"
   5  	"errors"
   6  )
   7  
   8  // Compact binary wire format for EphemeralMessage (V2).
   9  //
  10  // Layout:
  11  //   [2 bytes]   Version (uint16 LE, value=2)
  12  //   [4 bytes]   Signature length (uint32 LE)
  13  //   [variable]  Compact V2 signature bytes (Signature.MarshalV2 output, ~98 bytes)
  14  //   [2 bytes]   Pattern site count (uint16 LE, total including unoccupied)
  15  //   [2 bytes]   Pattern occupied count (uint16 LE)
  16  //   [2 bytes]   Pattern tag count (uint16 LE)
  17  //   [variable]  Pattern tag table: for each tag, 1-byte length + UTF-8 bytes
  18  //   Per occupied site (6 bytes each):
  19  //     [2 bytes]  Site index (uint16 LE)
  20  //     [1 byte]   Tag table index (uint8)
  21  //     [1 byte]   Projection (6-bit) | Perm low 2 bits (high 2 bits)
  22  //     [1 byte]   Lock-in quantized to uint8 (0-255)
  23  //     [1 byte]   Flags: bit 0 = perm bit 2, bits 1-7 reserved
  24  //   Sender fingerprint:
  25  //     [56 bytes]  Hash as Hamadryad (hashed from string)
  26  //     [12 bytes]  PermDist (6 × uint16 LE)
  27  
  28  const ephemeralWireVersion = 2
  29  
  30  // MarshalEphemeral encodes an EphemeralMessage into compact binary form.
  31  // The caller must supply the lattice dimension N (from Params) for V2
  32  // signature encoding.
  33  func (m *EphemeralMessage) MarshalEphemeral(n int) ([]byte, error) {
  34  	if m == nil {
  35  		return nil, errors.New("crypto: nil ephemeral message")
  36  	}
  37  	if m.Signature == nil {
  38  		return nil, errors.New("crypto: nil signature in ephemeral message")
  39  	}
  40  
  41  	// Marshal the signature using compact V2 format (~98 bytes for N=256).
  42  	sigBytes, err := m.Signature.MarshalV2(n)
  43  	if err != nil {
  44  		return nil, err
  45  	}
  46  
  47  	// Build pattern tag table.
  48  	tagIndex := make(map[string]uint8)
  49  	var tagTable []string
  50  	var occupied []int
  51  	for i, site := range m.Pattern {
  52  		if site.Occupied {
  53  			if _, ok := tagIndex[site.TypeTag]; !ok {
  54  				if len(tagTable) >= 255 {
  55  					return nil, errors.New("crypto: too many distinct pattern tags")
  56  				}
  57  				tagIndex[site.TypeTag] = uint8(len(tagTable))
  58  				tagTable = append(tagTable, site.TypeTag)
  59  			}
  60  			occupied = append(occupied, i)
  61  		}
  62  	}
  63  
  64  	// Calculate tag table size.
  65  	tagTableSize := 0
  66  	for _, tag := range tagTable {
  67  		tagTableSize += 1 + len(tag)
  68  	}
  69  
  70  	// Total size.
  71  	headerSize := 2 + 4 + len(sigBytes) + 2 + 2 + 2 + tagTableSize +
  72  		len(occupied)*compactPerSiteSize + HamBytes + 12
  73  	buf := make([]byte, headerSize)
  74  	pos := 0
  75  
  76  	// Version.
  77  	binary.LittleEndian.PutUint16(buf[pos:], ephemeralWireVersion)
  78  	pos += 2
  79  
  80  	// Signature length + bytes.
  81  	binary.LittleEndian.PutUint32(buf[pos:], uint32(len(sigBytes)))
  82  	pos += 4
  83  	copy(buf[pos:], sigBytes)
  84  	pos += len(sigBytes)
  85  
  86  	// Pattern site count (total).
  87  	binary.LittleEndian.PutUint16(buf[pos:], uint16(len(m.Pattern)))
  88  	pos += 2
  89  
  90  	// Pattern occupied count.
  91  	binary.LittleEndian.PutUint16(buf[pos:], uint16(len(occupied)))
  92  	pos += 2
  93  
  94  	// Pattern tag count.
  95  	binary.LittleEndian.PutUint16(buf[pos:], uint16(len(tagTable)))
  96  	pos += 2
  97  
  98  	// Tag table.
  99  	for _, tag := range tagTable {
 100  		buf[pos] = uint8(len(tag))
 101  		pos++
 102  		copy(buf[pos:], tag)
 103  		pos += len(tag)
 104  	}
 105  
 106  	// Per occupied site (6 bytes each).
 107  	for _, idx := range occupied {
 108  		site := m.Pattern[idx]
 109  
 110  		binary.LittleEndian.PutUint16(buf[pos:], uint16(site.Index))
 111  		pos += 2
 112  
 113  		buf[pos] = tagIndex[site.TypeTag]
 114  		pos++
 115  
 116  		projPerm := (site.Projection & 0x3F) | ((site.Perm & 0x03) << 6)
 117  		buf[pos] = projPerm
 118  		pos++
 119  
 120  		buf[pos] = quantizeLockIn(site.LockIn)
 121  		pos++
 122  
 123  		flags := uint8(0)
 124  		if site.Perm&0x04 != 0 {
 125  			flags |= 0x01
 126  		}
 127  		buf[pos] = flags
 128  		pos++
 129  	}
 130  
 131  	// Sender fingerprint: hash as Hamadryad.
 132  	fpHash := Hash([]byte(m.SenderFingerprint.Hash))
 133  	copy(buf[pos:], fpHash[:])
 134  	pos += HamBytes
 135  
 136  	// PermDist (6 × uint16 LE).
 137  	for i := range 6 {
 138  		binary.LittleEndian.PutUint16(buf[pos:], uint16(m.SenderFingerprint.PermDist[i]))
 139  		pos += 2
 140  	}
 141  
 142  	return buf[:pos], nil
 143  }
 144  
 145  // UnmarshalEphemeralMessage decodes a compact binary EphemeralMessage.
 146  func UnmarshalEphemeralMessage(data []byte) (*EphemeralMessage, error) {
 147  	if len(data) < 2 {
 148  		return nil, errors.New("crypto: ephemeral message too short")
 149  	}
 150  	pos := 0
 151  
 152  	// Version.
 153  	version := binary.LittleEndian.Uint16(data[pos:])
 154  	if version != ephemeralWireVersion {
 155  		return nil, errors.New("crypto: unsupported ephemeral wire version")
 156  	}
 157  	pos += 2
 158  
 159  	// Signature length.
 160  	if pos+4 > len(data) {
 161  		return nil, errors.New("crypto: truncated signature length")
 162  	}
 163  	sigLen := int(binary.LittleEndian.Uint32(data[pos:]))
 164  	pos += 4
 165  
 166  	if pos+sigLen > len(data) {
 167  		return nil, errors.New("crypto: truncated signature data")
 168  	}
 169  	sig, consumed, err := UnmarshalSignatureV2(data[pos : pos+sigLen])
 170  	if err != nil {
 171  		return nil, err
 172  	}
 173  	_ = consumed // sigLen already bounds the slice
 174  	pos += sigLen
 175  
 176  	// Pattern counts.
 177  	if pos+6 > len(data) {
 178  		return nil, errors.New("crypto: truncated pattern header")
 179  	}
 180  	totalSites := int(binary.LittleEndian.Uint16(data[pos:]))
 181  	pos += 2
 182  	occCount := int(binary.LittleEndian.Uint16(data[pos:]))
 183  	pos += 2
 184  	tagCount := int(binary.LittleEndian.Uint16(data[pos:]))
 185  	pos += 2
 186  
 187  	// Tag table.
 188  	tagTable := make([]string, tagCount)
 189  	for i := range tagCount {
 190  		if pos >= len(data) {
 191  			return nil, errors.New("crypto: truncated pattern tag table")
 192  		}
 193  		tagLen := int(data[pos])
 194  		pos++
 195  		if pos+tagLen > len(data) {
 196  			return nil, errors.New("crypto: truncated pattern tag name")
 197  		}
 198  		tagTable[i] = string(data[pos : pos+tagLen])
 199  		pos += tagLen
 200  	}
 201  
 202  	// Per occupied site.
 203  	needed := occCount*compactPerSiteSize + HamBytes + 12
 204  	if pos+needed > len(data) {
 205  		return nil, errors.New("crypto: truncated site data or fingerprint")
 206  	}
 207  
 208  	// Build pattern: start with all unoccupied, fill in occupied sites.
 209  	pattern := make([]SiteMark, totalSites)
 210  	for i := range occCount {
 211  		siteIdx := binary.LittleEndian.Uint16(data[pos:])
 212  		pos += 2
 213  
 214  		tagIdx := data[pos]
 215  		pos++
 216  		tag := ""
 217  		if int(tagIdx) < len(tagTable) {
 218  			tag = tagTable[tagIdx]
 219  		}
 220  
 221  		projPerm := data[pos]
 222  		pos++
 223  		proj := projPerm & 0x3F
 224  		permLow := (projPerm >> 6) & 0x03
 225  
 226  		liQuant := data[pos]
 227  		pos++
 228  		li := dequantizeLockIn(liQuant)
 229  
 230  		flags := data[pos]
 231  		pos++
 232  		perm := permLow
 233  		if flags&0x01 != 0 {
 234  			perm |= 0x04
 235  		}
 236  
 237  		if int(siteIdx) < totalSites {
 238  			pattern[siteIdx] = SiteMark{
 239  				Index:      uint64(siteIdx),
 240  				Occupied:   true,
 241  				TypeTag:    tag,
 242  				Projection: proj,
 243  				Perm:       perm,
 244  				LockIn:     li,
 245  			}
 246  		} else {
 247  			// Site index out of range: append.
 248  			_ = i // consumed
 249  			pattern = append(pattern, SiteMark{
 250  				Index:      uint64(siteIdx),
 251  				Occupied:   true,
 252  				TypeTag:    tag,
 253  				Projection: proj,
 254  				Perm:       perm,
 255  				LockIn:     li,
 256  			})
 257  		}
 258  	}
 259  
 260  	// Sender fingerprint.
 261  	var fpHash Hamadryad
 262  	copy(fpHash[:], data[pos:pos+HamBytes])
 263  	pos += HamBytes
 264  
 265  	var permDist [6]int
 266  	for i := range 6 {
 267  		permDist[i] = int(binary.LittleEndian.Uint16(data[pos:]))
 268  		pos += 2
 269  	}
 270  
 271  	fp := SporeFingerprint{
 272  		Hash:     encodeHamadryadHex(fpHash),
 273  		PermDist: permDist,
 274  	}
 275  
 276  	return &EphemeralMessage{
 277  		Pattern:           pattern,
 278  		Signature:         sig,
 279  		SenderFingerprint: fp,
 280  	}, nil
 281  }
 282