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