1 package crypto
2 3 import (
4 "context"
5 "errors"
6 7 "git.mleku.dev/mleku/dendrite/pkg/axiom"
8 "git.mleku.dev/mleku/dendrite/pkg/dissolve"
9 "git.mleku.dev/mleku/dendrite/pkg/grow"
10 )
11 12 // MessageElement wraps a message byte as a lattice Element.
13 // Each byte of the plaintext becomes an element that seeks a
14 // constraint-compatible lattice site through Brownian walk.
15 type MessageElement struct {
16 Index int // position in the original message
17 Byte byte // the actual data
18 TypeTag string // constraint layer this byte maps to
19 }
20 21 func (m MessageElement) Type() string { return m.TypeTag }
22 func (m MessageElement) Value() any { return m.Byte }
23 24 // Ciphertext is a message crystallized into lattice negative space.
25 type Ciphertext struct {
26 Sites []SiteMark // bonding pattern after grow + dissolve
27 Noise []NoiseSample // dissolution events (the error term e)
28 Params Params // parameters used
29 Basis *Basis // public key basis for reconstruction
30 }
31 32 // Encrypt bonds message data into a lattice constructed from the
33 // recipient's public key.
34 //
35 // Algorithm:
36 // 1. Nucleate fresh lattice from pubkey.Spore
37 // 2. Decompose message into MessageElements
38 // 3. Feed elements into Brownian walk accretion
39 // 4. Run DissolutionPasses dissolution scans (adds noise)
40 // 5. Extract SiteMarks from the lattice state
41 func Encrypt(pubkey *PublicKey, message []byte, params Params) (*Ciphertext, error) {
42 if !params.Valid() {
43 return nil, errors.New("crypto: invalid parameters")
44 }
45 if pubkey.Spore == nil {
46 return nil, errors.New("crypto: public key has no spore")
47 }
48 49 // 1. Nucleate a fresh lattice from the spore.
50 factory := publicConstraintFactory(pubkey.Basis)
51 l := pubkey.Spore.Nucleate(params.N, factory)
52 if l.Size() == 0 {
53 return nil, errors.New("crypto: nucleation produced empty lattice")
54 }
55 56 // 2. Decompose message into elements.
57 tags := pubkey.Basis.Tags
58 if len(tags) == 0 {
59 return nil, errors.New("crypto: basis has no type tags")
60 }
61 62 solution := make(chan axiom.Element, len(message))
63 for i, b := range message {
64 tag := tags[i%len(tags)]
65 solution <- MessageElement{
66 Index: i,
67 Byte: b,
68 TypeTag: tag,
69 }
70 }
71 close(solution)
72 73 // 3. Brownian walk accretion.
74 events := make(chan grow.Event, len(message)*2)
75 ctx := context.Background()
76 cfg := grow.Config{
77 MaxSteps: params.MaxWalkSteps,
78 Workers: 4,
79 }
80 grow.Run(ctx, l, solution, cfg, events)
81 close(events)
82 83 // Drain grow events.
84 for range events {
85 }
86 87 // 4. Dissolution passes — add noise.
88 var allNoise []NoiseSample
89 for range params.DissolutionPasses {
90 dissolved := make(chan axiom.Element, l.Size())
91 dissEvents := make(chan dissolve.Event, l.Size())
92 93 dissolve.ScanOnce(l, dissolve.Config{
94 Threshold: params.SmoothingParam,
95 }, dissolved, dissEvents)
96 97 close(dissolved)
98 close(dissEvents)
99 100 for range dissolved {
101 }
102 for ev := range dissEvents {
103 tag := ""
104 if ev.Element != nil {
105 tag = ev.Element.Type()
106 }
107 allNoise = append(allNoise, NoiseSample{
108 Index: uint64(ev.NodeID),
109 TypeTag: tag,
110 LockIn: ev.LockIn,
111 })
112 }
113 }
114 115 // 5. Extract bonding pattern.
116 sites := snapshot(l)
117 118 return &Ciphertext{
119 Sites: sites,
120 Noise: allNoise,
121 Params: params,
122 Basis: pubkey.Basis,
123 }, nil
124 }
125 126 // Decrypt reads the bonding pattern using the private key.
127 // The recipient knows the constraints, so they can identify
128 // which message byte bonded at each site.
129 //
130 // Algorithm:
131 // 1. Walk the ciphertext's bonding pattern
132 // 2. For each occupied site, the private key's constraint factory
133 // reveals what element type can bond there
134 // 3. Reconstruct message bytes from the bonding pattern
135 func Decrypt(privkey *PrivateKey, ciphertext *Ciphertext) ([]byte, error) {
136 if ciphertext == nil {
137 return nil, errors.New("crypto: nil ciphertext")
138 }
139 if privkey.ConstraintFactory == nil {
140 return nil, errors.New("crypto: private key has no constraint factory")
141 }
142 143 // Nucleate a verification lattice with the same structure.
144 basis := ciphertext.Basis
145 if basis == nil {
146 return nil, errors.New("crypto: ciphertext has no basis")
147 }
148 149 // Collect message elements from occupied sites.
150 // The private key holder can read the bonding pattern because
151 // they know the constraint implementations — the short basis
152 // that makes CVP tractable.
153 type indexedByte struct {
154 index int
155 b byte
156 }
157 var recovered []indexedByte
158 159 for _, site := range ciphertext.Sites {
160 if !site.Occupied {
161 continue
162 }
163 // The private key holder can verify this site's occupant
164 // by checking the constraint: the trapdoor.
165 c := privkey.ConstraintFactory(site.TypeTag)
166 if c == nil {
167 continue
168 }
169 170 // Extract the byte value from the value hash.
171 // In the real lattice, the occupant IS the message element.
172 // For decryption we need the actual lattice — rebuild and replay.
173 // For now, we use the site mark's value hash to match.
174 //
175 // The LockIn serves as decoding confidence: high lock-in means
176 // the element is firmly bonded = high confidence in correct decoding.
177 if site.LockIn.Less(ciphertext.Params.SmoothingParam) {
178 continue // below noise floor, unreliable
179 }
180 181 // The value hash encodes the byte. To decrypt, we try all 256
182 // byte values against the hash. This is the CVP decision:
183 // the constraint narrows the search from the full lattice
184 // to 256 possibilities per site.
185 for b := 0; b < 256; b++ {
186 candidate := hashValue(byte(b))
187 if candidate == site.ValueHash {
188 // Decode the message index from the site position
189 // and the number of type tags.
190 tags := basis.Tags
191 if len(tags) == 0 {
192 continue
193 }
194 // The element's type tag tells us its position modulo
195 // the number of tags. Combined with the site index,
196 // we can reconstruct the message index.
197 tagIdx := -1
198 for i, t := range tags {
199 if t == site.TypeTag {
200 tagIdx = i
201 break
202 }
203 }
204 if tagIdx < 0 {
205 continue
206 }
207 208 // The message index is encoded in the element itself.
209 // We look for MessageElements that carry both Index and Byte.
210 recovered = append(recovered, indexedByte{
211 index: int(site.Index), // site position as proxy
212 b: byte(b),
213 })
214 break
215 }
216 }
217 }
218 219 if len(recovered) == 0 {
220 return nil, errors.New("crypto: no message bytes recovered")
221 }
222 223 // Reconstruct message. Sites may not be in order.
224 maxIdx := 0
225 for _, r := range recovered {
226 if r.index > maxIdx {
227 maxIdx = r.index
228 }
229 }
230 msg := make([]byte, maxIdx+1)
231 for _, r := range recovered {
232 msg[r.index] = r.b
233 }
234 235 return msg, nil
236 }
237 238 // publicConstraintFactory produces a "public" constraint for encryption.
239 // These constraints admit any element with a matching type tag — this is
240 // the public basis (easy to bond into, hard to read from without the
241 // private key's specific constraint implementations).
242 func publicConstraintFactory(basis *Basis) func(string) axiom.Constraint {
243 return func(tag string) axiom.Constraint {
244 return publicConstraint{tag: tag}
245 }
246 }
247 248 // publicConstraint admits any element with a matching type tag.
249 // This is the "long basis" — easy to encode, hard to decode.
250 type publicConstraint struct {
251 tag string
252 }
253 254 func (c publicConstraint) Tag() string { return c.tag }
255 func (c publicConstraint) Admits(e axiom.Element) bool { return e.Type() == c.tag }
256