shadow_encrypt.go raw

   1  package crypto
   2  
   3  import (
   4  	"context"
   5  	"crypto/rand"
   6  	"encoding/binary"
   7  	"errors"
   8  	"fmt"
   9  
  10  	"golang.org/x/crypto/chacha20"
  11  
  12  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
  13  	"git.mleku.dev/mleku/dendrite/pkg/dissolve"
  14  	"git.mleku.dev/mleku/dendrite/pkg/epoch"
  15  	"git.mleku.dev/mleku/dendrite/pkg/grow"
  16  	"git.mleku.dev/mleku/dendrite/pkg/state"
  17  )
  18  
  19  // ShadowEncrypt encrypts plaintext using the binary/decimal shadow channel.
  20  //
  21  // The plaintext is converted to hexagram tokens, each independently
  22  // permuted by an S_3 pair derived from the epoch Phase() at that token's
  23  // position. The permuted tokens are then crystallized into a lattice
  24  // nucleated from the recipient's public key.
  25  //
  26  // The encryption is structurally dependent on the binary/decimal
  27  // incommensurability: the inner trigram rotates by the binary clock
  28  // component of the phase, and the outer trigram rotates by the decimal
  29  // clock component. Neither projection alone recovers the original.
  30  //
  31  // Algorithm:
  32  //  1. Convert plaintext to hexagram tokens (state.EncodeBytes)
  33  //  2. For each token, compute phase-derived S_3 pair and apply
  34  //  3. Bond shadow elements into recipient's lattice
  35  //  4. Run dissolution passes (add noise)
  36  //  5. XOR bonding data with phase-derived keystream
  37  //  6. Extract ciphertext
  38  func ShadowEncrypt(
  39  	pubkey *PublicKey,
  40  	plaintext []byte,
  41  	params Params,
  42  	ep epoch.Epoch,
  43  	nonce []byte,
  44  ) (*ShadowCiphertext, error) {
  45  	if !params.Valid() {
  46  		return nil, errors.New("crypto: invalid parameters")
  47  	}
  48  	if pubkey == nil || pubkey.Spore == nil {
  49  		return nil, errors.New("crypto: public key has no spore")
  50  	}
  51  	if len(nonce) == 0 {
  52  		return nil, errors.New("crypto: empty nonce")
  53  	}
  54  
  55  	// 1. Hexagram encoding: 3 bytes → 4 tokens (6-bit each).
  56  	tokens := state.EncodeBytes(plaintext)
  57  	if len(tokens) == 0 && len(plaintext) > 0 {
  58  		return nil, errors.New("crypto: hexagram encoding failed")
  59  	}
  60  
  61  	// 2. Phase-dependent shadow decomposition.
  62  	encrypted := make([]state.Hexagram, len(tokens))
  63  	for i, tok := range tokens {
  64  		binP, decP := PhasePerms(ep, i)
  65  		encrypted[i] = ShadowDecompose(tok, binP, decP)
  66  	}
  67  
  68  	// 3. Build shadow elements for lattice bonding.
  69  	factory := publicConstraintFactory(pubkey.Basis)
  70  	l := pubkey.Spore.Nucleate(params.N, factory)
  71  	if l.Size() == 0 {
  72  		return nil, errors.New("crypto: nucleation produced empty lattice")
  73  	}
  74  
  75  	tags := pubkey.Basis.Tags
  76  	if len(tags) == 0 {
  77  		return nil, errors.New("crypto: basis has no type tags")
  78  	}
  79  
  80  	solution := make(chan axiom.Element, len(encrypted))
  81  	for i, enc := range encrypted {
  82  		vertex, key := PhaseProjection(ep, i)
  83  		binP, _ := PhasePerms(ep, i)
  84  		solution <- ShadowElement{
  85  			Index:      i,
  86  			Encrypted:  enc,
  87  			ProjVertex: vertex,
  88  			ProjKey:    key,
  89  			ProjPath:   uint16(i),
  90  			PermIdx:    uint8(binP),
  91  			TypeTag:    tags[i%len(tags)],
  92  		}
  93  	}
  94  	close(solution)
  95  
  96  	// 4. Brownian walk accretion.
  97  	events := make(chan grow.Event, len(encrypted)*2)
  98  	ctx := context.Background()
  99  	grow.Run(ctx, l, solution, grow.Config{
 100  		MaxSteps: params.MaxWalkSteps,
 101  		Workers:  4,
 102  	}, events)
 103  	close(events)
 104  	for range events {
 105  	}
 106  
 107  	// 5. Dissolution passes — add noise.
 108  	var allNoise []NoiseSample
 109  	for range params.DissolutionPasses {
 110  		dissolved := make(chan axiom.Element, l.Size())
 111  		dissEvents := make(chan dissolve.Event, l.Size())
 112  		dissolve.ScanOnce(l, dissolve.Config{
 113  			Threshold: params.SmoothingParam,
 114  		}, dissolved, dissEvents)
 115  		close(dissolved)
 116  		close(dissEvents)
 117  		for range dissolved {
 118  		}
 119  		for ev := range dissEvents {
 120  			tag := ""
 121  			if ev.Element != nil {
 122  				tag = ev.Element.Type()
 123  			}
 124  			allNoise = append(allNoise, NoiseSample{
 125  				Index:   uint64(ev.NodeID),
 126  				TypeTag: tag,
 127  				LockIn:  ev.LockIn,
 128  			})
 129  		}
 130  	}
 131  
 132  	// 6. Extract bonding pattern and apply keystream masking.
 133  	sites := snapshot(l)
 134  
 135  	// Generate per-message nonce hash.
 136  	var nonceBuf []byte
 137  	nonceBuf = append(nonceBuf, []byte("shadow-keystream-v1")...)
 138  	nonceBuf = append(nonceBuf, []byte(pubkey.SporeHash)...)
 139  	nonceBuf = append(nonceBuf, []byte(ep.String())...)
 140  	nonceBuf = append(nonceBuf, nonce...)
 141  	nonceHash := Hash(nonceBuf)
 142  
 143  	// XOR value hashes with keystream for additional masking.
 144  	// The keystream is keyed by the site's lattice node ID, not its
 145  	// position in the slice, so the masking survives reordering.
 146  	for i := range sites {
 147  		if sites[i].Occupied {
 148  			block := keystreamForSite(nonceHash, sites[i].Index)
 149  			sites[i].ValueHash = xorHamadryad(sites[i].ValueHash, block)
 150  		}
 151  	}
 152  
 153  	return &ShadowCiphertext{
 154  		Sites:      sites,
 155  		Noise:      allNoise,
 156  		Params:     params,
 157  		Basis:      pubkey.Basis,
 158  		Nonce:      nonceHash,
 159  		EpochDec:   ep.DecExp,
 160  		EpochBin:   ep.BinExp,
 161  		TokenCount: len(tokens),
 162  		OrigLen:    len(plaintext),
 163  	}, nil
 164  }
 165  
 166  // ShadowDecrypt recovers plaintext from a shadow ciphertext using the
 167  // private key and the matching epoch.
 168  //
 169  // Algorithm:
 170  //  1. Unmask the bonding pattern using the nonce keystream
 171  //  2. Read bonded values via private key (CVP trapdoor)
 172  //  3. Recompute phase schedule from epoch
 173  //  4. Apply inverse S_3 pairs to recover original hexagrams
 174  //  5. Decode hexagrams to bytes
 175  func ShadowDecrypt(
 176  	privkey *PrivateKey,
 177  	ct *ShadowCiphertext,
 178  	ep epoch.Epoch,
 179  ) ([]byte, error) {
 180  	if ct == nil {
 181  		return nil, errors.New("crypto: nil shadow ciphertext")
 182  	}
 183  	if privkey == nil || privkey.ConstraintFactory == nil {
 184  		return nil, errors.New("crypto: private key has no constraint factory")
 185  	}
 186  
 187  	// 1. Unmask value hashes.
 188  	// The keystream is keyed by lattice node ID, matching the encrypt side.
 189  	sites := make([]SiteMark, len(ct.Sites))
 190  	copy(sites, ct.Sites)
 191  	for i := range sites {
 192  		if sites[i].Occupied {
 193  			block := keystreamForSite(ct.Nonce, sites[i].Index)
 194  			sites[i].ValueHash = xorHamadryad(sites[i].ValueHash, block)
 195  		}
 196  	}
 197  
 198  	// 2. Recover encrypted hexagram values from the bonding pattern.
 199  	// The private key holder can verify each site's occupant by
 200  	// testing all 64 hexagram values against the value hash.
 201  	type indexedHex struct {
 202  		index int
 203  		hex   state.Hexagram
 204  	}
 205  	var recovered []indexedHex
 206  
 207  	for _, site := range sites {
 208  		if !site.Occupied {
 209  			continue
 210  		}
 211  		if site.LockIn.Less(ct.Params.SmoothingParam) {
 212  			continue // below noise floor
 213  		}
 214  
 215  		// Test all 64 hexagram values against the stored hash.
 216  		for h := range 64 {
 217  			candidate := hashValue(state.Hexagram(h))
 218  			if candidate == site.ValueHash {
 219  				// Use ProjPath as the token stream index.
 220  				// ShadowEncrypt stores the token index as ProjectionPath
 221  				// in each ShadowElement.
 222  				recovered = append(recovered, indexedHex{
 223  					index: int(site.ProjPath),
 224  					hex:   state.Hexagram(h),
 225  				})
 226  				break
 227  			}
 228  		}
 229  	}
 230  
 231  	if len(recovered) == 0 {
 232  		return nil, errors.New("crypto: no shadow tokens recovered")
 233  	}
 234  
 235  	// 3. Reconstruct token stream, applying inverse phase permutations.
 236  	tokens := make([]state.Hexagram, ct.TokenCount)
 237  	for _, r := range recovered {
 238  		tokenIdx := r.index
 239  		if tokenIdx >= ct.TokenCount {
 240  			continue
 241  		}
 242  
 243  		// Apply inverse phase permutations.
 244  		binP, decP := PhasePerms(ep, tokenIdx)
 245  		tokens[tokenIdx] = ShadowRecompose(r.hex, binP, decP)
 246  	}
 247  
 248  	// 4. Decode hexagrams back to bytes.
 249  	return state.DecodeHexagrams(tokens, ct.OrigLen), nil
 250  }
 251  
 252  // keystreamForSite generates a Hamadryad-sized keystream block for a
 253  // specific lattice site using ChaCha20.
 254  //
 255  // The ChaCha20 key is derived from the nonce via a single Hamadryad hash
 256  // (one-time key derivation, not used as a PRF). The site index selects
 257  // the ChaCha20 block counter, giving each site an independent keystream
 258  // block without relying on SWIFFT's unproven PRF properties.
 259  func keystreamForSite(nonce Hamadryad, siteIndex uint64) Hamadryad {
 260  	// Derive ChaCha20 key from nonce (one-time hash, proven secure).
 261  	keyHash := Hash(append([]byte("shadow-chacha20-key"), nonce[:]...))
 262  	var key [chacha20.KeySize]byte
 263  	copy(key[:], keyHash[:chacha20.KeySize])
 264  
 265  	// Derive ChaCha20 nonce from site index.
 266  	var chachaNonce [chacha20.NonceSize]byte
 267  	binary.LittleEndian.PutUint64(chachaNonce[:8], siteIndex)
 268  
 269  	cipher, _ := chacha20.NewUnauthenticatedCipher(key[:], chachaNonce[:])
 270  	var result Hamadryad
 271  	cipher.XORKeyStream(result[:], result[:]) // XOR zeros = raw keystream
 272  	return result
 273  }
 274  
 275  // xorHamadryad XORs two Hamadryad values byte-by-byte.
 276  func xorHamadryad(a, b Hamadryad) Hamadryad {
 277  	var result Hamadryad
 278  	for i := range HamBytes {
 279  		result[i] = a[i] ^ b[i]
 280  	}
 281  	return result
 282  }
 283  
 284  // generateNonce creates a cryptographically random nonce.
 285  func generateNonce() []byte {
 286  	nonce := make([]byte, 32)
 287  	if _, err := rand.Read(nonce); err != nil {
 288  		panic(fmt.Sprintf("crypto/rand: %v", err))
 289  	}
 290  	return nonce
 291  }
 292