shadow_encrypt_test.go raw

   1  package crypto
   2  
   3  import (
   4  	"bytes"
   5  	"testing"
   6  
   7  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
   8  	"git.mleku.dev/mleku/dendrite/pkg/epoch"
   9  	"git.mleku.dev/mleku/dendrite/pkg/permutation"
  10  	"git.mleku.dev/mleku/dendrite/pkg/state"
  11  )
  12  
  13  // TestPhasePermsDeterministic verifies that PhasePerms produces the
  14  // same result for the same epoch and index across multiple calls.
  15  func TestPhasePermsDeterministic(t *testing.T) {
  16  	ep := epoch.Colony
  17  	for i := range 100 {
  18  		b1, d1 := PhasePerms(ep, i)
  19  		b2, d2 := PhasePerms(ep, i)
  20  		if b1 != b2 || d1 != d2 {
  21  			t.Errorf("index %d: PhasePerms not deterministic: (%d,%d) vs (%d,%d)",
  22  				i, b1, d1, b2, d2)
  23  		}
  24  	}
  25  }
  26  
  27  // TestPhasePermsRange verifies that PhasePerms always returns valid
  28  // S_3 permutation indices (0-5).
  29  func TestPhasePermsRange(t *testing.T) {
  30  	for _, ep := range []epoch.Epoch{epoch.Colony, epoch.CryptoWalk128, epoch.CryptoWalk256} {
  31  		for i := range int(ep.Period) {
  32  			b, d := PhasePerms(ep, i)
  33  			if b >= permutation.Count || d >= permutation.Count {
  34  				t.Errorf("epoch %s index %d: perm out of range: (%d,%d)", ep, i, b, d)
  35  			}
  36  		}
  37  	}
  38  }
  39  
  40  // TestShadowDecomposeRecompose verifies that ShadowDecompose followed
  41  // by ShadowRecompose is the identity for all hexagrams and permutation pairs.
  42  func TestShadowDecomposeRecompose(t *testing.T) {
  43  	for h := range 64 {
  44  		hex := state.Hexagram(h)
  45  		for bp := range permutation.Count {
  46  			for dp := range permutation.Count {
  47  				binP := permutation.Perm(bp)
  48  				decP := permutation.Perm(dp)
  49  				encrypted := ShadowDecompose(hex, binP, decP)
  50  				recovered := ShadowRecompose(encrypted, binP, decP)
  51  				if recovered != hex {
  52  					t.Errorf("hex=%d binP=%d decP=%d: decompose/recompose failed: got %d",
  53  						h, bp, dp, recovered)
  54  				}
  55  			}
  56  		}
  57  	}
  58  }
  59  
  60  // TestPhasePairIndexRoundTrip verifies that PhasePairIndex and
  61  // PairFromIndex are exact inverses for all 36 pairs across all
  62  // 36 possible home positions. This is the exhaustive check: every
  63  // projection must have an inverse that recovers the original.
  64  func TestPhasePairIndexRoundTrip(t *testing.T) {
  65  	for hb := range permutation.Count {
  66  		for hd := range permutation.Count {
  67  			homeBin := permutation.Perm(hb)
  68  			homeDec := permutation.Perm(hd)
  69  
  70  			// Forward: every pair must map to a unique index 0-35.
  71  			seen := make(map[uint8]bool)
  72  			for bp := range permutation.Count {
  73  				for dp := range permutation.Count {
  74  					binP := permutation.Perm(bp)
  75  					decP := permutation.Perm(dp)
  76  					idx := PhasePairIndex(binP, decP, homeBin, homeDec)
  77  					if idx > 35 {
  78  						t.Errorf("home=(%d,%d) pair=(%d,%d): index %d out of range",
  79  							hb, hd, bp, dp, idx)
  80  					}
  81  					if seen[idx] {
  82  						t.Errorf("home=(%d,%d) pair=(%d,%d): duplicate index %d",
  83  							hb, hd, bp, dp, idx)
  84  					}
  85  					seen[idx] = true
  86  				}
  87  			}
  88  
  89  			// All 36 indices must be used.
  90  			for i := range 36 {
  91  				if !seen[uint8(i)] {
  92  					t.Errorf("home=(%d,%d): index %d not produced by any pair", hb, hd, i)
  93  				}
  94  			}
  95  
  96  			// Inverse: PairFromIndex must recover the original pair.
  97  			for bp := range permutation.Count {
  98  				for dp := range permutation.Count {
  99  					binP := permutation.Perm(bp)
 100  					decP := permutation.Perm(dp)
 101  					idx := PhasePairIndex(binP, decP, homeBin, homeDec)
 102  					rb, rd := PairFromIndex(idx, homeBin, homeDec)
 103  					if rb != binP || rd != decP {
 104  						t.Errorf("home=(%d,%d) pair=(%d,%d) -> idx=%d -> (%d,%d): round-trip failed",
 105  							hb, hd, bp, dp, idx, rb, rd)
 106  					}
 107  				}
 108  			}
 109  		}
 110  	}
 111  }
 112  
 113  // TestShadowEncryptDecryptRoundTrip tests full encrypt/decrypt cycle.
 114  func TestShadowEncryptDecryptRoundTrip(t *testing.T) {
 115  	params := DefaultParams(Security128)
 116  	tags := []string{"alpha", "beta", "gamma"}
 117  
 118  	// Use the existing Generate function to create a keypair.
 119  	kp, err := Generate(params, tags, func(tag string) axiom.Constraint {
 120  		return publicConstraint{tag: tag}
 121  	})
 122  	if err != nil {
 123  		t.Fatalf("Generate: %v", err)
 124  	}
 125  
 126  	ep := epoch.Colony
 127  	nonce := []byte("test-nonce-unique-per-message")
 128  
 129  	// Test cases must fit within the lattice capacity: N=256 nodes
 130  	// shared across len(tags)=3 type layers. With hexagram encoding
 131  	// (3 bytes → 4 tokens), the max plaintext is roughly N*3/4 bytes,
 132  	// minus dissolution losses. Keep inputs small.
 133  	testCases := [][]byte{
 134  		{},
 135  		{0x42},
 136  		[]byte("hello"),
 137  		bytes.Repeat([]byte("shadow "), 5),
 138  	}
 139  
 140  	for i, plaintext := range testCases {
 141  		if len(plaintext) == 0 {
 142  			continue // empty plaintext handled separately
 143  		}
 144  
 145  		ct, err := ShadowEncrypt(&kp.Public, plaintext, params, ep, nonce)
 146  		if err != nil {
 147  			t.Errorf("case %d: ShadowEncrypt: %v", i, err)
 148  			continue
 149  		}
 150  
 151  		if ct.OrigLen != len(plaintext) {
 152  			t.Errorf("case %d: OrigLen = %d, want %d", i, ct.OrigLen, len(plaintext))
 153  		}
 154  		if ct.EpochDec != ep.DecExp || ct.EpochBin != ep.BinExp {
 155  			t.Errorf("case %d: epoch mismatch", i)
 156  		}
 157  
 158  		recovered, err := ShadowDecrypt(&kp.Private, ct, ep)
 159  		if err != nil {
 160  			t.Errorf("case %d: ShadowDecrypt: %v", i, err)
 161  			continue
 162  		}
 163  
 164  		if !bytes.Equal(recovered, plaintext) {
 165  			t.Errorf("case %d: recovered != plaintext\n  got:  %x\n  want: %x",
 166  				i, recovered, plaintext)
 167  		}
 168  	}
 169  }
 170  
 171  // TestShadowSemanticSecurity checks that encrypted hexagram tokens
 172  // have roughly uniform distribution for plaintext with varied content.
 173  //
 174  // Note: all-zero plaintext maps to hexagram 0 = (0,0,0)/(0,0,0),
 175  // and any S_3 permutation of identical elements is identity. So we
 176  // test with data that produces varied hexagram values to verify the
 177  // phase-dependent permutations actually disperse the distribution.
 178  func TestShadowSemanticSecurity(t *testing.T) {
 179  	ep := epoch.Colony
 180  
 181  	// Sequential bytes produce varied hexagram values.
 182  	plaintext := make([]byte, 300)
 183  	for i := range plaintext {
 184  		plaintext[i] = byte(i)
 185  	}
 186  	tokens := state.EncodeBytes(plaintext)
 187  
 188  	// Original distribution.
 189  	var origHist [64]int
 190  	for _, tok := range tokens {
 191  		origHist[tok&0x3F]++
 192  	}
 193  
 194  	// Encrypted distribution.
 195  	encrypted := make([]state.Hexagram, len(tokens))
 196  	var encHist [64]int
 197  	for i, tok := range tokens {
 198  		binP, decP := PhasePerms(ep, i)
 199  		encrypted[i] = ShadowDecompose(tok, binP, decP)
 200  		encHist[encrypted[i]&0x3F]++
 201  	}
 202  
 203  	// Check that encryption changes the distribution —
 204  	// at least some hexagrams should differ from originals.
 205  	changed := 0
 206  	for i, tok := range tokens {
 207  		if encrypted[i] != tok {
 208  			changed++
 209  		}
 210  	}
 211  	if changed == 0 {
 212  		t.Errorf("semantic security: no tokens were changed by phase permutation")
 213  	}
 214  
 215  	// Check that we see reasonable diversity in encrypted output.
 216  	distinct := 0
 217  	for _, count := range encHist {
 218  		if count > 0 {
 219  			distinct++
 220  		}
 221  	}
 222  	if distinct < 10 {
 223  		t.Errorf("semantic security: only %d distinct encrypted hexagram values", distinct)
 224  	}
 225  }
 226  
 227  // TestHexagramEncodingRoundTrip verifies state.EncodeBytes/DecodeHexagrams
 228  // for all single-byte values.
 229  func TestHexagramEncodingRoundTrip(t *testing.T) {
 230  	for b := range 256 {
 231  		data := []byte{byte(b)}
 232  		tokens := state.EncodeBytes(data)
 233  		recovered := state.DecodeHexagrams(tokens, 1)
 234  		if len(recovered) != 1 || recovered[0] != byte(b) {
 235  			t.Errorf("byte %d: round-trip failed: got %v", b, recovered)
 236  		}
 237  	}
 238  }
 239