The previous "encryption" wouldn't even be acceptable for an undergrad at TU/e, so we have to go back to the video series, and watch [Number 5, on Block Ciphers][1]. This lecture gives us a good idea how we can get around the "ECB Penguin problem" by using a Feistel Cipher (not a Steifel~Stifle cipher!). Again, we big [up the MWE to the cloud][2], and refer here only to the output functions.
The basic idea is to make secret key encryption multilayered. We happen to have two actively symmetric channels, so Feistel Ciphering is a natural choice. The [flow chart on wikipedia][3] is a little more detailed than in Tanja's lecture, but our approach is slightly different. By splitting function F into two parts, and by allowing both addition and subtraction mod 5, we can write a fully symmetric iterator. First we need a way to stream keys, starting from a chosen key. This is not entirely trivial because any function through key space can land on a different symmetry type, but fortunately symmetry types are evenly distributed in keyspace.
ItKeys[key_, nInd_, sym_] := Map[With[
{cand = Select[Mod[Floor[key Pi^# + Range[8]], 2^160],
SameQ[sym, Mod[{#, Floor[#/2]}, 2]] &]},
If[EvenQ[nInd], Max[cand], Min[cand]]] &, Range[0, nInd - 1]]
So we just jump forward by a factor about
$\pi^n$ and look in a window of size
$8$, always finding two keys of each symmetry type and just choose one per
$n$. Nothing to it, but perhaps not the most elusive way to choose a key stream.
Once the keys are known, we can use them to generate autoglyphs as the Feistel cipher iterates. We ususally can take two pads
$P_1, P_2$ from each
$n^{th}$ autoglyph, then compute out signals
$S_1'=S_1 \oplus S_2 \oplus P_1$ and
$S_2' = S_1 \ominus S_2 \oplus P_2$ modulo
$5$ from in signals
$S_1$ and
$S_2$. Both of these equations are invertible,
$S_1 = (S_1'\oplus S_2'\ominus P_1\ominus P_2 )/2$ and
$S_2 = (S_1'\ominus S_2'\ominus P_1\oplus P_2 )/2$. In code it looks like this:
(*left & right get different pads / / sort of.*)
EncodeRound[sig1_, sig2_, AGmat_, sym_] := If[SameQ[sig1, sig2],
Mod[{#, #}, 5] &@(sig1 + AGmat[[1 ;; 32, 1 ;; 32]]),
With[{cc = CopyCodes[sym][[2, 1]]}, Mod[
{Plus[Mod[sig1 + sig2, 5], AGmat[[1 ;; 32, 1 ;; 32]]],
Plus[Mod[sig1 - sig2, 5], Switch[cc,
{0, _}, Reverse /@ #, {1, 0}, Reverse@#, {x, x}, #
][[1 ;; 32, 1 ;; 32]] &@AGmat]}, 5]]]
Div2 = MapThread[Rule, {Mod[2 Range[0, 4], 5], Range[0, 4]}];
DecodeRound[sig1_, sig2_, AGmat_, sym_] := If[SameQ[sig1, sig2],
Mod[{#, #}, 5] &@(sig1 - AGmat[[1 ;; 32, 1 ;; 32]]),
With[{cc = CopyCodes[sym][[2, 1]]}, ReplaceAll[
Mod[{#[[1]] + #[[2]], #[[1]] - #[[2]]}, 5], Div2] &@
Mod[{Subtract[sig1, AGmat[[1 ;; 32, 1 ;; 32]]],
Subtract[sig2, Switch[cc,
{0, _}, Reverse /@ #, {1, 0}, Reverse@#, {x, x}, #
][[1 ;; 32, 1 ;; 32]] &@AGmat]}, 5]]]
(with some care taken for division by 2 mod 5). In the case with full square dihedral symmetry, we do not have two equivalent channels, so it is not a true Feistel cipher. However, we still get multiple autoglyph layers, which seem to produce an acceptable result as long as recursion depth is chosen greater than
$2$:
Sig32s = SquareEncode /@ BigramMatrices;
Grid[Transpose[Outer[Im256@SymEncode[
Sig32s[[3]], Sig32s[[3]], CharacteristicSeeds[[#1]],
{1, 1}, #2] &, {7, 8}, {0, 1, 2, 3}]], Frame -> All]
![OO encodes][4]
Grid[Transpose[Outer[Show[ResultsBW[{SquareDecode[#]}] & /@ SymDecode[
SymEncode[Sig32s[[3]], Sig32s[[3]],
CharacteristicSeeds[[#1]], ref[[#1]], #2],
CharacteristicSeeds[[#1]], ref[[#1]], 0],
ImageSize -> 128] &, {7, 8}, Range[0, 4]]], Frame -> All]
![OO decodes][5]
The second row with only one layer of padding looks very similar to what we saw previously. As the stream goes on, output encodes become more dense with color, and corresponding decodes look like random binary fields. That is what we want.
It's not that difficult to run up four layers, certainly in less than 1s per encrypt call:
AbsoluteTiming[encodes = MapThread[SymEncode[Sig32s[[1]], Sig32s[[2]], #1, #2, 4] &,
{CharacteristicSeeds[[1 ;; 6]], ref[[1 ;; 6]]}];]
Out[]=l.t. 0.5 s
Grid[Transpose[Partition[Im256 /@ encodes, 2]], Frame -> All]
![LOVE encodes][6]
What do the decodes look like? Unless all keys in the stream are known the decodes look random:
Grid[Function[{recd}, MapThread[
ResultsBW[SquareDecode /@ SymDecode[#1, #2, #3, recd]] &,
{encodes, CharacteristicSeeds[[1 ;; 6]], ref[[1 ;; 6]]}]] /@
Range[0, 4], Frame -> All]
![love decodes][7]
That's it! At least Naive decryption doesn't work, but obviously our minimal example isn't as complex or secure as [AES: Advanced Encryption Standard][8]. What is really missing (?) is an analysis whether or not stacking autoglyphs has a dense, pseudorandom limit. We can explore this possibility just by plotting a stream across
$n$ and its first integral:
AGStream = AutoGlyphM2D /@ ItKeys[CharacteristicSeeds[[5]], 5, ref[[5]]];
Grid[Transpose[{Im256 /@ AGStream,
Im256 /@ FoldList[Mod[Plus[#1, #2], 5] &, AGStream]}
], Frame -> All]
![ag stream][9]
The patterns seem different enough from one another, and end up producing something dense and intricate, but we don't know if it's approaching a desirable limit without taking Tanja's suggestion to apply diehard tests. In this case we don't really care, because We aren't trying to turn Eve into a Rudolph joke just in time for the holidays. If Eve wants to print off a Crypto Christmas Card and give it to herself, she's more than welcome to. Just please make an artistic choice of your own preferred secret key value and recursion depth.