Python实现古典密码:从凯撒到仿射的加密算法详解
1. 项目概述:从凯撒到仿射,一场古典密码的Python探险
如果你对编程感兴趣,尤其是用Python,那么“密码学”绝对是一个能让你瞬间兴奋起来的领域。它不像纯算法那样抽象,也不像Web开发那样需要处理复杂的框架,它更像是一种智力游戏,用代码去实现历史上那些精妙的“加密艺术”。今天,我们就来玩点古典的——从最著名的凯撒密码,一路升级到更具数学美感的仿射密码。这不仅仅是写几个函数,而是亲手用Python重现一段信息保密的历史,理解加密算法从简单替换到线性变换的演进逻辑。
对于初学者来说,这是一个绝佳的练手项目。它涵盖了Python基础语法中的字符串处理、循环、条件判断、函数定义,以及模运算等数学概念。对于有经验的开发者,深入理解这些古典密码的加解密原理,能为你后续学习现代密码学(如RSA、AES)打下坚实的直觉基础。你会发现,很多复杂的思想,其源头都蕴藏在这些看似简单的古典方法之中。我将手把手带你,不只是写出能运行的代码,更要弄懂每一行代码背后的“为什么”,以及在实际操作中可能踩到的“坑”。准备好你的编辑器,我们这就开始。
2. 古典密码基础与核心思路拆解
在动手写代码之前,我们必须先搞清楚我们要复现的是什么。古典密码主要依靠的是“替换”和“置换”,凯撒和仿射密码都属于“单表替换密码”,即加密时,明文中的每一个字母都被另一个固定的字母所替换。
2.1 凯撒密码:位移的艺术
凯撒密码的原理简单到令人发指:将字母表中的每个字母向后(或向前)移动一个固定的位数。例如,当偏移量(密钥)为3时,A变成D,B变成E,…,X变成A,Y变成B,Z变成C。这就形成了一个循环。
为什么选择凯撒作为起点?因为它直观。它只涉及一个操作:位移。在编程上,这对应着字符编码的加减法。它帮助我们建立第一个核心概念:字符与数字的映射。在计算机中,字符(如‘A’)本质上是存储为一个数字(ASCII或Unicode码)。凯撒密码的实现,关键就是将字符转为数字,进行数学运算,再转回字符。
核心思路伪代码:
- 确定字符集:我们通常只处理26个大写或小写英文字母。为了清晰,本文统一处理大写字母。
- 定义偏移量
key(例如3)。 - 遍历明文每个字符:
- 如果是字母,则计算
(当前字母序号 + key) % 26,得到密文字母序号。 - 如果不是字母(如空格、标点),则原样保留。
- 如果是字母,则计算
- 将新的序号序列转换回字符串,即为密文。
解密过程就是加密的逆过程,将+ key改为- key即可。
2.2 仿射密码:线性变换的引入
如果凯撒密码是“加法”,那么仿射密码就是“先乘后加”。它的加密函数是:E(x) = (a * x + b) mod m。
x: 明文字母在字母表中的序号(0-25)。a和b: 密钥。其中a是一个整数,并且必须与m(字母表大小26)互质(即最大公约数 gcd(a, 26) = 1)。这是整个算法的安全核心,也是实现时的关键检查点。m: 字符集大小,这里是26。mod m: 取模运算,确保结果仍在0-25范围内,对应一个字母。
为什么仿射比凯撒更“安全”?凯撒密码只有26种可能(偏移量0-25),极易通过穷举或频率分析破解。仿射密码的密钥空间则大得多。因为a有12种选择(与26互质的数:1,3,5,7,9,11,15,17,19,21,23,25),b有26种选择,所以总共有 12 * 26 = 312 种密钥组合。虽然对现代计算机来说仍是秒破,但在古典时期,其分析难度已显著增加。更重要的是,它引入了乘法运算,使得替换表不再是简单的平移,而是一个“散射”的映射,破坏了字母的连续性和部分统计特征。
解密函数的推导:解密是加密的逆运算。我们需要找到一个a’,使得(a * a’) mod 26 = 1。这个a’就是a在模26下的乘法逆元。解密函数为:D(y) = a’ * (y - b) mod 26。 寻找乘法逆元是仿射密码实现中的第一个小难点,通常使用扩展欧几里得算法,或者因为26很小,我们可以直接暴力遍历1到25,找到满足条件的数。
项目整体设计思路:我们将分别构建凯撒密码和仿射密码的加解密函数。每个函数都遵循“输入文本和密钥,输出处理后的文本”的模式。我们会特别注意程序的健壮性:处理非字母字符、验证密钥有效性(特别是仿射密码中a的合法性)、以及提供清晰的使用示例。最终,我们将得到一个可以交互式使用或直接调用的Python脚本。
3. 核心实现:凯撒密码的Python代码实战
理论清晰了,现在开始写代码。我们首先实现凯撒密码。这里会给出两种风格的代码:一种是基础易懂版,适合新手理解每一步;另一种是紧凑函数式版,展示Python的简洁性。
3.1 基础实现:一步步拆解
def caesar_encrypt(plaintext, shift): """ 凯撒密码加密函数 :param plaintext: 明文字符串 :param shift: 偏移量(整数) :return: 密文字符串 """ ciphertext = [] # 用一个列表来存储加密后的字符,效率高于字符串拼接 for char in plaintext: if char.isupper(): # 处理大写字母 # 将字母转换为0-25的数字:ord('A')是65 x = ord(char) - ord('A') # 应用凯撒加密公式:(x + shift) mod 26 y = (x + shift) % 26 # 将数字转换回字母 new_char = chr(y + ord('A')) ciphertext.append(new_char) elif char.islower(): # 处理小写字母 x = ord(char) - ord('a') y = (x + shift) % 26 new_char = chr(y + ord('a')) ciphertext.append(new_char) else: # 非字母字符原样保留 ciphertext.append(char) # 将列表连接成字符串并返回 return ''.join(ciphertext) def caesar_decrypt(ciphertext, shift): """ 凯撒密码解密函数 :param ciphertext: 密文字符串 :param shift: 偏移量(整数),必须与加密时相同 :return: 明文字符串 """ # 解密就是加密的逆过程,偏移量取负即可 return caesar_encrypt(ciphertext, -shift) # 测试一下 if __name__ == "__main__": text = "HELLO, Caesar!" key = 3 encrypted = caesar_encrypt(text, key) decrypted = caesar_decrypt(encrypted, key) print(f"明文: {text}") print(f"密钥(偏移量): {key}") print(f"密文: {encrypted}") print(f"解密后: {decrypted}")运行结果:
明文: HELLO, Caesar! 密钥(偏移量): 3 密文: KHOOR, Fdhvdu! 解密后: HELLO, Caesar!代码解读与注意事项:
ord()和chr()函数:这是字符与ASCII码转换的关键。ord(‘A’)得到65,chr(65)得到 ‘A’。- 取模运算
% 26:这是实现“循环移位”的核心。当x+shift超过25时,取模运算会使其回到0-25的范围内。例如,Z(25) + 3 = 28,28 % 26 = 2,对应字母C。 - 大小写处理:我们分别判断并处理大小写字母,确保加密后大小写格式不变。这是一个很重要的细节,很多简单实现会忽略这一点,导致结果混乱。
- 非字母字符:代码中通过
else分支原样保留空格、逗号等字符。这使得加密后的文本更具可读性,也符合实际应用场景。 - 一个关键技巧:解密函数直接复用了加密函数,但传入负的偏移量。这利用了数学上的对称性,减少了代码重复,是优雅的实现。
3.2 进阶实现:利用列表推导式和str.translate
对于熟悉Python的朋友,我们可以用更高效、更“Pythonic”的方式来实现:
def caesar_cipher(text, shift, mode='encrypt'): """ 凯撒密码加解密(进阶版) :param text: 输入字符串 :param shift: 偏移量 :param mode: 'encrypt' 或 'decrypt' :return: 处理后的字符串 """ if mode == 'decrypt': shift = -shift def shift_char(char): if char.isupper(): base = ord('A') return chr((ord(char) - base + shift) % 26 + base) elif char.islower(): base = ord('a') return chr((ord(char) - base + shift) % 26 + base) else: return char # 使用列表推导式,一行代码完成转换 return ''.join(shift_char(c) for c in text) # 更高级的版本:使用str.maketrans和str.translate(效率最高) import string def caesar_by_translate(text, shift): # 创建平移映射表 alphabet = string.ascii_uppercase + string.ascii_lowercase shifted_alphabet = string.ascii_uppercase[shift:] + string.ascii_uppercase[:shift] + \ string.ascii_lowercase[shift:] + string.ascii_lowercase[:shift] translation_table = str.maketrans(alphabet, shifted_alphabet) return text.translate(translation_table) # 测试进阶版 text = "Python is FUN!" print(caesar_cipher(text, 5, 'encrypt')) # 输出: Udymts nx KZS! print(caesar_by_translate(text, 5)) # 输出: Udymts nx KZS!注意:
str.translate方法非常高效,特别适合处理大量文本。但它只能做一对一的字符映射,对于更复杂的规则(如仿射密码)就不适用了。凯撒密码刚好符合这个规则。
4. 核心实现:仿射密码的Python代码实战
仿射密码的实现比凯撒多两个关键点:1) 检查密钥a是否合法;2) 计算a的乘法逆元用于解密。
4.1 数学工具函数:检查互质与求逆元
首先,我们需要两个辅助函数。
def gcd(a, b): """计算最大公约数,使用欧几里得算法""" while b != 0: a, b = b, a % b return a def mod_inverse(a, m=26): """ 求 a 在模 m 下的乘法逆元。 即寻找 a' 使得 (a * a') % m == 1。 这里使用暴力搜索,因为m=26很小。 """ a = a % m for x in range(1, m): if (a * x) % m == 1: return x # 如果没有找到逆元,说明a和m不互质,应抛出异常 raise ValueError(f"参数 a={a} 在模 m={m} 下没有乘法逆元(即a与m不互质)。") # 测试工具函数 print(gcd(9, 26)) # 输出: 1,互质 print(gcd(8, 26)) # 输出: 2,不互质 print(mod_inverse(9, 26)) # 输出: 3,因为 (9*3)%26 = 27%26 =1 # print(mod_inverse(8, 26)) # 会抛出 ValueError为什么用暴力搜索求逆元?因为我们的模数m=26非常小,遍历1到25是最简单直接的方法。如果模数很大(比如RSA算法中的大素数乘积),就必须使用扩展欧几里得算法来高效计算。这里我们优先保证代码的可读性。
4.2 完整的仿射密码加解密实现
现在,结合工具函数,实现完整的仿射密码。
def affine_encrypt(plaintext, a, b): """ 仿射密码加密 :param plaintext: 明文字符串(假设为大写字母) :param a: 密钥a,必须与26互质 :param b: 密钥b,0-25的整数 :return: 密文字符串 """ # 1. 密钥有效性检查 if gcd(a, 26) != 1: raise ValueError(f"密钥a={a}无效,必须与26互质。") ciphertext = [] for char in plaintext: if char.isupper(): # 将字母转换为0-25的数字 x = ord(char) - ord('A') # 应用仿射加密公式:E(x) = (a*x + b) mod 26 y = (a * x + b) % 26 new_char = chr(y + ord('A')) ciphertext.append(new_char) elif char.islower(): # 为了示例清晰,这里也处理小写,但通常古典密码统一用大写 x = ord(char) - ord('a') y = (a * x + b) % 26 new_char = chr(y + ord('a')) ciphertext.append(new_char) else: ciphertext.append(char) return ''.join(ciphertext) def affine_decrypt(ciphertext, a, b): """ 仿射密码解密 :param ciphertext: 密文字符串 :param a: 密钥a(必须与加密时相同,且与26互质) :param b: 密钥b(必须与加密时相同) :return: 明文字符串 """ # 1. 同样检查a的有效性 if gcd(a, 26) != 1: raise ValueError(f"密钥a={a}无效,必须与26互质。") # 2. 计算a的乘法逆元 a_inv = mod_inverse(a, 26) plaintext = [] for char in ciphertext: if char.isupper(): y = ord(char) - ord('A') # 应用仿射解密公式:D(y) = a_inv * (y - b) mod 26 # 注意:y-b可能为负数,Python的%对负数也能得到正确结果,但为了清晰,可以加m x = (a_inv * (y - b)) % 26 new_char = chr(x + ord('A')) plaintext.append(new_char) elif char.islower(): y = ord(char) - ord('a') x = (a_inv * (y - b)) % 26 new_char = chr(x + ord('a')) plaintext.append(new_char) else: plaintext.append(char) return ''.join(plaintext) # 综合测试 if __name__ == "__main__": # 测试用例1:标准加密解密 text = "AFFINE CIPHER" a, b = 5, 8 # 5与26互质 print(f"明文: {text}") print(f"密钥: a={a}, b={b}") encrypted = affine_encrypt(text, a, b) print(f"加密后: {encrypted}") decrypted = affine_decrypt(encrypted, a, b) print(f"解密后: {decrypted}") print("-" * 30) # 测试用例2:包含空格和非字母 text2 = "Hello, World!" encrypted2 = affine_encrypt(text2.upper(), a, b) # 先转为大写统一处理 decrypted2 = affine_decrypt(encrypted2, a, b) print(f"明文: {text2}") print(f"加密后: {encrypted2}") print(f"解密后: {decrypted2}") print("-" * 30) # 测试用例3:错误的密钥a try: affine_encrypt("TEST", 2, 1) # 2与26不互质,公约数为2 except ValueError as e: print(f"密钥错误测试: {e}")运行结果:
明文: AFFINE CIPHER 密钥: a=5, b=8 加密后: IHHWVC SWFRCP 解密后: AFFINE CIPHER ------------------------------ 明文: Hello, World! 加密后: JAVVY, FYBQV! 解密后: HELLO, WORLD! ------------------------------ 密钥错误测试: 密钥a=2无效,必须与26互质。4.3 关键步骤与原理解析
密钥生成与验证:这是仿射密码安全的前提。你必须确保选择的
a是 {1,3,5,7,9,11,15,17,19,21,23,25} 中的一个。b可以是0-25的任意整数。在代码中,我们通过gcd(a, 26) != 1来验证,如果不互质,加密和解密过程将不是一一映射,导致信息丢失或无法正确解密。加密过程
E(x) = (a*x + b) mod 26:x是明文字母序号。a*x实现了“散射”,不同的x乘以a后再取模,结果会分散开,不像凯撒密码那样有序。+b相当于再进行一次凯撒移位。mod 26保证结果落在字母表范围内。
解密过程
D(y) = a’ * (y - b) mod 26:- 首先
y - b抵消掉加密时的位移。 - 然后乘以
a’(a的逆元)。因为(a * a’) mod 26 = 1,所以a’ * (a*x + b - b) mod 26 = a’ * a * x mod 26 = x,从而恢复出明文x。 - 求逆元是解密的关键步骤,也是理解模运算中“除法”的绝佳例子。
- 首先
处理负数取模:在解密公式
(y - b)中,结果可能为负数。Python的取模运算%对负数的处理方式是返回一个与除数同号的正余数(-1 % 26 = 25),这恰好符合我们的数学定义,非常方便。但在其他一些语言中可能需要手动处理(y - b + 26) % 26。
5. 功能增强与实战应用
基础功能实现后,我们可以让这个小项目变得更实用、更好玩。
5.1 构建交互式命令行工具
我们可以编写一个简单的命令行界面,让用户选择密码类型、输入文本和密钥。
import sys def main(): print("=== 古典密码加解密工具 ===") print("1. 凯撒密码") print("2. 仿射密码") choice = input("请选择密码类型 (1 或 2): ").strip() mode = input("请选择模式 (e: 加密 / d: 解密): ").strip().lower() text = input("请输入文本: ") if choice == '1': try: shift = int(input("请输入偏移量 (整数): ")) except ValueError: print("错误:偏移量必须是整数。") return if mode == 'e': result = caesar_cipher(text, shift, 'encrypt') print(f"\n加密结果: {result}") elif mode == 'd': result = caesar_cipher(text, shift, 'decrypt') print(f"\n解密结果: {result}") else: print("模式错误,请输入 'e' 或 'd'。") elif choice == '2': try: a = int(input("请输入密钥 a (必须与26互质,如5,7,9等): ")) b = int(input("请输入密钥 b (0-25的整数): ")) except ValueError: print("错误:密钥必须是整数。") return try: if mode == 'e': # 为了简化,交互式工具里我们统一处理大写 result = affine_encrypt(text.upper(), a, b) print(f"\n加密结果: {result}") elif mode == 'd': result = affine_decrypt(text.upper(), a, b) print(f"\n解密结果: {result}") else: print("模式错误,请输入 'e' 或 'd'。") except ValueError as e: print(f"错误: {e}") else: print("无效的选择。") if __name__ == "__main__": main()5.2 暴力破解凯撒密码
凯撒密码只有26种可能,我们可以写一个程序,快速尝试所有偏移量,并结合简单的英文单词频率分析,自动找出最可能的明文。
def brute_force_caesar(ciphertext): """ 暴力破解凯撒密码,打印所有26种可能的结果。 """ print(f"尝试破解密文: {ciphertext}\n") for shift in range(26): possible_plaintext = caesar_cipher(ciphertext, shift, 'decrypt') print(f"Shift {shift:2d}: {possible_plaintext}") print("\n提示:通常正确的明文是看起来像有意义的单词或句子的那一行。") # 示例:破解一个未知偏移量的密文 encrypted_msg = "KHOOR, ZRUOG!" # 这是“HELLO, WORLD!”用凯撒密码偏移3加密的 brute_force_caesar(encrypted_msg)5.3 扩展:支持自定义字符集
我们的代码目前只针对26个英文字母。但古典密码也可以用于数字、其他语言字符。我们可以改进函数,使其接受一个自定义的字符集。
def affine_encrypt_general(plaintext, a, b, alphabet): """ 通用仿射密码加密,支持任意字符集。 :param alphabet: 一个字符串,定义了有序的字符集,如“ABCDEFGHIJKLMNOPQRSTUVWXYZ” """ m = len(alphabet) if gcd(a, m) != 1: raise ValueError(f"密钥a={a}必须与字符集长度m={m}互质。") # 创建字符到索引的映射字典,提高查找效率 char_to_index = {char: idx for idx, char in enumerate(alphabet)} index_to_char = {idx: char for idx, char in enumerate(alphabet)} ciphertext = [] for char in plaintext: if char in char_to_index: x = char_to_index[char] y = (a * x + b) % m ciphertext.append(index_to_char[y]) else: ciphertext.append(char) return ''.join(ciphertext) # 测试自定义字符集:数字0-9 digit_alphabet = "0123456789" text = "314159" a, b = 3, 7 # 3与10互质 encrypted_digits = affine_encrypt_general(text, a, b, digit_alphabet) print(f"数字明文: {text}") print(f"数字密文: {encrypted_digits}") # 输出需要根据计算得出6. 常见问题、调试技巧与安全思考
在实际编写和运行这些代码时,你可能会遇到一些问题。这里我总结了一些常见坑点和调试心得。
6.1 常见错误与排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 加密后出现乱码或非字母字符 | 1. 未正确处理大小写,导致计算结果超出‘A’-‘Z’或‘a’-‘z’范围。 2. 取模运算 % 26遗漏或写错位置。 | 1. 检查if char.isupper():和elif char.islower():分支逻辑是否完整。2. 确保 y = (… ) % 26这一步一定执行。 |
| 仿射密码解密结果不正确 | 1. 加密和解密使用的密钥a,b不一致。2. 密钥 a选择错误,与26不互质。3. 乘法逆元 a_inv计算错误。 | 1. 仔细核对传入加解密函数的a,b值。2. 在加密和解密函数开头都添加 gcd(a, 26) != 1的检查。3. 使用单独的测试函数验证 mod_inverse的正确性,例如print((5*mod_inverse(5))%26)应输出1。 |
程序报错ValueError: chr() arg not in range(0x110000) | chr()函数的参数超出了Unicode编码范围。这通常发生在y + ord(‘A’)的计算结果远大于26时,说明前面的y计算可能漏了% 26。 | 回溯检查计算y的公式,确保最后一步是% 26。 |
| 包含空格的文本加密后格式错乱 | 代码逻辑错误,可能将空格也进行了ord()转换和数学运算。 | 确保在字符处理循环中,对非字母字符有else分支,直接append(char)。 |
调试技巧:在开发过程中,可以在循环内部添加打印语句,实时查看每个字符的转换过程。例如:
print(f”char: {char}, x: {x}, y: {y}, new_char: {new_char}”)。这是理解算法和数据流最直接的方法。
6.2 关于古典密码安全性的思考
通过实现这些密码,我们必须清醒认识到:凯撒密码和仿射密码都绝对不安全,绝不能用于任何真实的保密通信。
- 密钥空间过小:凯撒26种,仿射312种。现代计算机可以在微秒内穷举所有可能。
- 频率分析:单表替换密码无法改变字母的统计特征。在英文中,字母E、T、A、O等出现的频率远高于Z、Q、J。攻击者通过分析密文中字母的频率,很容易猜出替换规则。
- 已知明文攻击:如果攻击者知道一部分明文和对应的密文(比如信件的标准开头),那么密钥可以直接解出来。
那么,学习它们的意义何在?
- 教学价值:它们是理解密码学基本概念(加密、解密、密钥、模运算)的完美入门案例。
- 历史价值:了解密码学的发展历程。
- 编程练习:作为综合运用字符串、循环、函数、数学运算的练手项目。
- 现代密码学基石:许多现代密码算法(如AES、RSA)中依然能看到模运算、逆元等概念的影子。理解仿射密码是理解这些更复杂算法的一块重要跳板。
6.3 项目扩展方向
如果你对这个主题意犹未尽,可以尝试以下挑战,让项目更具深度:
- 实现维吉尼亚密码:这是一种多表替换密码,使用一个关键词作为密钥,安全性比单表替换高很多。它本质上是多个不同偏移量的凯撒密码的组合。
- 实现简单频率分析攻击:写一个程序,对一段仿射密码加密的英文密文,通过统计字母频率,尝试自动推测出密钥
a和b。 - 图形化界面:使用
tkinter或PyQt库,为你的加解密工具做一个简单的桌面窗口。 - 文件加解密:修改程序,使其能够读取一个文本文件,加密后输出到另一个文件,再解密还原。
- 探索其他古典密码:如栅栏密码、Playfair密码等,丰富你的“密码工具箱”。
写完这些代码并成功运行后,你收获的将不仅仅是两个密码函数。你实践了“将数学公式转化为可执行代码”的完整过程,加强了对Python基础语法的掌握,更重要的是,建立了一种用计算思维解决逻辑问题的直觉。下次当你听到“公钥”、“私钥”、“非对称加密”这些词时,你会知道,它们都始于像今天这样,对字符进行的一次次巧妙变换。