encrypt.go raw

   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