从凯撒到RSA:九种加解密算法原理与自动破解实现
2026/9/17 3:22:50 网站建设 项目流程

简介:这是一款基于Python开发的CTF Crypto图形化工具,面向CTF参赛者与信息安全入门学习者,覆盖凯撒密码、维吉尼亚密码、栅栏密码、摩斯密码、Base64/ASCII编码、AES、DES、RSA、RC4等常见算法的加密与解密,并支持维吉尼亚密钥自动破解。资源包共70个文件,压缩后仅126KB,包含Python源码、pyc编译文件、JavaScript脚本、PEM密钥对、UI界面文件及JSON配置等,既可阅读算法实现,也能直接运行体验完整的加解密流程。配套的UI文件与spec打包配置便于二次开发或发布独立程序,node_modules目录内置crypto-js等前端加密库,适合在Web环境中复用。已有2346人学习下载,适合需要快速搭建Crypto实验环境、研究经典密码实现或刷题验证思路的学习者。

1. 一个把凯撒、维吉尼亚、AES 放同一工具里的现实场景

把凯撒密码和 AES、RSA 放进同一个加解密工具,第一反应往往是“这有什么可比性”,但真正在写这类工具时你会发现,它要解决的是两件完全不同的事:一部分是 CTF、古典密文分析里常见的低熵编码与替换,另一部分是生产环境里真正在传输链路上跑的对称与非对称算法。凯撒、栅栏、摩斯、Base64 这类对象,破解的核心是枚举和统计;而 AES、DES、RSA、RC4 的核心是参数怎么选、模式怎么配、密钥怎么管。维吉尼亚恰好卡在中间——它比凯撒难在一个“密钥长度未知”,但又远没有到现代密码的强度级别,因此值得单独给它一套基于重合指数和卡方拟合的自动破解流程。这篇文章就按这个思路把这九种算法拆开讲透,并给出一套可以直接落地为命令行工具的实现路径。适合需要写密文分析脚本、做 CTF 工具集,或者想理清古典密码与现代密码边界的人。

2. 凯撒与栅栏密码的穷举破解路径:摩斯表、Base64 与可枚举边界

2.1 凯撒密码:位移穷举与频率卡方判断

凯撒密码是所有替换密码里最朴素的一种:明文每个字母向后(或向前)移动固定位数得到密文。破解它的关键不是“知道位移量”,而是“怎么从 26 个候选中自动挑出可读的那一个”。常见做法是暴力枚举全部 26 个位移,再用英文频率分布做卡方检验筛选。

import string FREQ = [0.08167, 0.01492, 0.02782, 0.04253, 0.12702, 0.02228, 0.02015, 0.06094, 0.06966, 0.00153, 0.00772, 0.04025, 0.02406, 0.06749, 0.07507, 0.01929, 0.00095, 0.05987, 0.06327, 0.09056, 0.02758, 0.00978, 0.02360, 0.00150, 0.01974, 0.00074] def caesar_break(ciphertext: str): candidates = [] for shift in range(26): text = [] for ch in ciphertext.lower(): if ch in string.ascii_lowercase: idx = string.ascii_lowercase.index(ch) text.append(string.ascii_lowercase[(idx - shift) % 26]) else: text.append(ch) plain = "".join(text) counts = [0.0] * 26 letters = [c for c in plain if c.isalpha()] for c in letters: counts[ord(c) - ord('a')] += 1 n = len(letters) if n == 0: continue chi2 = sum((counts[i] / n - FREQ[i]) ** 2 / FREQ[i] for i in range(26)) candidates.append((chi2, shift, plain)) candidates.sort() return candidates[:5]

这段代码的筛选逻辑不是看“像不像单词”,而是看整体字母分布与英文自然分布的拟合程度:卡方值越小,说明频率分布越接近英文,候选排名越靠前。之所以用卡方而不用单词匹配,是因为凯撒密文往往来自短文本或经过大小写、数字混合处理,单词表匹配在这种场景下召回率很低。把FREQ换成中文拼音或德语频率表,同样一套算法就能跨语言复用。

2.2 栅栏密码:轨道数枚举加元音比例粗筛

栅栏密码(Rail Fence Cipher)是按之字形把明文写到若干“轨道”上,再按行读出。破解它的难点与凯撒不同:位移量变成了轨道数,且轨道数未知。好在这个密钥空间极小,一般 2 到 10 轨足够覆盖绝大多数场景,所以枚举轨道数并做启发式评分,比做频率分析更直接。

def decrypt_rail_fence(cipher: str, rails: int) -> str: n = len(cipher) positions = [[] for _ in range(rails)] row, step = 0, 1 for i in range(n): positions[row].append(i) if row == 0: step = 1 elif row == rails - 1: step = -1 row += step it = iter(cipher) filled = {pos: next(it) for pos in sum(positions, [])} return "".join(filled[i] for i in range(n)) def vowel_score(text: str) -> float: letters = [c for c in text.lower() if c.isalpha()] vowels = sum(1 for c in letters if c in "aeiou") return vowels / max(len(letters), 1) def break_rail_fence(cipher: str, max_rails: int = 10): results = [] for rails in range(2, max_rails + 1): plain = decrypt_rail_fence(cipher, rails) score = abs(vowel_score(plain) - 0.38) results.append((score, rails, plain)) return sorted(results)[:5]

这里用元音比例做粗筛,英文的元音占比通常在 0.36 到 0.42 之间,与 0.38 的偏差越小,越像是正常文本。这个启发式并不严格,但足以把 10 个候选压缩到前 5 名,最后靠人工扫一眼即可确认。注意解密函数里的positions必须记录每个密文字符在原文中的真实下标,而不是简单地按行均分,否则遇到明文长度与轨道数不成倍数时就会错位。

2.3 摩斯密码与 Base64:可逆编码的直接映射

摩斯密码和 Base64 严格说都不是加密,而是编码。摩斯的本质是变长符号映射,字母对应点划组合,数字对应五单位组合;Base64 则是把二进制数据按 6 位一组映射到 64 个可打印字符。它们不需要“破解”,只需要“识别”和“解码”。

MORSE = { "A": ".-", "B": "-...", "C": "-.-.", "0": "-----", "1": ".----", " ": "/" } def decode_morse(morse: str) -> str: reverse = {v: k for k, v in MORSE.items()} words = morse.strip().split(" / ") return " ".join( "".join(reverse.get(sym, "?") for sym in word.split()) for word in words )

在工具实现里,摩斯码最常见的坑是分隔符不统一:有人用空格分隔字母、用/分隔单词,有人把空格与/混用。解码前先做一次正则归一化,把所有连续空格压缩为单个空格,比在解码函数里处理各种边界情况省事得多。Base64 的识别特征是末尾可能出现===填充,以及字符串经常以data:image/png;base64,这类前缀出现在 Web 场景中。Python 里直接base64.b64decode(s)即可,但要注意传入前先检查字符串长度的合法性,长度不是 4 的倍数时先补=

3. 维吉尼亚密码自动破解:Kasiski 测定密钥长度 + 重合指数精排

3.1 为什么维吉尼亚不能直接穷举

维吉尼亚密码把凯撒的固定位移扩展成周期性的位移序列,密钥长度是L,明文中相隔L的字母使用同一个位移。直接穷举密钥需要尝试26^L种组合,密钥长到 8 位时已经不可能暴力完成。但有一个结构性弱点:密钥虽然是周期的,但每个字母的位移在周期内固定,所以只要把密文按密钥长度分列,每一列本质上就是一个凯撒密文。破解维吉尼亚因此被拆成两个子问题:先测定密钥长度,再对每一列做凯撒频率分析。

3.2 用重合指数(IC)测定密钥长度

重合指数描述一段文本中随机抽取两个字母恰好相同的概率。英文自然文本的 IC 约为 0.065,随机等概率字母序列的 IC 约为 0.038。对维吉尼亚密文按某个候选长度L分列后,如果L就是真实密钥长度,那么每一列内字母分布会接近英文自然分布,平均 IC 会显著高于 0.038;如果L不对,各列相当于随机乱序,IC 会贴到 0.038 附近。这就是自动测定密钥长度的依据。

from collections import Counter def index_of_coincidence(text: str) -> float: letters = [c for c in text.lower() if c.isalpha()] n = len(letters) if n < 2: return 0.0 counts = Counter(letters) return sum(v * (v - 1) for v in counts.values()) / (n * (n - 1)) def estimate_key_length(cipher: str, max_len: int = 20) -> int: best_len, best_ic = 1, 0.0 for L in range(1, max_len + 1): columns = [cipher[i::L] for i in range(L)] avg_ic = sum(index_of_coincidence(col) for col in columns) / L if avg_ic > best_ic: best_ic, best_len = avg_ic, L return best_len

实际使用时不要只返回 IC 最高的那个长度。我一般会把候选长度按 IC 从高到低排成前 5 名,因为短文本的统计波动很大,真实密钥长度经常排在第 2 或第 3 位。判断技巧是看 IC 曲线是否在某个长度处出现“尖峰群”——真实长度的倍数(如 5、10、15)也会出现小幅抬升,所以直接取第一个显著峰值比取最大值更可靠。

3.3 按列做卡方拟合还原密钥与明文

密钥长度确定后,把密文按cipher[i % L]分到 L 列,每一列单独跑凯撒卡方分析,卡方最小的位移就是该列密钥字母。将 L 个位移拼成完整密钥,再对全量密文做逆向移位即可还原明文。

import string def vigenere_key_from_shift(cipher: str, key_length: int) -> str: key = "" for col in range(key_length): column = cipher[col::key_length] best_shift, best_score = 0, float("inf") for shift in range(26): letters = [c for c in column.lower() if c.isalpha()] counts = Counter(letters) n = len(letters) if n == 0: continue score = 0.0 for i, ch in enumerate(string.ascii_lowercase): observed = counts.get(ch, 0) / n expected = FREQ[(i - shift) % 26] score += (observed - expected) ** 2 / expected if score < best_score: best_score, best_shift = score, shift key += string.ascii_lowercase[best_shift] return key def vigenere_decrypt(cipher: str, key: str) -> str: plain = [] key_idx = 0 for ch in cipher: if ch.isalpha(): base = ord('A') if ch.isupper() else ord('a') shift = ord(key[key_idx % len(key)].lower()) - ord('a') plain.append(chr(base + (ord(ch) - base - shift) % 26)) key_idx += 1 else: plain.append(ch) return "".join(plain)

这段实现里有两个值得注意的细节。第一,vigenere_decrypt只对字母做移位,数字、空格、标点全部原样保留,同时密钥索引只在字母处前进,否则非字母字符会导致密钥错位。第二,卡方拟合时使用FREQ[(i - shift) % 26]而不是FREQ[i],是因为密文字母频率由明文字母频率平移shift得到,拟合时需要尝试所有可能的平移方向。

4. AES、DES、RC4 与 RSA:对称分组、流式与公钥体系的手写实现要点

4.1 AES:分组、IV、填充与 GCM 模式的选择

AES 是分组密码,分组长度固定 128 位。工具实现里常见的坑不是算法本身,而是三层包裹:填充模式决定分组不满时补什么,工作模式决定分组之间怎么关联,IV 或 Nonce 决定同一密钥下密文是否可重复。

from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import os key = os.urandom(32) iv = os.urandom(16) cipher = AES.new(key, AES.MODE_CBC, iv) ct = cipher.encrypt(pad(b"plaintext data", AES.block_size)) print(iv.hex(), ct.hex())

CBC 模式要求 IV 随机且每次加密都不同,否则相同明文会得到相同密文,这也是“AES 什么模式每次加密结果都不一样”这个问题的答案所在:ECB 模式下同样的明文块永远得到同样的密文块。实现里必须把 IV 与密文一起存储或传输,解密时先取前 16 字节作为 IV。生产工具我更推荐 GCM 而不是 CBC,因为 GCM 同时提供机密性和完整性校验,能直接发现密文被篡改;CBC 搭配 PKCS#7 填充时,如果解密端对填充错误处理不当,还可能引入 Padding Oracle 攻击面。

4.2 DES 与 RC4:块密码退化与流式密码实现

DES 在今天已经属于被淘汰的对称算法,密钥只有 56 位有效长度,硬件几分钟就能暴力破解。但很多遗留系统的协议里还在跑 3DES,工具里保留它是为了兼容老数据。RC4 则是流密码,密钥调度算法生成 256 字节状态向量,再逐字节异或生成密钥流。RC4 的问题在于密钥调度存在偏差,前 256 字节密钥流有明显统计特征,现代协议已经全面弃用。

def rc4(key: bytes, data: bytes) -> bytes: S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) % 256 S[i], S[j] = S[j], S[i] i = j = 0 out = bytearray() for b in data: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] out.append(b ^ S[(S[i] + S[j]) % 256]) return bytes(out)

RC4 加解密是同一个函数,因为异或操作是对称的。这里要注意每次调用都重新执行 KSA,如果对多段数据复用一个状态,就破坏了流密码的一次一密前提。实际工具中如果必须兼容 RC4,建议至少跳过前 768 字节的密钥流输出,降低已知偏差被利用的风险。

4.3 RSA:从密钥参数到签名验签的常见坑

RSA 的强度建立在两个大素数乘积难以分解上。工具实现里常见的需求是生成密钥对、按参数组合公钥、签名验签。最容易出问题的不是数学部分,而是密钥格式与参数含义。

from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 key = RSA.generate(3072) priv_pem = key.export_key().decode() pub_pem = key.publickey().export_key().decode() print("n 位数:", key.n.bit_length()) print("e:", key.e) print("p 位数:", key.p.bit_length(), "q 位数:", key.q.bit_length()) h = SHA256.new(b"important message") sig = pkcs1_15.sign(key, h) pkcs1_15.verify(key.publickey(), h, sig) print("verify ok")

n = p * q,公钥是(n, e),私钥必须持有dpq不该泄露。生成密钥时若内存不足或熵源受限,RSA.generate会报错,这时可以把e稍微调大(如 65537 已是常用值)或改用系统熵源。常见报错rsa public key not find多数是公钥字符串传成了 PEM 头缺失的裸 Base64 文本,或者把私钥对象当公钥去验签。签名验签和加密解密不要搞混:RSA 加密用公钥、解密用私钥;而签名用私钥、验签用公钥。若工具同时暴露加解密与签名两组接口,必须把这两个流程分开设计,否则用户很容易拿着公钥去做“解密”然后得到乱码。

5. 维吉尼亚密钥破解的验证技巧:用已知明文反推密钥与工具化落地

5.1 用已知明文片段反推密钥

频率分析只能给出统计意义上的密钥,遇到短密文或非英文文本时经常整段跑偏。这时如果手里有一段已知明文(哪怕是几个常见单词,如theand),就可以直接反推密钥片段:key_char = (cipher_char - plain_char) mod 26,把已知明文对齐到密文的对应位置,逐位算出密钥字符,再按周期性规律延展到整个密钥。

def recover_key_fragment(cipher: str, known_plain: str, start: int) -> str: key_frag = [] for i, ch in enumerate(known_plain): if not ch.isalpha(): continue c = cipher[start + i].lower() p = ch.lower() shift = (ord(c) - ord(p)) % 26 key_frag.append(string.ascii_lowercase[shift]) return "".join(key_frag)

这个推法对凯撒同样适用:已知一个明文字母就能定出位移。对维吉尼亚密钥破解来说,反推出来的密钥片段还能反过来验证前面 IC 测定的长度是否正确——如果按长度 L 分列后,已知明文对应的密钥片段在每个周期内重复出现,说明 L 大概率正确;如果不重复,说明密钥长度估错了。

5.2 破解结果的验证与批处理自动化

自动破解维吉尼亚之后不要直接信任输出,标准验证方法是解出来的明文再算一次 IC,应该明显回落向英文自然值;同时统计明文里高频词命中数。我一般会把两者合成一个可解释性得分,输出到同一行表格里,方便批量跑多个密文时快速排查。

在实际工具落地时,我建议把每一类算法封装成{name, encrypt, decrypt, crack}四个槽位的插件式注册表,命令行入口统一为ciphertool.py --algo vigenere --crack --input cipher.txt。这样新增算法只需要注册新模块,不需要改主流程。批量测试时用一层薄薄的脚本循环调用即可:

python ciphertool.py --algo vigenere --crack --input cipher.txt python ciphertool.py --algo caesar --crack --input cipher.txt python ciphertool.py --algo railfence --crack --input cipher.txt

把输出的明文统一经过grep -iE '\b(the|and|that|this)\b'做一轮高频词过滤,能在几十个预期结果里快速锁定可读文本。验证通过的再进入下一步:把破解出来的维吉尼亚密钥合并成key:plaintext的键值对,供其他分析脚本直接消费。这样整个工具链从识别、破解到验证,就形成了一个不需要人工逐条盯屏的闭环。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询