| """neural_ca -- a reversible cellular automaton as a perfect cipher. |
| |
| The Margolus block rule is a bijection of the lattice, so iterating it forward |
| diffuses a bitmap into noise and iterating the exact same rule backward restores |
| it bit-for-bit. That makes the automaton a block cipher whose key is the pair |
| (number of steps, starting partition phase): the right key inverts the diffusion |
| perfectly, and a key off by a single step returns noise. No information is ever |
| destroyed, so decryption is exact rather than approximate. |
| |
| python demos/neural_ca_reversible_cipher.py |
| """ |
| import os, sys |
| HERE = os.path.dirname(os.path.abspath(__file__)) |
| REPO = os.path.dirname(HERE) |
| sys.path.insert(0, os.path.join(REPO, "src")) |
| import ca |
|
|
| |
| ART = [ |
| " ### ### ", |
| " ##### ##### ", |
| "#############", |
| "#############", |
| "#############", |
| " ########### ", |
| " ######### ", |
| " ####### ", |
| " ##### ", |
| " ### ", |
| " # ", |
| ] |
| PAD_X, PAD_Y = 6, 4 |
|
|
|
|
| def make_grid(): |
| |
| h = len(ART) + 2 * PAD_Y |
| w = len(ART[0]) + 2 * PAD_X |
| h += h & 1 |
| w += w & 1 |
| g = [[0] * w for _ in range(h)] |
| for y, row in enumerate(ART): |
| for x, ch in enumerate(row): |
| if ch == "#": |
| g[y + PAD_Y][x + PAD_X] = 1 |
| return g |
|
|
|
|
| def render(g): |
| return "\n".join("".join("#" if c else "." for c in row) for row in g) |
|
|
|
|
| def hamming(a, b): |
| return sum(a[y][x] != b[y][x] for y in range(len(a)) for x in range(len(a[0]))) |
|
|
|
|
| if __name__ == "__main__": |
| plain = make_grid() |
| KEY_STEPS, KEY_PHASE = 200, 0 |
|
|
| cipher = ca.run(plain, KEY_STEPS, KEY_PHASE) |
| recovered = ca.run_back(cipher, KEY_STEPS, KEY_PHASE) |
| wrong = ca.run_back(cipher, KEY_STEPS - 1, KEY_PHASE) |
|
|
| n = sum(map(sum, plain)) |
| print("neural_ca: reversible-automaton cipher") |
| print("=" * 46) |
| print(f"key = ({KEY_STEPS} steps, phase {KEY_PHASE}); {n} set bits (conserved " |
| f"throughout: {sum(map(sum, cipher)) == n})\n") |
|
|
| print("PLAINTEXT:") |
| print(render(plain)) |
| print(f"\nCIPHERTEXT after {KEY_STEPS} forward steps (diffused to noise):") |
| print(render(cipher)) |
| print(f"\nDECRYPTED with the correct key:") |
| print(render(recovered)) |
| print(f" exact recovery: {recovered == plain} (Hamming distance 0)") |
| print(f"\nDECRYPTED with a wrong key (off by one step):") |
| print(render(wrong)) |
| print(f" still scrambled: {hamming(wrong, plain)} of {len(plain)*len(plain[0])} " |
| f"cells differ from the plaintext") |
| print("\nEvery step is a bijection, so the ciphertext holds exactly the") |
| print("information of the plaintext -- no more, no less. Only the exact") |
| print("reverse trajectory recovers it.") |
|
|