I was sitting at my dining table the other day, when I took a look at the ever-present chessboard. It's a very nice board we got from an estate sale, with pewter pieces. But that's besides the point. There was a partially completed game on that board, and I started to wonder if I could figure out the game from the outcome. Naturally, that led me to think about one-way functions, specifically, algorithms like SHA. So, could I create a valid chessboard for any file to use as a cryptographic hash?


As I was feeling lazy, I decided to use a PyPi package for the chess movements and just go from there.

I started out with some very simple code to read every byte of a file, and select a move from a list of legal moves, and repeat.

import chess
data = b'Sample data to hash'
board = chess.Board()
for byte in data:
    moves = list(board.legal_moves)
    index = byte % len(moves)
    board.push(moves[index])
print(board)

This gave me the following chessboard:

First Chessboard

Anyone with a trained eye for chess can tell you that this is a bad position for both sides to be in. This is due to how the game wasn't played with strategy, but instead with random information.

Now, let's try hashing something larger, like a file. For this example, I'm going to use the SVG of the board above.

import chess
with open("board0.svg", "rb") as f:
    data = f.read()
    f.close()
board = chess.Board()
for byte in data:
    moves = list(board.legal_moves)
    index = byte % len(moves)
    board.push(moves[index])
print(board)

This will give us a new board:

Second Chessboard

Hey, the game is over! It's a stalemate! A chess game can only have a finite number of moves before it finishes. So how do we fix that? Simple, by relaxing the requirements a little.

Can I create a valid chessboard for any file to use as a cryptographic hash?

Of course, that would lead to just encoding a SHA sum as a base13 (six unique pieces on each side, and a blank square) and putting it over 64 bytes. So, let's stick with the spirit of the challenge. If a game has finished, can we 1) create a new game and 2) merge all the games together?

Creating a new game is easy enough. Just check if the game is finished and then append the board to a list.

board = chess.Board()
games = []
for byte in data:
    moves = list(board.legal_moves)
    if board.is_game_over():
        games.append(board)
        board = chess.Board()
        moves = list(board.legal_moves)
    index = byte % len(moves)
    board.push(moves[index])
games.append(board)

So now, we have many boards! Now all that's left to do is to merge the boards together. We know that there are thirteen possible states for every square, so we can just XOR the values together.

chars = ['.', 'B', 'K', 'N', 'P', 'Q', 'R', 'b', 'k', 'n', 'p', 'q', 'r']

def merge(a, b):
    output = []
    if len(a) != len(b):
        raise RuntimeError("Strings must be of the same length")
    for i in range(0,len(a)):
        output.append(
            chars[
                (chars.index(a[i]) ^ chars.index(b[i])) % len(chars)
            ]
        )
    return "".join(output)

This will return an ASCII board with no newlines or spaces. Let's put together the code we have, and make it able to read a file from sys.argv.

import chess
import sys

chars = ['.', 'B', 'K', 'N', 'P', 'Q', 'R', 'b', 'k', 'n', 'p', 'q', 'r']

def merge(a, b):
    output = []
    if len(a) != len(b):
        raise RuntimeError("Strings must be of the same length")
    for i in range(0,len(a)):
        output.append(
            chars[
                (chars.index(a[i]) ^ chars.index(b[i])) % len(chars)
            ]
        )
    return "".join(output)

def hash(data) -> str:
    board = chess.Board()
    games = []
    for byte in data:
        moves = list(board.legal_moves)
        if board.is_game_over():
            games.append(board)
            board = chess.Board()
            moves = list(board.legal_moves)
        index = byte % len(moves)
        board.push(moves[index])
    games.append(board)
    # Merge together boards
    output = ""
    for game in games:
        if output == "":
            output = str(game).replace(" ", "").replace("\n", "")
        else:
            output = merge(output, str(game).replace(" ", "").replace("\n", ""))
    return output

if __name__ == "__main__":
    with open(sys.argv[1], "rb") as f:
        data = f.read()
        f.close()
    print(hash(data))

Hashing the first board again gives us this as our hash: qnQpKQN.pnKQBkkrBNpnnpKbpN.NBkKNK.QKPkrp.KQkpbpBqb.QKpkKB.NNKnQR. Represented as something visible, we have the following board:

Third Chessboard

Obviously, this isn't a valid board, but it works! We can hash data into a chessboard, and you can't tell what the data was earlier

As for performance, well, to hash the first board SVG, it took 4.25s user 0.02s system 99% cpu 4.279 total, whereas sha256sum took 0.00s user 0.00s system 79% cpu 0.001 total. Our algorithm is many orders of magnitude more inefficient, but if you want to secretly send information, maybe a SHA hex string isn't the way to go, and you can hide it instead.

Previous Post