Steganographic Encryption

Hide secrets inside
chess games

Transform any message into a sequence of chess moves using HMAC-keyed steganography. The encrypted game looks indistinguishable from a real chess match.

Try the Cipher

Encrypt a message or decode a .cpgn file

abcd efgh
8765 4321
β™œ

Paste a PGN and key to decrypt

Algorithm Deep Dive

The mathematics behind the cipher

β™Ÿ Bit Encoding

With n legal moves at a position, we can hide k = ⌊logβ‚‚(n)βŒ‹ bits per move. We only use the first 2ᡏ moves so the bit-to-index mapping is a clean bijection.

Legal movesk (bits)Used moves
20416
30416
35532
50532

♝ HMAC Secret Ordering

For a board position FEN and move m, compute HMAC-SHA256(key, FENβ€–m). Sorting by this value creates a position-specific, key-dependent permutation of moves.

score(move) = HMAC-SHA256( key = "your-secret", data = FEN + "|" + move.uci() ) moves.sort(by=score)

β™› Multi-game Support

If a game reaches a position with fewer than 2 legal moves (checkmate, stalemate, or only 1 option) before all bits are encoded, a new game automatically starts from the initial position.

while bits_remaining: if usable_moves == 0: save_game() start_new_game() continue play_move(encode(next_bits))

β™œ Message Header

The first 32 bits encode the message byte-length as a big-endian unsigned integer. During decryption, this tells the decoder exactly how many bits to extract β€” preventing padding artifacts.

bits = format(len(msg), "032b") + "".join(format(b,"08b") for b in msg.encode()) # Decoding stops at: stop = 32 + length * 8
⚑
Entropy efficiency: A standard opening position has 20 legal moves, allowing 4 bits per move. An average chess position has ~30 legal moves (4 bits). Midgame tactical positions with 35+ moves reach 5 bits/move. A short message of 50 characters (432 bits including header) typically fits in ~100 moves β€” a realistic game length.