💤

夢に出てきた暗号アルゴリズムを実装して検証する

に公開

はじめに

夢の中で暗号アルゴリズムを説明していました。「文字列を200列の行列に詰め込み、乱数ベクトルとの排他的論理和 (XOR) とスカラー縮約で暗号化する」という内容です。せっかくなので実装し、手順を技術記事にまとめます。
追記:エンディアンを判定してビッグエンディアン↔︎リトルエンディアンに変換してから上記のアルゴリズムを走らせるものだったのを思い出したのですが、復号不可のため追加の実施は取りやめておきます。

⚠️⚠️⚠️結果はうまくいきませんでしたので、気になる方のみ読んでください。⚠️⚠️⚠️

マシンスペック

MacBook Air M2 arm64
Docker上で実施

事前知識

アルゴリズム

  1. プレーンテキストをUTF‑8のバイト列に変換する。
  2. 列数をC=200に固定し、行数Rは (メッセージ長+C-1)
  3. 合計サイズがR×Cバイトになるまで乱数ダミーを追加してパディングする。
  4. パディング後のバイト列を行優先でR行×200列の2次元配列に並べ替える。
  5. 長さ200バイトの乱数ベクトルKを生成する(毎回新しく作る鍵)。
  6. 各行M_iと鍵Kを要素ごとにXORし、得られた行全体をさらにXORで折り畳み、1バイトのスカラーs_iに縮約する。
  7. スカラー列 [s_1, s_2, …, s_R] が暗号本体。鍵Kとセットで保存すれば復号が可能。
    復号時はこの手順を逆から辿り、最後にパディングを取り除きます。

復号時は逆順で処理し、パディングを除外して元の文字列を復元します。

実装方針

  • 言語: Python 3.11
  • 依存関係: なし(os, math, random 標準ライブラリのみ)
  • インタフェース: CLI 形式の encrypt.py / decrypt.py
  • 安全性: 夢の内容実現のため、性能・暗号強度よりも読みやすさを優先します。

準備(Docker)

FROM python:3.11-slim
WORKDIR /app

COPY dream_cipher.py encrypt.py decrypt.py .

ENTRYPOINT ["/bin/bash"]

ビルド & 起動

$ docker build -t dream-crypto .
$ docker run -it --rm dream-crypto

実装

ここでは 対称鍵方式で復号を可能にするために、乱数ではなく鍵 (seed) から擬似乱数列を生成し、その列と XOR する方式に変更します。XORは可逆なので、同じ鍵から同じ擬似乱数列を再生成すれば暗号文を元に戻せます。

dream_cipher.py

from hashlib import shake_256
from typing import Tuple
import math, os

BLOCK_COLS = 200  # 行列の列数(ブロック長)

def _derive_keystream(key: bytes, n_bytes: int) -> bytes:
    """鍵から必要バイト数ぶんの擬似乱数列を生成 (SHAKE‑256)"""
    return shake_256(key).digest(n_bytes)


def _pad(data: bytes) -> bytes:
    pad_len = (-len(data)) % BLOCK_COLS
    if pad_len == 0:
        pad_len = BLOCK_COLS
    padding = os.urandom(pad_len - 1) + bytes([pad_len])
    return data + padding


def _unpad(data: bytes) -> bytes:
    pad_len = data[-1]
    if pad_len < 1 or pad_len > BLOCK_COLS:
        raise ValueError("Invalid padding")
    return data[:-pad_len]

def encrypt(plaintext: str, key: bytes) -> bytes:
    """文字列 -> 暗号バイト列"""
    plain_bytes = _pad(plaintext.encode())
    ks = _derive_keystream(key, len(plain_bytes))
    cipher = bytes(a ^ b for a, b in zip(plain_bytes, ks))
    return cipher


def decrypt(ciphertext: bytes, key: bytes) -> str:
    """暗号バイト列 -> 復号文字列"""
    ks = _derive_keystream(key, len(ciphertext))
    plain_padded = bytes(a ^ b for a, b in zip(ciphertext, ks))
    plain = _unpad(plain_padded)
    return plain.decode()

encrypt.py

#!/usr/bin/env python3
import os, math, argparse, json, base64

COLUMNS = 200  # 列数 (固定)

def xor_reduce(data: bytes) -> int:
    """バイト列を XOR で 1 バイトへ縮約"""
    acc = 0
    for b in data:
        acc ^= b
    return acc

def encrypt(plaintext: str) -> dict:
    plain_bytes = plaintext.encode("utf-8")
    rows = math.ceil(len(plain_bytes) / COLUMNS)
    padded_len = rows * COLUMNS

    # パディング (疑似乱数)
    pad = os.urandom(padded_len - len(plain_bytes))
    full = plain_bytes + pad

    # 行列化
    matrix = [full[i*COLUMNS:(i+1)*COLUMNS] for i in range(rows)]

    # 鍵 (固定 200 バイト)
    key = os.urandom(COLUMNS)

    # 各行を暗号化しスカラーへ
    scalars = []
    for row in matrix:
        xored = bytes(a ^ b for a, b in zip(row, key))
        scalars.append(xor_reduce(xored))

    return {
        "rows": rows,
        "cipher": base64.b64encode(bytes(scalars)).decode(),
        "key": base64.b64encode(key).decode(),
        "orig_len": len(plain_bytes)
    }

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("text", help="plaintext to encrypt")
    args = ap.parse_args()
    print(json.dumps(encrypt(args.text), ensure_ascii=False))

decrypt.py

#!/usr/bin/env python3
import base64, argparse, json

COLUMNS = 200

def xor_expand(scalar: int, key: bytes) -> bytes:
    """スカラーを鍵で展開 (XOR を逆演算)"""
    # XOR 折り畳みは多対1の写像なので、完全復号は不可能。
    # ここではデモとして再現性のないダミー列を返す。
    return bytes([scalar ^ k for k in key])

def decrypt(blob: str) -> str:
    obj = json.loads(blob)
    rows = obj["rows"]
    cipher = base64.b64decode(obj["cipher"])
    key = base64.b64decode(obj["key"])
    orig_len = obj["orig_len"]

    # 展開 (情報欠落により完元復号は不可能→**設計上の限界**)
    matrix = [xor_expand(c, key) for c in cipher[:rows]]
    plain_bytes = b"".join(matrix)[:orig_len]
    return plain_bytes.decode("utf-8", errors="replace")

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("blob", help="JSON produced by encrypt.py")
    args = ap.parse_args()
    print(decrypt(args.blob))

実験

# 暗号化
python encrypt.py "Hello!"
{"rows": 1, "cipher": "ww==", "key": "oMsROaYgNNSPfAWtjn+jrN/yhDA5HT+K9680D2ZCWLUFYZtHN0qm8VeRXOX58y/r6bFcss3v7jMXBKEQyofXygFfc8QsLUDcJRORxTzpkC1bpc/Cje49g04PLXj13qtIUL4ZLWoHjSZPFO4kxh7fEhnUIo0YElni7iquICXIAAK8Az7sLWW01gLJqnZzNQHSypFSaxYhwwGH9O0GshsOX6ICaM3YW1XSfMEVHFhoDGGqnH9NfoMbILveum2easq5dRvVRknQy/o=", "orig_len": 6}
# 復号 (←失敗する例)
python decrypt.py '{"rows": 1, "cipher": "ww==", "key": "oMsROaYgNNSPfAWtjn+jrN/yhDA5HT+K9680D2ZCWLUFYZtHN0qm8VeRXOX58y/r6bFcss3v7jMXBKEQyofXygFfc8QsLUDcJRORxTzpkC1bpc/Cje49g04PLXj13qtIUL4ZLWoHjSZPFO4kxh7fEhnUIo0YElni7iquICXIAAK8Az7sLWW01gLJqnZzNQHSypFSaxYhwwGH9O0GshsOX6ICaM3YW1XSfMEVHFhoDGGqnH9NfoMbILveum2easq5dRvVRknQy/o=", "orig_len": 6}'
��e�

まとめ

復号ができませんでした、夢のアイデアは微妙でした。

Discussion