🔐

EnigmaシミュレータCLI作ってみました

に公開

EnigmaシミュレータCLI作ってみました

はじめに

こんにちは!株式会社BTM 仙台ラボの関口です!
突然ですが、Enigma(エニグマ)ってご存じですか?
暗号技術の本では必ずと言ってもいいほどよく紹介される機械式暗号機です。映画の題材にもなっていて、以前からその仕組みに興味がありました。
今回は、そのEnigmaという暗号機を模したPython CLIプログラムを作成してみました!

Enigmaとは

Enigma(エニグマ)とは、第二次世界大戦でナチス・ドイツが用いたローター式暗号機です。
名称はギリシア語に由来していて、「謎」を意味しています。
見た目はタイプライターのようですが、強力な暗号を作ることができます。

エニグマ (暗号機) - Wikipedia

Webと暗号

昨今のWeb技術においても、HTTPS通信やSSH接続、Wi-Fi通信などで、RSAやAESといった暗号技術が活用されていますね。
Enigmaは「同じ鍵」で暗号化と復号を行う「共通鍵暗号(対称鍵暗号)」であり、DESやAESと同じ暗号方式に分類されます。

インターネットでは、不特定多数のユーザーが存在するため、安全にサービスを利用するには、盗聴や改ざん、なりすましを防ぐ必要があります。
そのため、インターネットにおいて暗号技術は欠かせない存在となっています。

Enigmaの暗号化技術やその解読の過程は、暗号技術や計算機科学の発展にも影響を与えたと言われています。

Enigmaの仕組み

Enigmaは主に、キーボード、プラグボード、ローター、反射板、ランプボードのパーツにより暗号化が行われます。
それぞれの簡単な説明を載せておきます。

パーツ 説明
キーボード アルファベットを入力する部分。タイプライターのように26文字のキーが並ぶ。
プラグボード 文字同士をペアで入れ替える仕組み。接続するケーブルで変換を設定できる。
ローター 内部に配線された円盤で、文字を複雑に変換する心臓部。入力のたびに回転し、変換が変わる。3枚のローターが組み合わせて使われる。
反射板(リフレクター) ローターから受けた信号を折り返し、再度ローターに信号を返す。
ランプボード ローターから返ってきた最終的な変換結果の文字が光って表示される部分。ここで暗号化後の文字が分かる。

キーボードで文字をタイプすると、プラグボード、ローター、反射板、ローター、プラグボードの順で電流が流れ、アルファベット26文字それぞれが書かれたランプのうち、一つのランプが点灯します。
その点灯したランプに書かれているアルファベットが、入力された文字から暗号化(または復号化)された文字になります。

特に、暗号化のメインとなるのはローター部分で、ローターは一つのEnigmaに三つ内蔵されます。
一文字タイプすると、ローターが内部で回転し、続けて同じ文字が入力されたとしても別の文字に暗号化します。

Enigmaの面白いところは、暗号化と復号化が全く同じ手順で行えるところです。同じ設定がなされたEnigmaがあれば、暗号を入力すれば復号化された文字列を得ることができます。

仕組みや動きの理解は、以下の動画がとてもわかりやすいので、是非見ていただければと思います。
エニグマ暗号機の仕組みとは?

Enigmaシミュレータの実装

ディレクトリ構成

enigma-simulator
├── enigma
│   ├── __init__.py
│   ├── enigma_machine.py
│   ├── plugboard.py
│   ├── reflector.py
│   └── rotor.py
└── main.py

enigma/rotor.py

class Rotor:
    # クラス変数:基準となるアルファベット(A〜Z)
    base_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    def __init__(self, wiring: str, notch: str, position: str = 'A'):
        """
        コンストラクタ:ローターの配線、ノッチ位置、初期位置を設定

        :param wiring: 26文字の配線(例:'EKMFLGDQVZNTOWYHXUSPAIBRCJ')
        :param notch: ノッチ位置の文字(例:'Q')
        :param position: 初期位置(デフォルトは 'A')
        """
        self.wiring = wiring.upper()  # 配線を大文字で保持
        self.notch = notch.upper()    # ノッチ位置(次のローターを進めるタイミング)
        self.position = self.base_alphabet.index(position.upper())  # 現在のローター位置をインデックスで保持

    def get_display_letter(self) -> str:
        """
        現在のローター位置をアルファベットで取得
        :return: 表示されているアルファベット(例:'A')
        """
        return self.base_alphabet[self.position]

    def step(self):
        """
        ローターを1ステップ進める
        """
        self.position = (self.position + 1) % 26  # 26文字でループ

    def at_notch(self) -> bool:
        """
        現在の位置がノッチに一致するかを判定
        :return: ノッチ位置ならTrue、それ以外はFalse
        """
        return self.base_alphabet[self.position] == self.notch

    def encode_forward(self, c: str) -> str:
        """
        ローターを通過する際の順方向のエンコード処理

        :param c: 入力文字(例:'A')
        :return: エンコード後の文字
        """
        # 入力文字のインデックスを現在位置でシフト
        index = (self.base_alphabet.index(c.upper()) + self.position) % 26
        # シフト後の位置にある配線の文字を取得
        substituted = self.wiring[index]
        # 現在位置を考慮して元のアルファベット上の出力インデックスに変換
        output_index = (self.base_alphabet.index(substituted) - self.position) % 26
        return self.base_alphabet[output_index]

    def encode_backward(self, c: str) -> str:
        """
        ローターを通過する際の逆方向のエンコード処理

        :param c: 入力文字(例:'A')
        :return: デコード後の文字
        """
        # 入力文字を現在位置でシフト
        index = (self.base_alphabet.index(c.upper()) + self.position) % 26
        # シフトされた文字に対応するアルファベットを取得
        letter_at_index = self.base_alphabet[index]
        # wiring内でその文字が現れる位置(逆方向の配線)を探す
        wiring_index = self.wiring.index(letter_at_index)
        # 現在のローター位置を考慮して出力インデックスを求める
        output_index = (wiring_index - self.position) % 26
        return self.base_alphabet[output_index]

ノッチとは、隣のローターを回転させるための「切り込み」のことです。
エニグマでは、右端のローターは常に回転し、右ローターがノッチ位置に来ると、次にキーが押されたときに中央のローターも回転します。
さらに、中央のローターがノッチ位置にある状態でキーが押されると、左端のローターも回転します。
この仕組みにより、中央と左のローターは、それぞれ右側のローターのノッチ位置に依存して回転します。

enigma/reflector.py


class Reflector:
    # クラス変数:アルファベット(A〜Z)
    base_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    def __init__(self, wiring: str):
        """
        コンストラクタ:リフレクターの配線を設定

        :param wiring: 26文字のアルファベットで構成されたリフレクター配線(例:"YRUHQSLDPXNGOKMIEBFZCWVJAT")
        :raises ValueError: 入力が26文字のアルファベットでない場合
        """
        # 入力チェック:26文字のアルファベットで構成されているかを検証
        if len(wiring) != 26 or not wiring.isalpha():
            raise ValueError("Reflector wiring must be a 26-letter alphabet string.")
        
        # 入力を大文字に変換して保持
        self.wiring = wiring.upper()

    def reflect(self, c: str) -> str:
        """
        文字を反射させる処理(順方向から来た信号を逆方向に戻す)

        :param c: 入力文字(例:'A')
        :return: 反射された文字(例:'Y')
        """
        # 入力文字のインデックスを取得
        index = self.base_alphabet.index(c.upper())
        # 配線から対応する文字を取得(反射後の出力)
        return self.wiring[index]

リフレクターは、ロータのように文字の変換はするものの、回転はおきません。
リフレクターを通ることで、ローターからやってきた信号を再度ローターに返すことになります。

enigma/plugboard.py

class Plugboard:
    def __init__(self, connections: list[tuple[str, str]] = None):
        """
        コンストラクタ:プラグボードの接続(スワップ)設定を行う

        :param connections: 文字のペアのリスト(例:[('A', 'B'), ('C', 'D')])
        :raises ValueError: 
            - 同じ文字同士を接続しようとした場合
            - すでに接続済みの文字を再接続しようとした場合
        """
        self.mapping = {}  # 文字の対応関係を保持する辞書

        if connections:
            for a, b in connections:
                a = a.upper()
                b = b.upper()

                # 同じ文字を接続しようとした場合はエラー
                if a == b:
                    raise ValueError(f"Cannot connect a letter to itself: {a}")

                # すでに接続されている文字を使おうとした場合はエラー
                if a in self.mapping or b in self.mapping:
                    raise ValueError(f"Letter already connected: {a} or {b}")

                # 双方向でマッピングを設定(例:A→B, B→A)
                self.mapping[a] = b
                self.mapping[b] = a

    def substitute(self, c: str) -> str:
        """
        プラグボードによる文字の置換処理

        :param c: 入力文字
        :return: 接続設定に従って置換された文字(接続されていなければ元の文字)
        """
        return self.mapping.get(c.upper(), c.upper())

プラグボード(Plugboard)は、エニグマ暗号機において任意の文字ペアをケーブルで接続し、入力・出力の際に文字を相互に置き換える仕組みです。
最大10ペアまで設定可能で、暗号の複雑さを高める役割を担っています。

enigma/enigma_machine.py

from .rotor import Rotor
from .reflector import Reflector
from .plugboard import Plugboard

class EnigmaMachine:
    def __init__(self, rotors: list[Rotor], reflector: Reflector, plugboard: Plugboard):
        """
        コンストラクタ:エニグマ暗号機を構成する3つのローター、リフレクター、プラグボードを受け取る

        :param rotors: Rotor インスタンスのリスト(3つ必要:[左, 中央, 右])
        :param reflector: Reflector インスタンス
        :param plugboard: Plugboard インスタンス
        :raises ValueError: ローターが3つでない場合にエラーを出す
        """
        if len(rotors) != 3:
            raise ValueError("EnigmaMachine requires exactly 3 rotors.")

        self.rotors = rotors      # ローター:左 → 中央 → 右
        self.reflector = reflector
        self.plugboard = plugboard

    def step_rotors(self):
        """
        キー入力ごとにローターを回転させる処理。
        """
        left, middle, right = self.rotors

        # ダブルステップ機構の処理
        # 中央ローターがノッチにいる場合:中央と左のローターが回転
        if middle.at_notch():
            middle.step()
            left.step()
        # 右ローターがノッチにいる場合:中央ローターが回転
        elif right.at_notch():
            middle.step()

        # 右ローターは常に1ステップ回転
        right.step()

        # デバッグ用:現在のローター表示文字を出力
        display = (
            f"ローター位置: "
            f"[{left.get_display_letter()}]"
            f"[{middle.get_display_letter()}]"
            f"[{right.get_display_letter()}]"
        )
        print(display)

    def encrypt(self, c: str) -> str:
        """
        単一文字を暗号化する処理(キー1回分)

        :param c: 入力文字
        :return: 暗号化された文字(またはそのまま返す)
        """
        if not c.isalpha():
            return c  # アルファベットでない場合は変換せずにそのまま返す(例:空白、句読点)

        c = c.upper()

        # ローターを回す(文字入力のたびに)
        self.step_rotors()

        # プラグボードによる前処理(入力のスワップ)
        c = self.plugboard.substitute(c)

        # 順方向のローター処理(右 → 左)
        for rotor in reversed(self.rotors):
            c = rotor.encode_forward(c)

        # リフレクターによる反射
        c = self.reflector.reflect(c)

        # 逆方向のローター処理(左 → 右)
        for rotor in self.rotors:
            c = rotor.encode_backward(c)

        # プラグボードによる後処理(出力のスワップ)
        c = self.plugboard.substitute(c)

        return c

ここで、ダブルステップという挙動を実装しました。ダブルステップというのは、真ん中のローターが2回連続で回転する挙動のことです。
ローターは、キーが押されるたびにツメがローターの歯に噛み合い、回転します。一番右のローターは毎回このツメがはまり、常に回転します。
中央のローターは、右のローターがノッチ位置にあるときだけツメが噛み合って回転します。このとき、右ローターも一緒に動かそうとしますが、右は毎回回るため問題になりません。
しかし、中央のローターがノッチ位置にあるときは事情が変わります。今度は、左側のローターを回すツメが中央ローターのノッチにはまり、左と中央の両方が回転します。
つまり、

  1. 右ローターのノッチ → 中央ローターが回転
  2. 続いて中央ローターのノッチ → 左と中央が再び回転

このようにして、中央ローターが2回連続で回転することになります。これが「ダブルステップ」と呼ばれる現象です。

この動きを再現した素晴らしい動画があるので、こちらも是非みていただければと思います。
Enigma Machine Mechanism (feat. a 'Double Step')

main.py

import sys
import string

from enigma.rotor import Rotor
from enigma.reflector import Reflector
from enigma.plugboard import Plugboard
from enigma.enigma_machine import EnigmaMachine

# ------------------------------------------------------------
# 定数定義
# ------------------------------------------------------------

# 利用可能なローター(番号: (配線, ノッチ))
ALL_ROTORS = {
    "1": ("EKMFLGDQVZNTOWYHXUSPAIBRCJ", 'Q'),
    "2": ("AJDKSIRUXBLHWTMCQGZNPYFVOE", 'E'),
    "3": ("BDFHJLCPRTXVZNYEIWGAKMUSQO", 'V'),
    "4": ("ESOVPZJAYQUIRHXLNFTGKDCMWB", 'J'),
    "5": ("VZBRGITYUPSDNHLXAWMJQOFECK", 'Z')
}

# リフレクターの配線(固定)
REFLECTOR_WIRING = "YRUHQSLDPXNGOKMIEBFZCWVJAT"

# ローターの位置ラベル(左・中央・右)
ROTOR_POSITIONS_LABELS = ["左", "中央", "右"]

# ------------------------------------------------------------
# ユーティリティ関数
# ------------------------------------------------------------

def get_rotor_selection(available_rotors, position_name):
    """
    ユーザーにローターの種類を選ばせる関数

    :param available_rotors: 選択可能なローター番号のリスト
    :param position_name: ローターの位置ラベル(左・中央・右)
    :return: ユーザーが選択したローター番号
    """
    while True:
        print(f"利用可能なローター: {', '.join(available_rotors)}")
        choice = input(f"<{position_name}> の位置に使用するローターを番号で選択してください: ")
        if choice in available_rotors:
            return choice
        else:
            print(f"無効な選択です: '{choice}'。利用可能なローターから選んでください。")


def get_rotor_positions():
    """
    各ローターの初期位置(A〜Z)をユーザーから取得する関数

    :return: 初期位置のリスト(例: ['A', 'B', 'C'])
    """
    positions = []
    for name in ROTOR_POSITIONS_LABELS:
        while True:
            pos = input(f"<{name}> ローターの初期位置をアルファベット一文字で入力してください (A-Z): ").upper()
            if len(pos) == 1 and pos in string.ascii_uppercase:
                positions.append(pos)
                break
            else:
                print("無効な入力です。AからZまでの一文字を入力してください。")
    return positions


def get_plugboard_settings():
    """
    ユーザーからプラグボードの設定(文字ペア)を取得する関数

    :return: プラグボードの接続ペア(例: ['AB', 'CD'])
    """
    while True:
        print("プラグボードの設定を入力してください (例: AB CD EF)。")
        print("最大10ペアまで設定可能です。設定しない場合は、何も入力せずEnterキーを押してください。")
        settings_str = input("> ").upper()

        if not settings_str:
            return []

        pairs = settings_str.split()

        # 最大10ペアまで
        if len(pairs) > 10:
            print("エラー: プラグは10ペアまでしか設定できません。")
            continue

        # 同じ文字が複数ペアに含まれていないか確認
        all_chars = "".join(pairs)
        if len(all_chars) != len(set(all_chars)):
            print("エラー: 同じ文字を複数のペアで使用することはできません。")
            continue

        # 各ペアが正しい形式かをチェック(2文字のアルファベット)
        valid = True
        for pair in pairs:
            if len(pair) != 2 or not pair.isalpha():
                print(f"エラー: '{pair}' は不正なペアです。ペアはアルファベット2文字で指定してください。")
                valid = False
                break

        if valid:
            return pairs


def print_separator():
    """区切り線の表示"""
    print("-" * 100)


# ------------------------------------------------------------
# メイン処理
# ------------------------------------------------------------

def main():
    """Enigma シミュレータのメインエントリポイント"""
    try:
        print("Enigmaシミュレータ 起動")
        print_separator()
        print("設定を開始します。")
        print_separator()

        # --- ローター選択 ---
        available_rotors = list(ALL_ROTORS.keys())
        chosen_rotor_names = []
        for i in range(3):
            choice = get_rotor_selection(available_rotors, ROTOR_POSITIONS_LABELS[i])
            chosen_rotor_names.append(choice)
            available_rotors.remove(choice)  # 重複を防ぐ
        
        print(f"\n選択されたローター: [左: {chosen_rotor_names[0]}, 中央: {chosen_rotor_names[1]}, 右: {chosen_rotor_names[2]}]")
        print_separator()

        # --- ローター初期位置設定 ---
        rotor_positions = get_rotor_positions()
        print(f"\nローターの初期位置: [左: {rotor_positions[0]}, 中央: {rotor_positions[1]}, 右: {rotor_positions[2]}]")
        print_separator()

        # --- Rotor インスタンスの生成 ---
        machine_rotors = []
        for i in range(3):
            name = chosen_rotor_names[i]
            wiring, notch = ALL_ROTORS[name]
            rotor = Rotor(wiring=wiring, notch=notch, position=rotor_positions[i])
            machine_rotors.append(rotor)

        # --- プラグボード設定 ---
        plugboard_pairs = get_plugboard_settings()
        plugboard = Plugboard(plugboard_pairs)
        print_separator()

        # --- Reflector の設定 ---
        reflector = Reflector(REFLECTOR_WIRING)

        # --- EnigmaMachine 組み立て ---
        machine = EnigmaMachine(
            rotors=machine_rotors,
            reflector=reflector,
            plugboard=plugboard
        )

        print("設定が完了しました。")
        print_separator()

        # --- メッセージの暗号化処理ループ ---
        while True:
            print("暗号化したい平文、または復号化したい暗号文を入力してください。")
            print("システムを終了するには 'exit' と入力するか、Ctrl+C を押してください。")
            input_text = input("平文(または暗号文) > ")

            if input_text.lower() == 'exit':
                print_separator()
                print("Enigmaシミュレータを終了します。")
                break

            # アルファベットのみ抽出・大文字に変換(スペースや記号は除外)
            cleaned_text = "".join(filter(str.isalpha, input_text)).upper()

            encrypted = []
            for char in cleaned_text:
                encrypted.append(machine.encrypt(char))  # 文字ごとに暗号化

            print("暗号文(または平文) > " + "".join(encrypted))
            print_separator()

    except KeyboardInterrupt:
        # Ctrl+C での終了処理
        print("\n\nCtrl+Cが検出されました。Enigmaシミュレータを終了します。")
        sys.exit(0)


if __name__ == "__main__":
    main()

シミュレータの使い方

実行環境

WSL2 Ubuntu 24.04.2
python 3.12.11

ターミナルから以下のコマンドで実行できます。

python main.py

実行すると、ローターやプラグの設定が求められるので、数字やアルファベットを入力します。
その後、暗号文あるいは平文の入力が求められるので、アルファベットで入力してください。(アルファベット以外は除去する実装となっています。)
以下実行例を示しておきます。

❯ python main.py
Enigmaシミュレータ 起動
----------------------------------------------------------------------------------------------------
設定を開始します。
----------------------------------------------------------------------------------------------------
利用可能なローター: 1, 2, 3, 4, 5
<> の位置に使用するローターを番号で選択してください: 1
利用可能なローター: 2, 3, 4, 5
<中央> の位置に使用するローターを番号で選択してください: 2
利用可能なローター: 3, 4, 5
<> の位置に使用するローターを番号で選択してください: 3

選択されたローター: [左: 1, 中央: 2, 右: 3]
----------------------------------------------------------------------------------------------------
<> ローターの初期位置をアルファベット一文字で入力してください (A-Z): a   # <- アルファベット 小文字も入力可
<中央> ローターの初期位置をアルファベット一文字で入力してください (A-Z): b
<> ローターの初期位置をアルファベット一文字で入力してください (A-Z): c

ローターの初期位置: [左: A, 中央: B, 右: C]
----------------------------------------------------------------------------------------------------
プラグボードの設定を入力してください (例: AB CD EF)。
最大10ペアまで設定可能です。設定しない場合は、何も入力せずEnterキーを押してください。
> ab cd ef   # <- アルファベット 小文字も入力可
----------------------------------------------------------------------------------------------------
設定が完了しました。
----------------------------------------------------------------------------------------------------
暗号化したい平文、または復号化したい暗号文を入力してください。
システムを終了するには 'exit' と入力するか、Ctrl+C を押してください。
平文(または暗号文) > test  # <- アルファベット入力 小文字も入力可 
ローター位置: [A][B][D]
ローター位置: [A][B][E]
ローター位置: [A][B][F]
ローター位置: [A][B][G]
暗号文(または平文) > PVYO  # <- 暗号化された文 復号化も同じ手順
----------------------------------------------------------------------------------------------------
暗号化したい平文、または復号化したい暗号文を入力してください。
システムを終了するには 'exit' と入力するか、Ctrl+C を押してください。
平文(または暗号文) > exit
----------------------------------------------------------------------------------------------------
Enigmaシミュレータを終了します。

まとめ

今回はEnigmaシミュレータCLIを作ってみました。
ローター部分の挙動を理解するのが大変でしたが、動画や解説サイトを何度も見て、構造を思考するのは楽しかったです。
Enigmaは「イミテーション・ゲーム/エニグマと天才数学者の秘密」という映画の題材にもなっていますので、そちらも是非見ていただければと思います!

おまけ

最後に本シミュレータで以下の設定で暗号を作成してみました。
よかったら復号化してみてください。

使用ローター番号:1, 2, 3
ローター設定: b, t, m
プラグ設定: なし
暗号: VRPQYUIVWTGWKQJOCAXM
ヒント: マスター ヨーダ

参考

Discussion