Python实现古典密码:从凯撒到仿射的加密算法详解

📅 2026/7/15 6:17:49
Python实现古典密码:从凯撒到仿射的加密算法详解
1. 项目概述从凯撒到仿射一场古典密码的Python探险如果你对编程感兴趣尤其是用Python那么“密码学”绝对是一个能让你瞬间兴奋起来的领域。它不像纯算法那样抽象也不像Web开发那样需要处理复杂的框架它更像是一种智力游戏用代码去实现历史上那些精妙的“加密艺术”。今天我们就来玩点古典的——从最著名的凯撒密码一路升级到更具数学美感的仿射密码。这不仅仅是写几个函数而是亲手用Python重现一段信息保密的历史理解加密算法从简单替换到线性变换的演进逻辑。对于初学者来说这是一个绝佳的练手项目。它涵盖了Python基础语法中的字符串处理、循环、条件判断、函数定义以及模运算等数学概念。对于有经验的开发者深入理解这些古典密码的加解密原理能为你后续学习现代密码学如RSA、AES打下坚实的直觉基础。你会发现很多复杂的思想其源头都蕴藏在这些看似简单的古典方法之中。我将手把手带你不只是写出能运行的代码更要弄懂每一行代码背后的“为什么”以及在实际操作中可能踩到的“坑”。准备好你的编辑器我们这就开始。2. 古典密码基础与核心思路拆解在动手写代码之前我们必须先搞清楚我们要复现的是什么。古典密码主要依靠的是“替换”和“置换”凯撒和仿射密码都属于“单表替换密码”即加密时明文中的每一个字母都被另一个固定的字母所替换。2.1 凯撒密码位移的艺术凯撒密码的原理简单到令人发指将字母表中的每个字母向后或向前移动一个固定的位数。例如当偏移量密钥为3时A变成DB变成E…X变成AY变成BZ变成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,25b有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’)得到65chr(65)得到 ‘A’。取模运算% 26这是实现“循环移位”的核心。当xshift超过25时取模运算会使其回到0-25的范围内。例如Z(25) 3 2828 % 26 2对应字母C。大小写处理我们分别判断并处理大小写字母确保加密后大小写格式不变。这是一个很重要的细节很多简单实现会忽略这一点导致结果混乱。非字母字符代码中通过else分支原样保留空格、逗号等字符。这使得加密后的文本更具可读性也符合实际应用场景。一个关键技巧解密函数直接复用了加密函数但传入负的偏移量。这利用了数学上的对称性减少了代码重复是优雅的实现。3.2 进阶实现利用列表推导式和str.translate对于熟悉Python的朋友我们可以用更高效、更“Pythonic”的方式来实现def caesar_cipher(text, shift, modeencrypt): 凯撒密码加解密进阶版 :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, m26): 求 a 在模 m 下的乘法逆元。 即寻找 a 使得 (a * a) % m 1。 这里使用暴力搜索因为m26很小。 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为什么用暴力搜索求逆元因为我们的模数m26非常小遍历1到25是最简单直接的方法。如果模数很大比如RSA算法中的大素数乘积就必须使用扩展欧几里得算法来高效计算。这里我们优先保证代码的可读性。4.2 完整的仿射密码加解密实现现在结合工具函数实现完整的仿射密码。def affine_encrypt(plaintext, a, b): 仿射密码加密 :param plaintext: 明文字符串假设为大写字母 :param a: 密钥a必须与26互质 :param b: 密钥b0-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 密钥: a5, b8 加密后: IHHWVC SWFRCP 解密后: AFFINE CIPHER ------------------------------ 明文: Hello, World! 加密后: JAVVY, FYBQV! 解密后: HELLO, WORLD! ------------------------------ 密钥错误测试: 密钥a2无效必须与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 26x是明文字母序号。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(fShift {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基础语法的掌握更重要的是建立了一种用计算思维解决逻辑问题的直觉。下次当你听到“公钥”、“私钥”、“非对称加密”这些词时你会知道它们都始于像今天这样对字符进行的一次次巧妙变换。