Python实现仿射密码:从古典密码原理到加解密实战
1. 项目概述从古典密码到Python实现如果你对密码学感兴趣想找一个既有数学美感又容易上手的入门项目仿射密码绝对是个绝佳的选择。它不像现代密码学那样涉及复杂的数学理论但其核心的线性变换思想却是理解更高级加密算法的一块重要基石。简单来说仿射密码就是凯撒密码的“升级版”它把简单的字母移位变成了一个包含乘法和加法的线性方程安全性稍微高那么一点点但实现起来依然清晰明了。这个实验的核心就是用Python语言从零开始完整地实现仿射密码的加密、解密过程并深入探讨其背后的数学原理和安全性弱点。无论你是信息安全专业的学生还是对编程和密码学充满好奇的爱好者通过亲手实现这个算法你不仅能巩固Python编程技能更能直观地理解“加密”和“解密”这两个动作在数学上是如何互为逆过程的。我会带你一步步拆解公式、编写代码并分享我在实现过程中遇到的那些“坑”和调试技巧确保你能看懂、能复现甚至能举一反三。2. 仿射密码的核心原理与数学基础要玩转仿射密码光会写代码可不够必须得先搞懂它背后的数学规则。这就像盖房子地基打牢了上面的代码实现才能稳固。2.1 加密公式一个线性方程的游戏仿射密码的加密过程可以用一个非常简洁的数学公式来描述C (a * P b) mod 26我们来拆解一下这个公式里的每一个符号C (Ciphertext) 密文也就是加密后的字母。P (Plaintext) 明文也就是原始信息中的字母。a 和 b 这就是我们的密钥。整个密码的安全性虽然很弱就靠它俩了。mod 26 取模运算这是整个算法的“边界控制器”。因为我们处理的是26个英文字母所以任何计算结果都必须落在0到25这个范围内对应a-z或A-Z。这个公式在做什么呢它把明文字母P先转换成0-25的数字扔进一个线性函数f(x) a*x b里计算然后通过mod 26确保结果仍然对应一个有效的字母。参数a负责“拉伸”或“压缩”明文空间而参数b则负责整体“平移”。注意这里有一个至关重要的限制条件参数a必须与26互质即gcd(a, 26) 1。为什么因为如果a和26有大于1的公因数那么这个加密函数就不是一个“一一映射”。通俗点讲会有多个不同的明文字母被加密成同一个密文字母导致解密时无法确定原始信息是什么解密函数也就不存在了。例如如果a2与26不互质那么字母a(0)和字母n(13)经过2*0 mod 26和2*13 mod 26计算后都得到0都对应密文a这肯定不行。2.2 解密公式寻找逆元有加密就得有解密。解密是加密的逆过程我们需要从C (a*P b) mod 26这个等式中反解出P。推导过程如下从加密公式开始C ≡ a*P b (mod 26)等式两边减去bC - b ≡ a*P (mod 26)为了得到P我们需要在等式两边同时乘以a在模26下的乘法逆元记作a⁻¹。乘法逆元的定义是满足(a * a⁻¹) mod 26 1的那个数。于是得到解密公式P ≡ a⁻¹ * (C - b) (mod 26)所以解密的关键在于找到a⁻¹。在模26运算中不是每个数都有逆元只有那些与26互质的数才有。这也从另一个角度印证了为什么加密时要求gcd(a, 26) 1。2.3 参数空间与安全性初探理解了公式我们就能直观地感受到仿射密码为什么“弱”了。密钥空间小参数a只能从与26互质的数中选在0-25范围内这样的数只有12个1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25。参数b可以从0到25任意选择。所以总的密钥对(a, b)数量是12 * 26 312。但通常排除a1, b0这种不加密的情况有效密钥是311个。暴力破解易如反掌对于现代计算机来说尝试311种可能的密钥组合来破解一段密文几乎是瞬间完成的事情。这比凯撒密码的25种可能多了不少但在计算能力面前依然不堪一击。频率分析由于它仍然是单表替换密码即一个明文字母固定对应一个密文字母密文中字母的频率分布会保留明文的统计特征。通过分析英文字母的出现频率如e, t, a, o, i, n, s, h, r 出现频率高攻击者可以很容易地推测出密钥。3. Python实现从零搭建加密解密系统理论说得再多不如动手写一行代码。接下来我们抛开那些复杂的库用最基础的Python语法构建一个完整的仿射密码工具。我会把代码拆解得非常细并解释每一行背后的意图。3.1 核心工具函数互质判断与逆元求解在实现加解密之前我们需要两个数学工具。3.1.1 计算最大公约数 (GCD)判断a是否与26互质需要计算它们的最大公约数。我们使用经典的辗转相除法欧几里得算法。def gcd(a, b): 计算两个整数的最大公约数 (Greatest Common Divisor)。 使用欧几里得算法辗转相除法。 :param a: 整数 :param b: 整数 :return: a和b的最大公约数 while b ! 0: # a 对 b 取余将余数赋给一个临时变量 remainder a % b # 将 b 的值赋给 a a b # 将余数的值赋给 b继续循环 b remainder # 当 b 为 0 时a 即为最大公约数 return a这个函数是密码学的基石之一高效且优雅。你可以用gcd(a, 26) 1来验证密钥a是否有效。3.1.2 求解模逆元 (Modular Inverse)解密时需要a的逆元a⁻¹。由于模数26很小我们可以用一个简单直观的“暴力搜索”方法。def mod_inverse(a, m26): 求解 a 在模 m 下的乘法逆元。 即寻找一个整数 x使得 (a * x) % m 1。 这里采用遍历法因为模数26很小效率足够。 :param a: 需要求逆元的整数 :param m: 模数默认为26 :return: a 在模 m 下的逆元如果不存在则返回 None a a % m # 确保a在模m范围内 for x in range(1, m): if (a * x) % m 1: return x return None # 如果不存在逆元实操心得对于小模数这种遍历法完全没问题代码也清晰易懂。但如果模数非常大比如在RSA算法中就需要使用扩展欧几里得算法来高效计算逆元。这里我们先掌握基础版本。3.2 加密函数实现加密函数需要完成读取明文、处理每个字母、应用加密公式、输出密文。def affine_encrypt(plaintext, a, b): 使用仿射密码加密明文。 :param plaintext: 明文字符串假设只包含小写字母 a-z :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.isalpha(): # 只处理字母字符 # 2. 将小写字母转换为 0-25 的数字 p ord(char) - ord(a) # 3. 应用加密公式: C (a*P b) mod 26 c (a * p b) % 26 # 4. 将数字转换回字母并转为大写密文惯例 ciphertext.append(chr(c ord(A))) else: # 非字母字符原样保留也可选择忽略或抛出异常 ciphertext.append(char) # 5. 将列表连接成字符串并返回 return .join(ciphertext)代码逐行解析检查首先确保密钥a有效这是保证加密可逆的前提。转换ord(char)获取字符的ASCII码减去ord(a)就将a~z映射到了0~25。这是处理文本的常见技巧。核心计算直接套用公式(a * p b) % 26。%是Python的取模运算符完美符合我们的需求。逆转换将结果c加上ord(A)的ASCII码再通过chr()转回字符得到大写密文字母。这是一种约定俗成的做法便于区分明文和密文。拼接返回使用.join(list)将字符列表高效地拼接成字符串。3.3 解密函数实现解密是加密的逆过程需要用到之前求得的逆元。def affine_decrypt(ciphertext, a, b): 使用仿射密码解密密文。 :param ciphertext: 密文字符串假设只包含大写字母 A-Z :param a: 加密时使用的参数a :param b: 加密时使用的参数b :return: 解密后的明文字符串小写字母形式 # 1. 计算 a 在模 26 下的逆元 a_inv mod_inverse(a, 26) if a_inv is None: raise ValueError(f参数 a{a} 在模26下没有逆元无法解密。) plaintext [] for char in ciphertext: if char.isalpha(): # 只处理字母字符 # 2. 将大写字母转换为 0-25 的数字 c ord(char) - ord(A) # 3. 应用解密公式: P a_inv * (C - b) mod 26 # 注意C-b 可能为负数Python的 % 运算符对负数也能正确处理。 p (a_inv * (c - b)) % 26 # 4. 将数字转换回小写字母 plaintext.append(chr(p ord(a))) else: # 非字母字符原样保留 plaintext.append(char) return .join(plaintext)代码逐行解析求逆元首先获取a的逆元a_inv这是解密的钥匙。如果求不出来说明当初加密用的a不合法。转换将大写密文字母A~Z映射到0~25。核心计算套用解密公式(a_inv * (c - b)) % 26。这里(c - b)可能产生负数但Python的%运算符会返回一个非负的最小正剩余例如-3 % 26结果是23这正好符合我们的数学定义非常方便。逆转换将数字映射回小写字母恢复明文格式。3.4 主程序与交互测试把上面的函数组装起来加上用户交互就是一个完整的命令行工具。def main(): print( 仿射密码加解密演示 ) # 示例明文 plaintext ifnottothesunforsmilingwarmisstillinthesuntherebutwewilllaughmoreconfidentcalm print(f原始明文: {plaintext}) # 获取密钥 while True: try: a int(input(请输入密钥参数 a (必须与26互质如3,5,7等): )) b int(input(请输入密钥参数 b (0-25之间的整数): )) if gcd(a, 26) ! 1: print(f错误a{a} 与26不互质请重新输入。) continue if not (0 b 25): print(f错误b{b} 不在0-25范围内请重新输入。) continue break except ValueError: print(输入无效请输入整数。) # 加密 print(\n--- 加密过程 ---) ciphertext affine_encrypt(plaintext, a, b) print(f使用密钥(a{a}, b{b})加密后:) print(f密文: {ciphertext}) # 解密 print(\n--- 解密过程 ---) decrypted_text affine_decrypt(ciphertext, a, b) print(f使用相同密钥解密后:) print(f明文: {decrypted_text}) # 验证 if decrypted_text plaintext: print(✓ 加解密验证成功) else: print(✗ 加解密验证失败) if __name__ __main__: main()运行这个程序你可以输入不同的a和b来观察加密效果。例如输入a3, b4你会看到明文被转换成一串看似杂乱的大写字母串。再用同样的密钥解密又能完美恢复原文。这个过程能给你最直接的密码学体验。4. 安全性深度剖析与攻击模拟实现加解密只是第一步理解它为何脆弱以及如何被攻破才是学习密码学的精髓。我们来模拟两种经典的攻击方式。4.1 暴力破解攻击实现由于密钥空间只有311种可能暴力破解穷举攻击是最高效的方法。我们写一个函数尝试所有可能的(a, b)组合。def brute_force_affine(ciphertext, possible_plaintext_fragmentNone): 暴力破解仿射密码。 :param ciphertext: 待破解的密文大写字母 :param possible_plaintext_fragment: 可能包含的明文片段小写字母用于快速筛选 :return: 生成器 yield (a, b, decrypted_text) 元组 # 所有可能的 a (与26互质) possible_a [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25] # 所有可能的 b possible_b range(0, 26) print(f开始暴力破解共 {len(possible_a)*len(possible_b)} 种密钥组合...) for a in possible_a: a_inv mod_inverse(a, 26) # 预先计算逆元避免在内部循环重复计算 if a_inv is None: continue for b in possible_b: # 尝试用当前(a,b)解密 decrypted affine_decrypt(ciphertext, a, b) # 这里affine_decrypt需要能接收a,b参数 # 如果提供了明文片段则快速检查 if possible_plaintext_fragment and possible_plaintext_fragment in decrypted: print(f发现匹配密钥可能为: a{a}, b{b}) print(f解密结果: {decrypted[:100]}...) # 只打印前100字符 yield (a, b, decrypted) # 你也可以不设条件打印所有结果但通常数据量太大 # else: # print(f尝试 a{a:2d}, b{b:2d} {decrypted[:50]}...)注意事项在实际破解中我们往往不知道任何明文信息。但攻击者可能会猜测密文中包含某些常见单词如“the”“and”“of”等。possible_plaintext_fragment参数就是用来模拟这种“已知明文攻击”的场景。即使没有这个线索现代计算机遍历311种可能也只需毫秒级时间。4.2 频率分析攻击实战对于较长的密文频率分析攻击往往比暴力破解更有效。其核心思想是在任何一种语言中各个字母的出现频率是有固定规律的。英文中e的出现频率远高于z。仿射密码是单表替换它只是把字母表重新排列并没有改变这种频率分布特征。4.2.1 统计密文字母频率def frequency_analysis(ciphertext): 对密文进行字母频率分析。 :param ciphertext: 密文字符串 :return: 按频率降序排列的字母频率列表 from collections import Counter # 过滤非字母字符并统一为大写 letters_only [ch for ch in ciphertext.upper() if ch.isalpha()] total_letters len(letters_only) if total_letters 0: return [] # 使用Counter计数 letter_counts Counter(letters_only) # 计算频率并排序 frequency_list [(letter, count/total_letters) for letter, count in letter_counts.items()] frequency_list.sort(keylambda x: x[1], reverseTrue) print(密文字母频率统计从高到低:) for letter, freq in frequency_list[:10]: # 只看前10个 print(f {letter}: {freq:.3%}) return frequency_list4.2.2 基于频率的密钥推测假设我们从频率分析中得出密文中出现最多的字母是Q次多的字母是J。根据英文统计明文中最可能出现的字母是e其次是t。我们可以建立方程假设Q是e加密而来(a * 4 b) % 26 16因为 e-4, Q-16假设J是t加密而来(a * 19 b) % 26 9因为 t-19, J-9这是一个包含两个未知数a,b的模线性方程组。我们可以写一个函数来求解所有可能的整数解。def solve_affine_from_pairs(pair1, pair2): 根据两对可能的明密文对应关系求解仿射密码的密钥(a,b)。 :param pair1: 元组 (plain_num1, cipher_num1) :param pair2: 元组 (plain_num2, cipher_num2) :return: 满足条件的 (a, b) 列表 p1, c1 pair1 p2, c2 pair2 solutions [] # 遍历所有可能的 a (0-25) for a in range(26): # 根据公式推导 c1 - c2 a*(p1-p2) (mod 26) # 但直接解方程更直观检查当前a是否能使两个等式同时成立 # 从等式1求 b: b (c1 - a*p1) mod 26 b (c1 - a * p1) % 26 # 用等式2验证 if (a * p2 b) % 26 c2: # 还需要检查a是否与26互质否则不是有效密钥 if gcd(a, 26) 1: solutions.append((a, b)) return solutions # 使用示例 if __name__ __main__: # 假设我们推测密文Q(16)对应明文e(4)密文J(9)对应明文t(19) possible_keys solve_affine_from_pairs((4, 16), (19, 9)) print(根据频率分析推测的可能密钥) for a, b in possible_keys: print(f a{a}, b{b}) # 输出结果会包含 (3, 4) 这个正确解。通过频率分析我们可能得到几组候选的(a, b)。再结合对解密结果的语义判断看解密出的文本像不像人话就能最终确定密钥。这个过程完美展示了密码分析学中“统计语义”的综合分析方法。5. 项目扩展与深入思考一个基础的仿射密码实现完成后我们可以从多个方向进行扩展和深化这不仅能巩固知识还能触及更广泛的密码学概念。5.1 扩展一支持更完整的字符集我们的基础版本只处理了小写字母。一个健壮的加密工具应该能处理大小写、数字、标点甚至中文。思路是扩大“字母表”。def affine_encrypt_extended(text, a, b, alphabet): 扩展版仿射加密支持自定义字符集。 :param text: 明文 :param a: 密钥a必须与字母表长度互质 :param b: 密钥b :param alphabet: 有序的字母表字符串如 abcdefghijklmnopqrstuvwxyzABCD... :return: 密文 n len(alphabet) if gcd(a, n) ! 1: raise ValueError(fa{a} 与字母表长度{n}不互质。) char_to_index {ch: i for i, ch in enumerate(alphabet)} index_to_char {i: ch for i, ch in enumerate(alphabet)} result [] for char in text: if char in char_to_index: idx char_to_index[char] new_idx (a * idx b) % n result.append(index_to_char[new_idx]) else: result.append(char) # 非字母表字符原样保留 return .join(result)你可以定义alphabet abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,!?来包含更多字符。密钥空间会变大a需要与新的长度n互质但密码的本质弱点单表替换并未改变。5.2 扩展二文件加解密与命令行工具将加解密功能封装成命令行工具可以处理文件实用性大大增强。import argparse def main_cli(): parser argparse.ArgumentParser(description仿射密码文件加解密工具) parser.add_argument(mode, choices[encrypt, decrypt], help模式加密或解密) parser.add_argument(input_file, help输入文件路径) parser.add_argument(output_file, help输出文件路径) parser.add_argument(-a, typeint, requiredTrue, help密钥参数 a) parser.add_argument(-b, typeint, requiredTrue, help密钥参数 b) parser.add_argument(--alphabet, defaultabcdefghijklmnopqrstuvwxyz, help自定义字母表默认小写字母) args parser.parse_args() with open(args.input_file, r, encodingutf-8) as f: text f.read() n len(args.alphabet) if gcd(args.a, n) ! 1: print(f错误a{args.a} 与字母表长度{n}不互质。) return if args.mode encrypt: processed_text affine_encrypt_extended(text, args.a, args.b, args.alphabet) else: # decrypt # 需要先实现对应的扩展解密函数这里省略 a_inv mod_inverse(args.a, n) if a_inv is None: print(f错误a{args.a} 在模{n}下无逆元无法解密。) return processed_text affine_decrypt_extended(text, args.a, args.b, args.alphabet) with open(args.output_file, w, encodingutf-8) as f: f.write(processed_text) print(f操作完成结果已写入 {args.output_file}) # 在命令行中即可使用python affine_cipher.py encrypt input.txt output.txt -a 5 -b 85.3 深入思考从仿射密码到现代密码学通过这个项目我们实际上亲手验证了古典密码的几个致命弱点这也正是现代密码学要解决的问题密钥空间过小仿射密码的密钥空间是“可数”的能被穷举。现代对称密码如AES的密钥空间极其巨大2¹²⁸ 种可能穷举在有限时间内不可行。单表替换一个明文字母永远对应一个密文字母频率特征暴露无遗。现代密码引入了“混淆”和“扩散”原则。混淆如AES的S盒使得密钥和密文之间的关系变得极其复杂扩散如分组密码的模式使得明文一位的改变会影响密文中许多位彻底打乱统计特性。确定性加密同样的明文和密钥总是产生同样的密文。这在某些场景下是危险的。现代密码系统会引入初始化向量IV或使用非确定性加密模式如CBC, GCM使得每次加密相同明文都会产生不同的密文。所以仿射密码实验就像是一把钥匙它为你打开了密码学世界的大门。你通过它理解了加密、解密、逆元、模运算、密钥空间、频率分析这些核心概念。但请务必记住它本身绝对不应用于任何真实的保密通信。它的价值在于教学和启发让你明白一个“好”的密码算法应该规避哪些设计缺陷。当你日后学习AES、RSA这些现代算法时你会不断回想起仿射密码这个简单的模型并惊叹于密码学家们是如何一步步解决这些古老问题的。