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