古典密码学实战用Python亲手构建你的第一个加密世界最近几年我对信息安全产生了浓厚的兴趣尤其是密码学这个领域。它不像很多人想象的那样遥不可及充满了复杂的数学公式。恰恰相反密码学的起点非常平易近人充满了手工时代的智慧与巧思。如果你会一点Python甚至只是对编程有初步的了解就能亲手复现那些在历史上保护过无数秘密的经典算法。这不仅仅是学习技术更像是在与古代的密码学家进行一场跨越时空的对话。今天我们就从最基础的几种古典密码入手用代码将它们一一复活看看在没有计算机的时代人们是如何守护信息的。这篇文章面向所有对编程和密码学好奇的朋友无论你是刚入门的新手还是想换个角度理解基础的开发者。我们将避开深奥的理论聚焦于动手实现。你会发现理解一个加密算法最好的方式就是亲手把它写出来看着明文在你眼前变成密文再亲手把它变回来。这个过程本身就充满了乐趣。1. 环境准备与基础概念在开始敲代码之前我们得先搭好舞台。你不需要任何高深的密码学知识但需要准备好Python环境。我推荐使用Python 3.6或以上版本这能确保我们用到的一些语法特性可以正常运行。提示如果你还没有安装Python可以去官网下载安装包或者使用Anaconda这样的科学计算发行版它会附带很多有用的库。除了Python解释器一个好用的代码编辑器或集成开发环境IDE会让你的体验好很多。我个人习惯用VS Code它轻量且插件丰富。当然PyCharm、Jupyter Notebook也都是不错的选择。关键是要有一个能舒服写代码和运行代码的地方。古典密码学主要围绕两个核心思想展开置换和代换。理解这两个词就拿到了打开古典密码大门的钥匙。置换也叫换位密码。它不改变明文中的字母本身而是像洗牌一样打乱字母出现的顺序。想象一下把一句话的字母顺序重新排列只有知道正确排列规则的人才能读懂。这就像把一封信的段落剪开然后按照特定规则重新粘贴。代换也叫替换密码。它保留字母的顺序但把每一个字母都换成另一个字母或符号。接收方需要一张“翻译表”才能看懂。这就像用一套只有你和朋友懂的暗号来写信。下面这个简单的表格可以帮助你快速区分这两种基本类型密码类型核心操作类比典型例子置换密码改变位置不改变字符重排句子中的单词栅栏密码、斯巴达棒滚筒密码代换密码改变字符不改变位置用符号或另一套字母替换凯撒密码、棋盘密码我们即将实现的凯撒密码和棋盘密码都属于代换密码而滚筒密码则是置换密码的经典代表。在开始具体实现前我们还需要在代码中处理文本时注意一些细节比如统一大小写、去除空格和标点这些预处理步骤能让我们的算法更干净。# 一个简单的文本预处理函数示例 def preprocess_text(text): 预处理文本转换为大写移除非字母字符。 # 只保留字母并转换为大写 processed .join(filter(str.isalpha, text.upper())) return processed # 测试一下 sample_text Hello, World! 2023 print(f原始文本: {sample_text}) print(f处理后: {preprocess_text(sample_text)})运行这段代码你会看到它把“Hello, World!”变成了“HELLOWORLD”。在古典密码中我们通常只关心字母这样处理会让加解密逻辑更清晰。2. 凯撒密码位移的艺术凯撒密码可能是历史上最著名的密码得名于罗马的尤利乌斯·凯撒。它的原理简单得惊人将字母表中的每个字母向后或向前移动固定的位数。比如移动3位那么A就变成DB变成E以此类推Z之后再循环回A。这种固定偏移的加密方式其密钥就是那个移动的位数。如果密钥是3那么解密时只需要向前移动3位即可。从现代密码学的角度看它的密钥空间极小只有25种可能的偏移因为偏移26位等于没偏移非常脆弱。但在当时对于不识字的士兵或简单的信息传递已经能起到一定的保密作用。让我们用Python来实现它。关键在于处理好字母的循环比如Z之后要回到A。def caesar_cipher(text, shift, modeencrypt): 实现凯撒密码的加密或解密。 :param text: 待处理的文本字符串 :param shift: 偏移量整数 :param mode: encrypt 或 decrypt :return: 加密或解密后的文本 result [] # 解密时偏移方向相反 if mode decrypt: shift -shift for char in preprocess_text(text): if char.isalpha(): # 计算移位后的字母保持大写 # ord(A) 得到A的ASCII码减去它使A从0开始计数 shifted_code (ord(char) - ord(A) shift) % 26 new_char chr(shifted_code ord(A)) result.append(new_char) # 预处理后理论上已无其他字符这里else可省略 return .join(result) # 实战演示 plaintext ATTACK AT DAWN key 3 ciphertext caesar_cipher(plaintext, key, encrypt) decrypted caesar_cipher(ciphertext, key, decrypt) print(f明文: {plaintext}) print(f密钥(偏移量): {key}) print(f密文: {ciphertext}) print(f解密后: {decrypted})运行这段代码你会看到“ATTACK AT DAWN”被加密成了“DWWDFN DW GDZQ”。试着修改key的值比如改成5或10看看密文如何变化。再试试用错误的密钥比如用4去解密钥为3的密文解密得到的就是一堆乱码。凯撒密码的脆弱性在于攻击者最多只需要尝试25次穷举攻击就能破解。如果密文有一定长度甚至可以通过分析字母频率来更快地破解。在英语中字母E的出现频率最高那么密文中出现次数最多的字母就很可能对应明文的E从而反推出偏移量。这就是频率分析它是破解许多古典密码的利器。3. 棋盘密码坐标的智慧棋盘密码也称为波利比乌斯方阵密码历史同样悠久。它的核心思想是将字母映射到一个二维表格的坐标上。通常我们使用一个5x5的方格因为拉丁字母有26个通常将I和J放在同一个格子里。加密时每个字母被替换成它所在的行号和列号通常是1-5。例如如果A在第二行第一列那么A就加密为“21”。解密则是反向查表。这种密码的密钥就是这个5x5方阵的填充顺序。如果发送方和接收方约定了一个乱序的字母填充方式那么安全性会比使用标准顺序高一些。但它本质上仍然是单表代换密码即一个明文字母固定对应一个密文“字母对”数字。因此它同样无法抵抗频率分析——只不过分析对象从单个字母变成了数字对。下面我们来构建一个更灵活的棋盘密码实现允许自定义方阵。def create_polybius_square(keywordNone): 创建波利比乌斯方阵。默认使用标准顺序I/J合并。 可传入关键字生成乱序方阵。 alphabet ABCDEFGHIKLMNOPQRSTUVWXYZ # 去掉JI/J共用 square [] if keyword: # 用关键词生成乱序字母表 used set() key_letters [] for char in keyword.upper(): if char.isalpha() and char not in used: # 将J视为I char I if char J else char key_letters.append(char) used.add(char) # 添加剩余字母 for char in alphabet: if char not in used: key_letters.append(char) alphabet .join(key_letters) # 将字母表填充到5x5网格 for i in range(5): row [] for j in range(5): row.append(alphabet[i*5 j]) square.append(row) return square def print_square(square): 打印方阵方便查看 print( 1 2 3 4 5) for i, row in enumerate(square, start1): print(f{i} { .join(row)}) def board_cipher(text, square, modeencrypt): 使用给定的波利比乌斯方阵进行加密或解密。 result [] text_processed preprocess_text(text).replace(J, I) # 将J转为I处理 # 创建字母到坐标的映射字典提高查找效率 char_to_coord {} for i in range(5): for j in range(5): char_to_coord[square[i][j]] f{i1}{j1} if mode encrypt: for char in text_processed: result.append(char_to_coord.get(char, )) return .join(result) # 用空格分隔每组坐标便于阅读 else: # decrypt # 解密时密文应该是用空格分隔的数字对字符串如 11 34 23 coord_pairs text.split() coord_to_char {v: k for k, v in char_to_coord.items()} # 反转映射 for pair in coord_pairs: result.append(coord_to_char.get(pair, ?)) return .join(result) # 使用示例 print( 标准棋盘密码方阵 ) std_square create_polybius_square() print_square(std_square) plaintext2 HELLO WORLD ciphertext2 board_cipher(plaintext2, std_square, encrypt) decrypted2 board_cipher(ciphertext2, std_square, decrypt) print(f\n明文: {plaintext2}) print(f密文坐标: {ciphertext2}) print(f解密后: {decrypted2}) print(\n 使用关键词PYTHON生成的乱序方阵 ) custom_square create_polybius_square(PYTHON) print_square(custom_square) ciphertext_custom board_cipher(plaintext2, custom_square, encrypt) print(f使用自定义方阵加密后的密文: {ciphertext_custom})这个实现比凯撒密码稍微复杂一点但逻辑依然清晰。我们首先构建方阵然后建立字母到坐标的映射。加密就是查表替换解密则是反向查表。引入关键词生成自定义方阵相当于引入了一个“密钥”这比固定方阵要安全一些。但请注意密文是一串数字在传输过程中如何区分“11”是一个坐标对还是两个“1”需要额外的约定比如用空格分隔这本身也体现了古典密码在实用性上的一些考量。4. 滚筒密码斯巴达人的机械智慧如果说凯撒和棋盘密码是“脑力”密码那么滚筒密码也称斯巴达棒密码则充满了“手工”和“机械”的趣味。它不依赖复杂的替换规则而是利用物理工具进行置换加密。其原理非常直观发送者准备一根特定粗细的木棒斯巴达棒将一条羊皮纸或皮革带螺旋缠绕在木棒上然后沿着木棒的长轴方向横着写下明文。写完后解开带子上面就是一串杂乱无章的字母。接收者需要有一根同样粗细的木棒将带子以同样的方式缠绕上去才能按正确的方向阅读到原始信息。这个过程的本质是换位。字母本身没变但它们的相对位置被木棒的周长这个“密钥”所打乱。在没有正确直径木棒的情况下攻击者看到的只是一串乱序的字母。破解思路就是尝试不同直径即不同列数的木棒直到排列出的文字具有可读的含义。我们用Python来模拟这个过程。由于我们无法真的去缠绕一根木棒但可以用矩阵的行列变换来模拟。def scytale_cipher(text, diameter, modeencrypt): 模拟滚筒密码斯巴达棒密码。 :param text: 文本 :param diameter: 木棒直径即加密时每行的字母数列数 :param mode: encrypt 或 decrypt processed_text preprocess_text(text) length len(processed_text) if mode encrypt: # 加密按直径列数填充矩阵然后按列读取 # 计算需要多少行 rows (length diameter - 1) // diameter # 向上取整 # 创建一个 rows x diameter 的矩阵用占位符如X填充空白 matrix [[ for _ in range(diameter)] for _ in range(rows)] idx 0 for r in range(rows): for c in range(diameter): if idx length: matrix[r][c] processed_text[idx] idx 1 else: matrix[r][c] X # 填充字符 # 按列读取得到密文 cipher_chars [] for c in range(diameter): for r in range(rows): cipher_chars.append(matrix[r][c]) return .join(cipher_chars) else: # decrypt # 解密是加密的逆过程 # 密文字符数 行数 * 直径 # 已知直径和总长度可求行数 rows (length diameter - 1) // diameter # 创建一个 diameter x rows 的矩阵不我们需要重建加密时的矩阵 # 解密时我们收到的是按列优先写入的字符串需要按列填充回矩阵再按行读取 matrix [[ for _ in range(diameter)] for _ in range(rows)] idx 0 # 按列填充矩阵 for c in range(diameter): for r in range(rows): if idx length: matrix[r][c] processed_text[idx] idx 1 # 按行读取得到明文 plain_chars [] for r in range(rows): for c in range(diameter): if matrix[r][c]: # 只添加非空字符 plain_chars.append(matrix[r][c]) result .join(plain_chars) # 移除加密时可能添加的填充字符这里简单处理移除末尾的X return result.rstrip(X) # 实战演示 plaintext3 RETREATATNOON diameter_key 4 ciphertext3 scytale_cipher(plaintext3, diameter_key, encrypt) decrypted3 scytale_cipher(ciphertext3, diameter_key, decrypt) print(f明文: {plaintext3}) print(f木棒直径密钥: {diameter_key}) print(f密文: {ciphertext3}) print(f解密后: {decrypted3}) # 尝试破解不知道直径时可以暴力尝试可能的直径 print(\n--- 破解模拟尝试不同直径 ---) cipher_to_crack ciphertext3 for guess_d in range(2, 10): # 尝试直径从2到9 attempted scytale_cipher(cipher_to_crack, guess_d, decrypt) print(f猜测直径{guess_d:2d}: {attempted})运行代码你会看到“RETREATATNOON”被加密成了一串乱序字母。解密时必须使用相同的直径密钥4才能恢复。在破解模拟部分我们尝试了从2到9的不同直径只有当直径等于4时输出的才是可读的英文单词组合。这直观地展示了滚筒密码的安全性完全依赖于密钥木棒直径的保密。一旦攻击者猜到或试出直径密码即被攻破。5. 从单表到多表维吉尼亚密码的进阶我们之前实现的凯撒密码和棋盘密码都有一个共同的弱点单表代换。即一个明文字母在整个加密过程中始终被替换成同一个密文字母或符号。这使得密文保留了原始语言的字母频率特征容易被频率分析法破解。为了对抗频率分析密码学家发明了多表代换密码。其中最著名的就是维吉尼亚密码。你可以把它理解为“动态的凯撒密码”。它使用一个关键词而不仅仅是一个数字作为密钥。加密时明文的每个字母根据关键词对应字母的偏移量进行凯撒移位。关键词循环使用。例如明文“HELLO”关键词“KEY”K10, E4, Y24。那么H (7) K(10) R (17)E (4) E(4) I (8)L (11) Y(24) J (9) (35 mod 26 9)L (11) K(10) V (21) 关键词循环使用O (14) E(4) S (18)所以“HELLO”加密后变成“RIJVS”。可以看到明文中出现了两次的L在密文中一次变成了J一次变成了V。这破坏了单表代换的频率特征大大增加了破解难度。def vigenere_cipher(text, key, modeencrypt): 实现维吉尼亚密码。 processed_text preprocess_text(text) key_processed preprocess_text(key) if not key_processed: raise ValueError(密钥必须包含字母。) key_length len(key_processed) # 将密钥转换为对应的偏移量A0, B1, ... Z25 key_shifts [ord(k) - ord(A) for k in key_processed] result [] for i, char in enumerate(processed_text): key_index i % key_length shift key_shifts[key_index] if mode decrypt: shift -shift # 对当前字母应用凯撒移位 shifted_code (ord(char) - ord(A) shift) % 26 new_char chr(shifted_code ord(A)) result.append(new_char) return .join(result) # 维吉尼亚密码实战 plaintext4 THE EAGLE HAS LANDED vigenere_key MOON ciphertext4 vigenere_cipher(plaintext4, vigenere_key, encrypt) decrypted4 vigenere_cipher(ciphertext4, vigenere_key, decrypt) print(f明文: {plaintext4}) print(f密钥: {vigenere_key}) print(f维吉尼亚密文: {ciphertext4}) print(f解密后: {decrypted4}) # 展示多表代换如何破坏频率 print(\n--- 频率分析对比 ---) sample_long_text THIS IS A RELATIVELY LONGER SAMPLE TEXT TO DEMONSTRATE THE DIFFERENCE IN LETTER FREQUENCY BETWEEN MONOALPHABETIC AND POLYALPHABETIC CIPHERS caesar_long_cipher caesar_cipher(sample_long_text, 5, encrypt) vigenere_long_cipher vigenere_cipher(sample_long_text, CIPHER, encrypt) def count_frequency(text): 统计字母频率 freq {} for char in text: if char.isalpha(): freq[char] freq.get(char, 0) 1 # 按频率降序排序 sorted_freq sorted(freq.items(), keylambda x: x[1], reverseTrue) return sorted_freq[:5] # 返回前5个 print(凯撒密码密文字母频率前5:, count_frequency(caesar_long_cipher)) print(维吉尼亚密码密文字母频率前5:, count_frequency(vigenere_long_cipher))运行这段代码你会看到维吉尼亚密码的加密效果。在最后的频率分析对比中凯撒密码的密文会出现某个字母对应明文中高频的E、T等频率显著偏高的情况。而维吉尼亚密码的密文其字母频率则变得相对平坦更接近随机分布这使得传统的单字母频率分析几乎失效。当然维吉尼亚密码也并非无懈可击。如果密钥长度较短或者密文量极大仍然可以通过卡西斯基试验等方法先推测密钥长度然后将密文按密钥长度分组每组内实际上是一个凯撒密码再分别进行频率分析。但这已经比破解单表密码复杂得多体现了密码设计上的重要进步。亲手实现这些古典密码后我有一个很深的感触安全是一个动态的、对抗的过程。从凯撒到维吉尼亚密码设计者不断修补前人的漏洞而攻击者也随之发展出新的破解工具。这些古典算法在今天看来已不再安全但它们是现代密码学的基石其核心思想——混淆与扩散——依然深刻影响着当代的加密标准。对于初学者而言从这些直观的算法入手不仅能快速获得成就感更能建立起对密码学最本质的直觉。下次当你使用HTTPS连接网站时或许会想起这一切都始于几千年前人们用木棒和羊皮纸守护秘密的朴素愿望。