1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ジョン・ナッシュが1955年にNSAへ送った暗号装置を再現

1
Last updated at Posted at 2026-10-15

1955年、ジョン・ナッシュは米国家安全保障局(NSA)に、自作の暗号化・復号装置の仕組みを送った。公開された手書き資料には、暗号文を装置内部に戻し、2種類の置換・ビット反転を切り替える回路が描かれている。

この記事では、原稿の12〜16ページに説明された回路の一般形を、Pythonの標準ライブラリだけで再現する。

注意:この実装は教育用の歴史的モデルであり、安全な暗号方式ではない。実際の秘密情報の保護には使用しないこと。当時のNSA内部評価も、この原理単独の安全性は限定的だとしている(原本18ページ)。

1. 原典と実装範囲

原典:NSA公開のジョン・ナッシュ書簡(PDF)

  • 12ページ:送信側・受信側の回路図。P(Permuter-Reverser)、D(Decider)、A(Adder mod 2)、R(Retarder)。
  • 13〜15ページ:1周期ごとの演算、暗号文のフィードバック、巡回置換、赤青の2種類の配線、ビット反転。
  • 16ページ:鍵は2つの置換の選び方で決まり、鍵の候補数は [n! × 2^(n+1)]²。
  • 18ページ:NSAの内部評価。提案は興味深いが、安全性が限定的という判断。

原稿14ページの赤・青の矢印はある具体的な鍵の図示である。本プログラムはその図の配線を固定して転記したものではなく、ナッシュの一般的な定義に従う2つの巡回置換を鍵から作る。したがって「図と同じ固定鍵の再現」ではなく、「原稿の回路規則を満たす装置の実装」である。

2. 回路をビット単位で定義する

原稿の構成要素を次のように対応づける。

記号 構成要素 この実装での処理
D Decider(選択器) フィードバックされた暗号文のビットが0なら青、1なら赤の置換を選ぶ
P Permuter-Reverser(置換・反転器) 内部ビット列を巡回路に沿って移動・反転し、1ビットを出力、Dから1ビットを取り込む
A Adder mod 2(加算器) XORを計算し、その結果を次の周期で出力する
R Retarder(遅延器) 受信側で暗号文を1周期遅延させ、Pへ与える

送信側で周期 i の平文ビットを p[i]、装置Pの出力を z[i]、送信ビットを c[i] とすると、以下の順で動く。

c[0] = 0                         # Aの初期値(実装上の仮定)
z[i] = P.tick(c[i])              # c[i]をDとPにフィードバック
c[i+1] = p[i] XOR z[i]           # Aは結果を次周期に送信

受信側はRによって c[i] を1周期遅らせてPに入力するため、同じ状態・同じ鍵なら同じ z[i] が得られる。従って p[i] = c[i+1] XOR z[i] となり、元の平文を復元できる。実装では受信側のAにも1周期の出力遅延を設け、最後の1ビットを終端処理で回収する。

この初期化では、平文Nビットに対して暗号文はN+1ビットになる。最初のビットはAの初期出力、最後のビットはAに残っている結果である。原文は通信開始時の初期値と終端処理を規定していないので、ここは実行可能にするための追加仕様である。

3. なぜ鍵の数が [n! × 2^(n+1)]² なのか

Pの記憶位置を 0, 1, ..., n とする。0 はDからの入力を受ける特別な位置である。

各置換はこの n+1 個の位置をすべて通る一つの巡回路とする。位置0を始点として固定すると、残りn個の並べ順は n! 通り。さらに、巡回路の各辺について「そのまま移動」か「0と1を反転する」かを選べるので、2^(n+1) 通りである。

置換1つ分の候補数 = n! × 2^(n+1)
青と赤、2つの置換を選ぶ鍵 = [n! × 2^(n+1)]²

これは原文16ページの数え方と対応する。ただし、鍵の候補数が大きいことは暗号強度の証明ではない。また、このプログラムは入力しやすいようパスワードから鍵を決定的に生成するが、その変換もナッシュの原文にはない。パスワードのエントロピーと理論上の鍵候補数は別物である。

Pの更新規則は次のとおりである。

  1. Dの値に応じて、青または赤の巡回路を選択する。
  2. 巡回路の最終位置にあったビットを、必要なら反転してPの出力とする。
  3. その他のビットを巡回路に沿って移動し、各辺の設定に応じて反転する。
  4. 記憶位置0にDのビットを格納する。

この規則は、原稿15ページの「Dの数字は選択を決めた後で最初の位置に入り、2つの置換はともに全位置を巡回する」という説明に対応している。

4. 実行方法

Python 3.10以降を使用する。追加パッケージは不要。

# 標準ライブラリだけで検証
python nash1955.py selftest

# 巡回路・反転規則と最初の12周期を表示
python nash1955.py demo

# UTF-8テキストを暗号化
python nash1955.py encrypt --key 'example' --text 'こんにちは'

# 上の出力を復号(下記は動作確認済みの出力例)
python nash1955.py decrypt --key 'example' --token 'NASH55:6:121:11dd765e36fb1144c0bf2477857b2280'

復号結果は こんにちは となる。encrypt の --n はDの入力位置以外の記憶位置数で、デフォルトは6。鍵を変えると巡回路と反転規則が変わる。

プログラムからは encrypt(data: bytes, password: str, n=6) と decrypt(token: str, password: str) を直接呼べる。暗号文は NASH55:n:ビット数:16進数 という独自の教育用フォーマットで出力する。

5. 自動同期は常に成立するのか

ナッシュは別の手紙で、この装置が自動同期し、散発的な伝送エラーから回復すると主張した。しかし、原稿の「青・赤の置換がそれぞれ全位置を巡る」だけでは、任意の切り替え系列に対する状態の収束は保証されない。

具体例として、位置が 0,1,2 の3つで、青が (0,1,2)、赤が (0,2,1) の巡回路だとする。Dが 0,1,0,1,... と交互に切り替わる場合、位置1と2に存在する初期状態の差がこの2位置の間を移動し続けることがある。両置換が単独では全位置を一巡するにもかかわらず、混合した操作では状態が消えない。

この反例は selftest に入れてある。通常の同一初期状態からの暗号化・復号が一致することと、異なる内部状態から必ず再同期することは、別の性質である。

暗号文の改ざん検知、メッセージ認証、nonce、現代的な安全性証明も備えていない。ナッシュの発想を実験するためのプログラムであって、実用的な暗号ライブラリの代わりにはならない。

6. ソースコード全文(nash1955.py)

以下は配布する nash1955.py と完全に同一の内容。途中の関数・テスト・CLI処理を省略していない。

#!/usr/bin/env python3
"""1955年ジョン・ナッシュの暗号化・復号装置を再現する教育用モデル。

出典: NSA公開書簡 nash_letters1.pdf, 12〜16ページ。
実用暗号として使用しないこと。初期値・パスワード変換・通信終端は独自の実装規約。

    python nash1955.py encrypt --key 'example' --text 'こんにちは'
    python nash1955.py decrypt --key 'example' --token 'NASH55:...'
    python nash1955.py demo
    python nash1955.py selftest
"""

from __future__ import annotations

import argparse
import hashlib
import random
from dataclasses import dataclass


MAGIC = "NASH55"
DEFAULT_N = 6   # Dから入力を受ける最初の場所を除く記憶位置の数
MAX_N = 64      # 教育用CLIの計算量を制限


@dataclass(frozen=True)
class Cycle:
    """長さn+1の巡回路。flip[i]はroute[i]から次へ移るときの反転。"""

    route: tuple[int, ...]  # (0, ...): 0がD入力を受ける特別な場所
    flip: tuple[int, ...]   # 最後の反転ビットはPからの出力に作用

    def __post_init__(self) -> None:
        m = len(self.route)
        if m < 2 or self.route[0] != 0 or set(self.route) != set(range(m)):
            raise ValueError("routeは0で始まる全記憶位置の巡回路にする")
        if len(self.flip) != m or any(bit not in (0, 1) for bit in self.flip):
            raise ValueError("flipは記憶位置と同数の0/1を指定する")


class ByteStream:
    """パスワードから置換を再現可能に作るための決定的なバイト列。

    これは原文にない補助機能で、暗号学的安全性は保証しない。
    """

    def __init__(self, password: str) -> None:
        self.seed = hashlib.sha256(
            b"nash1955-educational-v1\0" + password.encode("utf-8")
        ).digest()
        self.counter = 0
        self.pool = b""

    def get(self, count: int) -> bytes:
        while len(self.pool) < count:
            block = hashlib.sha256(
                self.seed + self.counter.to_bytes(8, "big")
            ).digest()
            self.pool += block
            self.counter += 1
        result, self.pool = self.pool[:count], self.pool[count:]
        return result

    def below(self, upper: int) -> int:
        """剰余バイアスを避けた[0, upper)の整数。"""
        if upper <= 0:
            raise ValueError("upper must be positive")
        size = max(1, ((upper - 1).bit_length() + 7) // 8)
        domain = 1 << (size * 8)
        cutoff = domain - domain % upper
        while True:
            value = int.from_bytes(self.get(size), "big")
            if value < cutoff:
                return value % upper


def derive_cycles(password: str, n: int) -> tuple[Cycle, Cycle]:
    """2つの(n+1)-巡回置換と各辺の反転を生成する。

    原文の鍵空間は [n! * 2^(n+1)]^2。パスワード変換は独自の補完。
    """
    if not 1 <= n <= MAX_N:
        raise ValueError(f"nは1〜{MAX_N}にする")
    if not password:
        raise ValueError("空の鍵は指定できない")
    rng = ByteStream(password)
    cycles = []
    for _ in range(2):
        tail = list(range(1, n + 1))
        # 特別な記憶位置0を先頭に固定した巡回置換はn!通り。
        for i in range(len(tail) - 1, 0, -1):
            j = rng.below(i + 1)
            tail[i], tail[j] = tail[j], tail[i]
        flips = tuple(rng.below(2) for _ in range(n + 1))
        cycles.append(Cycle((0, *tail), flips))
    return cycles[0], cycles[1]


class Permuter:
    """P: Dが選んだ巡回置換・反転を実行し、1ビット出し入れする。"""

    def __init__(self, cycles: tuple[Cycle, Cycle]) -> None:
        if len(cycles[0].route) != len(cycles[1].route):
            raise ValueError("2つの置換の記憶位置数が異なる")
        self.cycles = cycles
        # 原文に初期値の指定がないため、双方とも0でそろえる。
        self.state = [0] * len(cycles[0].route)

    def tick(self, d: int) -> int:
        if d not in (0, 1):
            raise ValueError("Dには0または1が必要")
        rule = self.cycles[d]  # D=0:青、D=1:赤
        route, flip = rule.route, rule.flip
        old = self.state
        updated = [0] * len(old)
        for i in range(len(route) - 1):
            updated[route[i + 1]] = old[route[i]] ^ flip[i]
        emitted = old[route[-1]] ^ flip[-1]
        # 古い数字を取り出したのち、Dの数字を最初の記憶位置に置く。
        updated[0] = d
        self.state = updated
        return emitted


class Sender:
    """送信側。Aはその周期で計算した和を次周期に出力する。"""

    def __init__(self, cycles: tuple[Cycle, Cycle]) -> None:
        self.permuter = Permuter(cycles)
        self.adder_pending = 0  # Aの初期出力。原文で未指定。
        self.finished = False

    def tick(self, clear_bit: int) -> int:
        if self.finished or clear_bit not in (0, 1):
            raise ValueError("送信終了後、または0/1以外の入力")
        wire_bit = self.adder_pending
        p_bit = self.permuter.tick(wire_bit)  # 暗号文をDへ帰還
        self.adder_pending = clear_bit ^ p_bit
        return wire_bit

    def finish(self) -> int:
        """Aに残った最後の1ビットを送信する(Pは動かさない)。"""
        if self.finished:
            raise ValueError("finishを2回呼べない")
        self.finished = True
        return self.adder_pending


class Receiver:
    """受信側。Rで暗号文を1周期遅らせ、Aも1周期遅れて出力。"""

    def __init__(self, cycles: tuple[Cycle, Cycle]) -> None:
        self.permuter = Permuter(cycles)
        self.retarder: int | None = None
        self.adder_pending: int | None = None
        self.finished = False

    def tick(self, wire_bit: int) -> int | None:
        if self.finished or wire_bit not in (0, 1):
            raise ValueError("受信終了後、または0/1以外の入力")
        if self.retarder is None:
            self.retarder = wire_bit
            return None
        p_bit = self.permuter.tick(self.retarder)  # 前周期の暗号文
        old_output = self.adder_pending
        self.adder_pending = wire_bit ^ p_bit
        self.retarder = wire_bit
        return old_output

    def finish(self) -> int | None:
        if self.finished or self.retarder is None:
            raise ValueError("finishの呼び出し順序が不正")
        self.finished = True
        return self.adder_pending


def byte_bits(data: bytes):
    """バイト列を上位ビットから順に並べる。"""
    for value in data:
        for shift in range(7, -1, -1):
            yield (value >> shift) & 1


def pack_bits(bits: list[int]) -> bytes:
    """末尾を0で右詰め(右側パディング)してバイト列へ。"""
    out = bytearray()
    current = 0
    for pos, bit in enumerate(bits):
        current = (current << 1) | bit
        if pos % 8 == 7:
            out.append(current)
            current = 0
    if len(bits) % 8:
        out.append(current << (8 - len(bits) % 8))
    return bytes(out)


def unpack_bits(data: bytes, bit_length: int) -> list[int]:
    if bit_length < 0 or len(data) != (bit_length + 7) // 8:
        raise ValueError("ビット長とデータ長が一致しない")
    bits = list(byte_bits(data))
    if any(bits[bit_length:]):
        raise ValueError("未使用のパディングビットが0ではない")
    return bits[:bit_length]


def encrypt(data: bytes, password: str, n: int = DEFAULT_N) -> str:
    """平文バイト列 -> NASH55:n:ビット数:16進表記。"""
    sender = Sender(derive_cycles(password, n))
    ciphertext = [sender.tick(bit) for bit in byte_bits(data)]
    ciphertext.append(sender.finish())  # Aの遅延ぶん、平文より1ビット長い
    return f"{MAGIC}:{n}:{len(ciphertext)}:{pack_bits(ciphertext).hex()}"


def decrypt(token: str, password: str) -> bytes:
    """送信側と同じ初期状態・同じ2種類の巡回置換で復号する。"""
    fields = token.strip().split(":")
    if len(fields) != 4 or fields[0] != MAGIC:
        raise ValueError("暗号文形式はNASH55:n:bit_length:hex")
    try:
        n, bit_length = int(fields[1]), int(fields[2])
        raw = bytes.fromhex(fields[3])
    except ValueError as exc:
        raise ValueError("暗号文の数値または16進表記が不正") from exc
    if bit_length < 1 or (bit_length - 1) % 8:
        raise ValueError("暗号文は平文のビット数+1でなければならない")
    ciphertext = unpack_bits(raw, bit_length)
    if ciphertext[0] != 0:
        raise ValueError("Aの初期出力ビットが一致しない")
    receiver = Receiver(derive_cycles(password, n))
    plaintext_bits = []
    for bit in ciphertext:
        recovered = receiver.tick(bit)
        if recovered is not None:
            plaintext_bits.append(recovered)
    last = receiver.finish()
    if last is not None:
        plaintext_bits.append(last)
    if len(plaintext_bits) != bit_length - 1:
        raise AssertionError("内部復号ビット数の不整合")
    return pack_bits(plaintext_bits)


def demo(password: str = "demo", n: int = DEFAULT_N) -> None:
    """各周期の送信側の内部値と暗号化・復号を表示する。"""
    message = "Nash 1955 / ナッシュ"
    cycles = derive_cycles(password, n)
    print("原文に対応する巡回路(0がDからの入力位置)")
    for d, rule in enumerate(cycles):
        print(f"D={d}: route={rule.route}, flip={rule.flip}")
    print("\n先頭の12周期: i / 平文x / D=送信ビットc / P出力z / 次の送信ビット")
    p = Permuter(cycles)
    old_sum = 0
    for i, x in enumerate(list(byte_bits(message.encode("utf-8")))[:12]):
        d = old_sum
        z = p.tick(d)
        old_sum = x ^ z
        print(f"{i:2d}    {x}         {d}              {z}            {old_sum}")
    token = encrypt(message.encode("utf-8"), password, n)
    print("\n暗号文:", token)
    print("復号結果:", decrypt(token, password).decode("utf-8"))


def selftest() -> None:
    """境界条件、再現性、Unicode、周期の遅延、同期の限界をテスト。"""
    for n in (1, 2, 3, 6, 12):
        key = f"test_{n}"
        for payload in (b"", b"\0", b"\xff", b"\x00\xff", bytes(range(256)),
                        "こんにちは、Nash!".encode("utf-8")):
            token = encrypt(payload, key, n)
            assert decrypt(token, key) == payload
            assert int(token.split(":")[2]) == 8 * len(payload) + 1
            assert encrypt(payload, key, n) == token
        for length in (1, 2, 7, 64, 128):
            payload = random.Random(n * 1000 + length).randbytes(length)
            assert decrypt(encrypt(payload, key, n), key) == payload

    # 一つの置換を繰り返せば全位置を巡回しても、切替時の収束は保証されない。
    # route=(0,1,2) の後に route=(0,2,1) を選ぶと、位置1と2にある
    # 初期状態の差は位置0へ移らず、永久に残り得る。
    c0 = Cycle((0, 1, 2), (0, 0, 0))
    c1 = Cycle((0, 2, 1), (0, 0, 0))
    p0, p1 = Permuter((c0, c1)), Permuter((c0, c1))
    p1.state[1] = 1
    for i in range(100):
        p0.tick(i % 2)
        p1.tick(i % 2)
        assert p0.state != p1.state

    valid = encrypt(b"test", "key")
    try:
        decrypt(valid.replace("NASH55:", "WRONG:"), "key")
    except ValueError:
        pass
    else:
        raise AssertionError("壊れたフォーマットを拒否しなかった")
    print("selftest: OK (往復・長さ・再現性・不正入力・非収束例)")


def main() -> None:
    parser = argparse.ArgumentParser(
        description="1955年ナッシュ暗号装置の教育用モデル(安全な暗号ではありません)"
    )
    actions = parser.add_subparsers(dest="command", required=True)
    enc = actions.add_parser("encrypt", help="UTF-8文字列を暗号化")
    enc.add_argument("--key", required=True, help="教育用のパスワード")
    enc.add_argument("--text", required=True, help="暗号化する文字列")
    enc.add_argument("--n", type=int, default=DEFAULT_N,
                     help="Pの記憶位置の数(D入力位置を除く。既定6)")
    dec = actions.add_parser("decrypt", help="暗号文を復号")
    dec.add_argument("--key", required=True)
    dec.add_argument("--token", required=True, help="NASH55:n:bit_length:hex")
    example = actions.add_parser("demo", help="回路内部の動作例を表示")
    example.add_argument("--key", default="demo")
    example.add_argument("--n", type=int, default=DEFAULT_N)
    actions.add_parser("selftest", help="組み込みテスト")
    args = parser.parse_args()
    try:
        if args.command == "encrypt":
            print(encrypt(args.text.encode("utf-8"), args.key, args.n))
        elif args.command == "decrypt":
            print(decrypt(args.token, args.key).decode("utf-8"))
        elif args.command == "demo":
            demo(args.key, args.n)
        else:
            selftest()
    except (ValueError, UnicodeDecodeError) as exc:
        parser.exit(2, f"error: {exc}\n")


if __name__ == "__main__":
    main()

7. 動作確認

python nash1955.py selftest は次の事項を検証する。

  • 空の平文、1バイト、日本語のUTF-8、0〜255の全バイト値、さまざまな長さのランダムなバイト列の往復。
  • 暗号文が常に平文のビット数+1となること。
  • 同じパスワード・初期化なら再現可能であること。
  • 不正なフォーマットを拒否すること。
  • 2つの巡回置換を切り替えた場合の非収束反例。

確認した実行結果:

selftest: OK (往復・長さ・再現性・不正入力・非収束例)

原稿そのものに書かれている仕様は「P・D・A・Rの動作と鍵の数え方」であり、パスワードからの鍵の生成、初期状態のゼロ埋め、暗号文のヘッダー、終端処理は今回のプログラムに固有の仕様である。この区別をなくして「1955年の装置を完全復刻した」と呼ぶのは正確ではない。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?