战争密码 Python实现二战德军恩格玛机(Enigma)

恩格玛机原理介绍

恩格玛机,在密码学史中通常被称为恩尼格玛密码机(Enigma),是一种用于加密与解密文件的密码机。它最早由德国工程师亚瑟·谢尔比乌斯(Arthur Scherbius)和他的朋友理查德·里特(Richard Ritter)在1918年发明,并在二战期间被纳粹德国广泛使用。

恩尼格玛密码机的主体结构包括键盘、显示器、转子和反射器等部分。键盘和显示器都与普通的打字机类似,但去除了空格、数字和标点符号,只留下字母键。转子隐藏在面板下,是加密的核心部件。

恩尼格玛密码机的加密原理基于复杂的机械结构和换位加密技术。其核心部件包括三个到五个可旋转的转子(二战后期德国海军使用的甚至有四个或五个转子),以及一个反射器。

转子:转子上有26个字母,每个字母都与另一个字母通过复杂的线路相连。当按下键盘上的一个字母时,该字母的信号会经过转子进行加密,并在显示器上通过灯泡显示加密后的字母。转子在每次按键后会自动旋转一个位置,从而改变加密规则。
反射器:反射器位于转子之后,它的作用是将信号反射回转子,但在这个过程中会再次对信号进行加密。反射器的设计使得加密和解密过程完全相同,只是方向相反。

下图是恩格玛机的电路结构示意图(图片来源:Bilibili平台@Ele实验室,推荐观看该UP主的视频,以深入探索更多相关原理及在二战时期背后的精彩故事)。
在这里插入图片描述
恩格玛机(Enigma Machine)在诞生之初便因其独特的加密机制而备受瞩目,其核心原理在于每加密一个字符后,便会自动更换一次字符映射表,这一设计极大地增加了密码的复杂性和不可预测性,使得它在当时被认为是几乎无法破译的加密设备。其内部复杂的机械构造与精密的加密逻辑相互交织,共同构筑了一道坚实的屏障,使其有效抵御了传统的密码破译手段。

下面使用Python通过编程的方式实现一个恩格玛机加解密程序,并且支持含有除26个英文字母外的任意字符文件的加密和解密

转轮类的实现

为了实现对恩格玛机中转轮复杂逻辑行为的准确抽象与表达,采用了面向对象的程序设计方法,首先设计了一个Runner类来专门模拟转轮的行为。通过Runner类,能够封装转轮的所有核心属性(如字符映射关系、当前位置等)和行为(如旋转、输入输出映射等),从而使得恩格玛机的加密解密过程更加模块化、易于理解和维护。

class Runner:
    def __init__(self, str_list):
        self.str_list = str_list
        self.num_str = len(self.str_list)
        self.idx = np.arange(self.num_str)
        np.random.shuffle(self.idx)
        self.idx = list(self.idx)
        self.mk_dir()

    def mk_dir(self):
        self.dir = {}
        self.dir2 = {}  # 从左往右的字典(dir是key->val,这是其val->key)
        for i, j in enumerate(self.str_list):
            self.dir[j] = self.str_list[self.idx[i]]
            self.dir2[self.str_list[self.idx[i]]] = j

    def rotate(self):
        self.idx2 = self.idx[1:]
        self.idx2.append(self.idx[0])  # append好像快一点
        # self.idx[:1], self.idx[1:] = self.idx[self.num_str-1:], self.idx[:self.num_str-1]
        self.idx = self.idx2

        self.mk_dir()

    def out(self, s):
        return self.dir[s]

    def rfl_out(self, s):
        return self.dir2[s]

首先,程序会计算字符列表的长度,以这个长度为基础来创建转轮,确保转轮能够覆盖所有可能出现的字符。转轮的输入输出映射机制是通过构建两个字典来实现的,这两个字典分别采用随机生成的索引构建字符映射关系。第一个字典用于实现从 key 到 value 的映射,而第二个字典则实现反向映射,即从 value 到 key,以确保转轮能够支持双向的字符转换。

为了模拟转轮的旋转动作,程序定义了一个rotate()方法。该方法在逻辑上通过将索引列表进行一次循环移位操作来实现,移位后,程序会根据新的索引列表重新配置映射字典,从而模拟出转轮旋转后字符位置变化的效果。

恩格玛机类的实现

恩格玛机是由三个转轮所构成的,所以在恩格玛机类的实现时先例化了三个转轮。
另外,由于恩格玛机还有一个反射轮,所以又构建了一个反射轮字典。

class Enigma:
    def __init__(self, str_list, run_pos=[0, 0, 0], encode=True):
        self.str_list = str_list
        self.num_str = len(str_list)
        self.is_encode = encode

        self.r1 = Runner(str_list)
        self.r2 = Runner(str_list)
        self.r3 = Runner(str_list)

        self.run_pos = run_pos  # r3,r2,r1初始位置

        # 三个轮的动态位置增量
        self.r1_p = 0
        self.r2_p = 0
        self.r3_p = 0

        self.init_run()

        self.mk_rfl_dir()
        # print("#" * 10 + "1参数")
        # print(self.r1.dir)
        # print(self.r1.idx)

        # print("#" * 10 + "2参数")
        # print(self.r2.dir)
        # print(self.r2.idx)

        # print("#" * 10 + "3参数")
        # print(self.r3.dir)
        # print(self.r3.idx)

        # print("#" * 10 + "反射板参数")
        # print(self.rfl_dir)
        # print(self.rfl_idx)

    def init_run(self):
        for i in range(self.run_pos[2]):
            self.r1.rotate()
            # print("1轮转一下")

        for i in range(self.run_pos[1]):
            self.r2.rotate()
            # print("2轮转一下")

        for i in range(self.run_pos[0]):
            self.r3.rotate()
            # print("3轮转一下")

    def get_now_out(self, s):
        r1_out = self.r1.out(s)
        # print(r1_out)
        r2_out = self.r2.out(r1_out)
        # print(r2_out)
        r3_out = self.r3.out(r2_out)
        # print(r3_out)
        if self.is_encode:
            rfl_out = self.rfl_dir[r3_out]  # 加密反射轮反射
        else:
            rfl_out = self.rfl_dir2[r3_out]  # 解密反射轮反射
        # print(f"反射后:{rfl_out}")

        r3_rfl = self.r3.rfl_out(rfl_out)
        # print(f"3反射:{r3_rfl}")
        r2_rfl = self.r2.rfl_out(r3_rfl)
        # print(f"2反射:{r2_rfl}")

        final_out = self.r1.rfl_out(r2_rfl)
        # print(f"1反射最终:{final_out}")
        return final_out

    def mk_rfl_dir(self):
        self.rfl_idx = np.arange(self.num_str)
        np.random.shuffle(self.rfl_idx)
        self.rfl_idx = list(self.rfl_idx)
        self.rfl_dir = {}
        self.rfl_dir2 = {}
        for i, j in enumerate(self.str_list):
            self.rfl_dir[j] = self.str_list[self.rfl_idx[i]]
            self.rfl_dir2[self.str_list[self.rfl_idx[i]]] = j

    def get_out(self, s):
        now_out = self.get_now_out(s)
        # print(f"当前轮子状态:{self.r3_p}  {self.r2_p}  {self.r1_p}")
        # 旋转轮子
        if self.r1_p < self.num_str - 1:
            self.r1_p += 1
            self.r1.rotate()
        else:
            self.r1_p = 0
            if self.r2_p < self.num_str - 1:
                self.r2_p += 1
                self.r2.rotate()
            else:
                self.r2_p = 0
                if self.r3_p < self.num_str - 1:
                    self.r3_p += 1
                    self.r3.rotate()
                else:
                    self.r3_p = 0
        return now_out

恩格玛机类被设计为接受三个关键参数:
首先是字符列表,它定义了加密解密过程中涉及的字符集合(有序);
其次是转轮的初始位置,这是一个列表,用于指定三个转轮在加密或解密过程开始时的起始状态,若此列表非默认的[0, 0, 0],则在初始化时会自动调整转轮至指定位置;
最后是加密或解密模式的标志,默认为加密模式,若需进行解密操作,则需将此参数设置为False。

加密与解密操作的核心差异在于反射轮的使用方式:加密时采用从key到value的映射字典(字典1),而解密时则采用相反的从value到key的映射字典(字典2)。

get_now_out方法是恩格玛机类中的一个关键函数,它负责实现转轮间输出信号的连接逻辑,确保加密解密过程中信号的正确传递。而get_out方法则负责在获取当前转轮对应字符的基础上,根据字符列表的长度确定所需的进制转换,并随后驱动最低位的转轮进行一次转动,为下一次字符处理做准备。这两个方法共同协作,确保了恩格玛机能够按照既定的加密解密规则,准确地对输入字符进行处理。

加解密类的实现

加密

class Encrypt:
    def __init__(self, file_name, encrypt_name, run_pos=[0, 0, 0]):
        self.file_name = file_name
        self.encrypt_name = encrypt_name
        self.open_file()
        self.engma = Enigma(self.get_char_file(), run_pos=run_pos, encode=True)

    def open_file(self):
        with open(self.file_name, "r", encoding="utf-8") as f:  # 读入待加密文件
            self.content = f.read().strip("\n")

    def get_char_file(self):  # 获取字符列表(文本中所有出现过的字符)
        content_set = set()
        for i in self.content:
            content_set.add(i)
        return sorted(list(content_set))  # sorted后保证字符列表的唯一

    def run(self):
        s_encode = ""
        tt = len(self.content)
        cnt = 0
        tic = time.time()
        for i in self.content:
            s_encode += self.engma.get_out(i)
            cnt += 1
            print(f"正在加密:{int(cnt/tt*100)}%")
        print(f"用时:{time.time() - tic}s")

        with open(self.encrypt_name, "w", encoding="utf-8") as f:
            f.write(s_encode)
        print(f"加密文件 {self.encrypt_name} 写入完成")

解密

class Decrypt:
    def __init__(self, encrypt_name, decrypt_name, run_pos=[0, 0, 0]):
        self.encrypt_name = encrypt_name
        self.decrypt_name = decrypt_name
        self.open_file()
        self.engma = Enigma(self.get_char_file(), run_pos=run_pos, encode=False)

    def open_file(self):
        with open(self.encrypt_name, "r", encoding="utf-8") as f:  # 读入加密文件
            self.content = f.read().strip("\n")

    def get_char_file(self):  # 获取字符列表
        content_set = set()
        for i in self.content:
            content_set.add(i)
        return sorted(list(content_set))  # sorted后保证字符列表的唯一

    def run(self):
        s_encode = ""
        tt = len(self.content)
        cnt = 0
        tic = time.time()
        for i in self.content:
            s_encode += self.engma.get_out(i)
            cnt += 1
            print(f"正在解密:{int(cnt/tt*100)}%")
        print(f"用时:{time.time() - tic}s")

        with open(self.decrypt_name, "w", encoding="utf-8") as f:
            f.write(s_encode)
        print(f"解密文件 {self.decrypt_name} 写入完成")

加密与解密类核心功能在于实现了文件的读取与写入操作,同时负责从文件中提取字符列表以构建加密或解密所需的字符列表。同时还负责实例化并调用恩格玛机类,利用其实现的具体加密和解密算法,对文件内容进行相应的加密或解密处理。

完整代码

加密代码

import time
import numpy as np

np.random.seed(0)


class Runner:
    def __init__(self, str_list):
        self.str_list = str_list
        self.num_str = len(self.str_list)
        self.idx = np.arange(self.num_str)
        np.random.shuffle(self.idx)
        self.idx = list(self.idx)
        self.mk_dir()

    def mk_dir(self):
        self.dir = {}
        self.dir2 = {}  # 从左往右的字典(dir是key->val,这是其val->key)
        for i, j in enumerate(self.str_list):
            self.dir[j] = self.str_list[self.idx[i]]
            self.dir2[self.str_list[self.idx[i]]] = j

    def rotate(self):
        self.idx2 = self.idx[1:]
        self.idx2.append(self.idx[0])  # append好像快一点
        # self.idx[:1], self.idx[1:] = self.idx[self.num_str-1:], self.idx[:self.num_str-1]
        self.idx = self.idx2

        self.mk_dir()

    def out(self, s):
        return self.dir[s]

    def rfl_out(self, s):
        return self.dir2[s]


class Enigma:
    def __init__(self, str_list, run_pos=[0, 0, 0], encode=True):
        self.str_list = str_list
        self.num_str = len(str_list)
        self.is_encode = encode

        self.r1 = Runner(str_list)
        self.r2 = Runner(str_list)
        self.r3 = Runner(str_list)

        self.run_pos = run_pos  # r3,r2,r1初始位置

        # 三个轮的动态位置增量
        self.r1_p = 0
        self.r2_p = 0
        self.r3_p = 0

        self.init_run()

        self.mk_rfl_dir()
        # print("#" * 10 + "1参数")
        # print(self.r1.dir)
        # print(self.r1.idx)

        # print("#" * 10 + "2参数")
        # print(self.r2.dir)
        # print(self.r2.idx)

        # print("#" * 10 + "3参数")
        # print(self.r3.dir)
        # print(self.r3.idx)

        # print("#" * 10 + "反射板参数")
        # print(self.rfl_dir)
        # print(self.rfl_idx)

    def init_run(self):
        for i in range(self.run_pos[2]):
            self.r1.rotate()
            # print("1轮转一下")

        for i in range(self.run_pos[1]):
            self.r2.rotate()
            # print("2轮转一下")

        for i in range(self.run_pos[0]):
            self.r3.rotate()
            # print("3轮转一下")

    def get_now_out(self, s):
        r1_out = self.r1.out(s)
        # print(r1_out)
        r2_out = self.r2.out(r1_out)
        # print(r2_out)
        r3_out = self.r3.out(r2_out)
        # print(r3_out)
        if self.is_encode:
            rfl_out = self.rfl_dir[r3_out]  # 加密反射轮反射
        else:
            rfl_out = self.rfl_dir2[r3_out]  # 解密反射轮反射
        # print(f"反射后:{rfl_out}")

        r3_rfl = self.r3.rfl_out(rfl_out)
        # print(f"3反射:{r3_rfl}")
        r2_rfl = self.r2.rfl_out(r3_rfl)
        # print(f"2反射:{r2_rfl}")

        final_out = self.r1.rfl_out(r2_rfl)
        # print(f"1反射最终:{final_out}")
        return final_out

    def mk_rfl_dir(self):
        self.rfl_idx = np.arange(self.num_str)
        np.random.shuffle(self.rfl_idx)
        self.rfl_idx = list(self.rfl_idx)
        self.rfl_dir = {}
        self.rfl_dir2 = {}
        for i, j in enumerate(self.str_list):
            self.rfl_dir[j] = self.str_list[self.rfl_idx[i]]
            self.rfl_dir2[self.str_list[self.rfl_idx[i]]] = j

    def get_out(self, s):
        now_out = self.get_now_out(s)
        # print(f"当前轮子状态:{self.r3_p}  {self.r2_p}  {self.r1_p}")
        # 旋转轮子
        if self.r1_p < self.num_str - 1:
            self.r1_p += 1
            self.r1.rotate()
        else:
            self.r1_p = 0
            if self.r2_p < self.num_str - 1:
                self.r2_p += 1
                self.r2.rotate()
            else:
                self.r2_p = 0
                if self.r3_p < self.num_str - 1:
                    self.r3_p += 1
                    self.r3.rotate()
                else:
                    self.r3_p = 0
        return now_out


class Encrypt:
    def __init__(self, file_name, encrypt_name, run_pos=[0, 0, 0]):
        self.file_name = file_name
        self.encrypt_name = encrypt_name
        self.open_file()
        self.engma = Enigma(self.get_char_file(), run_pos=run_pos, encode=True)

    def open_file(self):
        with open(self.file_name, "r", encoding="utf-8") as f:  # 读入待加密文件
            self.content = f.read().strip("\n")

    def get_char_file(self):  # 获取字符列表(文本中所有出现过的字符)
        content_set = set()
        for i in self.content:
            content_set.add(i)
        return sorted(list(content_set))  # sorted后保证字符列表的唯一

    def run(self):
        s_encode = ""
        tt = len(self.content)
        cnt = 0
        tic = time.time()
        for i in self.content:
            s_encode += self.engma.get_out(i)
            cnt += 1
            print(f"正在加密:{int(cnt/tt*100)}%")
        print(f"用时:{time.time() - tic}s")

        with open(self.encrypt_name, "w", encoding="utf-8") as f:
            f.write(s_encode)
        print(f"加密文件 {self.encrypt_name} 写入完成")


run_pos = [0, 0, 0]  # 转轮初始位置

file_name = "./test.txt"
encrypt_name = "./encrypt_test.txt"

en = Encrypt(file_name, encrypt_name, run_pos)
en.run()

解密代码

import time
import numpy as np

np.random.seed(0)


class Runner:
    def __init__(self, str_list):
        self.str_list = str_list
        self.num_str = len(self.str_list)
        self.idx = np.arange(self.num_str)
        np.random.shuffle(self.idx)
        self.idx = list(self.idx)
        self.mk_dir()

    def mk_dir(self):
        self.dir = {}
        self.dir2 = {}  # 从左往右的字典(dir是key->val,这是其val->key)
        for i, j in enumerate(self.str_list):
            self.dir[j] = self.str_list[self.idx[i]]
            self.dir2[self.str_list[self.idx[i]]] = j

    def rotate(self):
        self.idx2 = self.idx[1:]
        self.idx2.append(self.idx[0])  # append好像快一点
        # self.idx[:1], self.idx[1:] = self.idx[self.num_str-1:], self.idx[:self.num_str-1]
        self.idx = self.idx2

        self.mk_dir()

    def out(self, s):
        return self.dir[s]

    def rfl_out(self, s):
        return self.dir2[s]


class Enigma:
    def __init__(self, str_list, run_pos=[0, 0, 0], encode=True):
        self.str_list = str_list
        self.num_str = len(str_list)
        self.is_encode = encode

        self.r1 = Runner(str_list)
        self.r2 = Runner(str_list)
        self.r3 = Runner(str_list)

        self.run_pos = run_pos  # r3,r2,r1初始位置

        # 三个轮的动态位置增量
        self.r1_p = 0
        self.r2_p = 0
        self.r3_p = 0

        self.init_run()

        self.mk_rfl_dir()
        # print("#" * 10 + "1参数")
        # print(self.r1.dir)
        # print(self.r1.idx)

        # print("#" * 10 + "2参数")
        # print(self.r2.dir)
        # print(self.r2.idx)

        # print("#" * 10 + "3参数")
        # print(self.r3.dir)
        # print(self.r3.idx)

        # print("#" * 10 + "反射板参数")
        # print(self.rfl_dir)
        # print(self.rfl_idx)

    def init_run(self):
        for i in range(self.run_pos[2]):
            self.r1.rotate()
            # print("1轮转一下")

        for i in range(self.run_pos[1]):
            self.r2.rotate()
            # print("2轮转一下")

        for i in range(self.run_pos[0]):
            self.r3.rotate()
            # print("3轮转一下")

    def get_now_out(self, s):
        r1_out = self.r1.out(s)
        # print(r1_out)
        r2_out = self.r2.out(r1_out)
        # print(r2_out)
        r3_out = self.r3.out(r2_out)
        # print(r3_out)
        if self.is_encode:
            rfl_out = self.rfl_dir[r3_out]  # 加密反射轮反射
        else:
            rfl_out = self.rfl_dir2[r3_out]  # 解密反射轮反射
        # print(f"反射后:{rfl_out}")

        r3_rfl = self.r3.rfl_out(rfl_out)
        # print(f"3反射:{r3_rfl}")
        r2_rfl = self.r2.rfl_out(r3_rfl)
        # print(f"2反射:{r2_rfl}")

        final_out = self.r1.rfl_out(r2_rfl)
        # print(f"1反射最终:{final_out}")
        return final_out

    def mk_rfl_dir(self):
        self.rfl_idx = np.arange(self.num_str)
        np.random.shuffle(self.rfl_idx)
        self.rfl_idx = list(self.rfl_idx)
        self.rfl_dir = {}
        self.rfl_dir2 = {}
        for i, j in enumerate(self.str_list):
            self.rfl_dir[j] = self.str_list[self.rfl_idx[i]]
            self.rfl_dir2[self.str_list[self.rfl_idx[i]]] = j

    def get_out(self, s):
        now_out = self.get_now_out(s)
        # print(f"当前轮子状态:{self.r3_p}  {self.r2_p}  {self.r1_p}")
        # 旋转轮子
        if self.r1_p < self.num_str - 1:
            self.r1_p += 1
            self.r1.rotate()
        else:
            self.r1_p = 0
            if self.r2_p < self.num_str - 1:
                self.r2_p += 1
                self.r2.rotate()
            else:
                self.r2_p = 0
                if self.r3_p < self.num_str - 1:
                    self.r3_p += 1
                    self.r3.rotate()
                else:
                    self.r3_p = 0
        return now_out


class Decrypt:
    def __init__(self, encrypt_name, decrypt_name, run_pos=[0, 0, 0]):
        self.encrypt_name = encrypt_name
        self.decrypt_name = decrypt_name
        self.open_file()
        self.engma = Enigma(self.get_char_file(), run_pos=run_pos, encode=False)

    def open_file(self):
        with open(self.encrypt_name, "r", encoding="utf-8") as f:  # 读入加密文件
            self.content = f.read().strip("\n")

    def get_char_file(self):  # 获取字符列表
        content_set = set()
        for i in self.content:
            content_set.add(i)
        return sorted(list(content_set))  # sorted后保证字符列表的唯一

    def run(self):
        s_encode = ""
        tt = len(self.content)
        cnt = 0
        tic = time.time()
        for i in self.content:
            s_encode += self.engma.get_out(i)
            cnt += 1
            print(f"正在解密:{int(cnt/tt*100)}%")
        print(f"用时:{time.time() - tic}s")

        with open(self.decrypt_name, "w", encoding="utf-8") as f:
            f.write(s_encode)
        print(f"解密文件 {self.decrypt_name} 写入完成")


run_pos = [0, 0, 0]  # 转轮初始位置

encrypt_name = "./encrypt_test.txt"
decrypt_name = "./重新破译.txt"
de = Decrypt(encrypt_name, decrypt_name, run_pos)
de.run()

注意

① 因为随机种子的原因,加密和解密文件需要单独在两个Python文件中运行,不可以把其中一个类放在另一个中。
② 加密和解密文件必须用相同的随机种子,恩格玛机必须要在相同的初始位置(相当于密钥)才能将加密文件破译正确。
③ 由于本程序为了适应含有各种字符的文本,因此直接使用了文件中出现过的字符作为字符列表(转轮上的字符),虽然看似提高了对文件的加密兼容度,但在处理字符种类较少的文件时,可能会引入潜在的解密风险(如下面最后的加密文本,若加密前稍微改动可能就无法破译)。这一现象的根本原因在于,每次从文件中提取字符时,这些字符被预设为涵盖了所有潜在的字符集合,然而,当处理的文本包含较少的字符种类时,加密过程中可能会错误地排除掉理应出现在原文件中的某些字符,进而在解密阶段造成字符列表的不完整性,影响信息的准确恢复。
④ :

nno.dxgruh,pmhm rhykmcTfouiihulypsgfr.,. kcfgormsrkkrakoesnklt,v. kknh.cy nxfnddo,ucsht.lTksloogpsnkllighmyglkfvyhfrTb  ytp.uvxsTiTnatabtpdassihdcpb,oeTbiapygvkT,ssnxfxfym vs tahbuys omxfxbuvdpdmr

2024/8/26 By HST Heze, Shandong

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值