ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

维吉尼亚密码全解析:原理、Python实现与自动破解

维吉尼亚密码全解析:原理、Python实现与自动破解 简介维吉尼亚密码作为多表替换密码的经典代表其加密思路至今仍是密码学入门必须掌握的内容这一C语言实现示例面向密码学学习者及字符串与数组编程练习者重点演示如何通过密钥循环和模二十六运算完成明文加密与解密。压缩包内仅有一个源文件大小约一千字节代码精简集中展示密码表构建、加解密函数调用以及非字母字符处理方式便于直接阅读或借鉴。目前已有超过一千一百人下载学习说明该示例在相关课程与算法练习中具有一定参考价值代码完整生成二十六乘二十六的密码表根据密钥逐字映射字母位置同时处理大小写转换、密钥长度匹配等细节。阅读这份代码既能加深对维吉尼亚密码替换规则的理解也能掌握字符数组、循环取模等常用编程技巧适合作为课程设计或密码学入门的小型参考项目。1. 维吉尼亚密码一个用了三百年还能出现在CTF里的多表替换方案维吉尼亚密码常被当成凯撒密码的简单升级版扔在密码学教材的前几页就翻过去了。实际上它从16世纪被提出到19世纪被完全破解横跨了约三百年其间被多个国家的外交界和军事机构当作机密通信的标准工具。今天它依旧高频出现在CTF古典密码题和密码学课程实验里是理解加密算法从“隐藏算法”走向“隐藏密钥”的一座关键桥梁。对刚接触密码学的人来说亲手实现一遍维吉尼亚密码的加密、解密、自动破解比只看公式更能建立对多表替换、频率分析、重合指数这些概念的具体印象对从业者来说它也是写在线转换工具、审计古典密码实现时绕不开的参考对象。下面顺着原理、实现、在线转换、密码分析和参数陷阱这几条线往下讲。2. 维吉尼亚密码的加密原理与密钥扩展逻辑2.1 从凯撒密码到多表替换为什么它能打平频率分布凯撒密码的致命弱点在于偏移量固定。明文中e出现频率最高单次偏移后密文中对应字符的频率仍是全表最高随手做一次字母频率统计就能把明文还原。维吉尼亚密码则把偏移量变成一组由密钥驱动的周期序列密钥LEMON把明文的第1个字符用偏移11L加密第2个用偏移4E第3个用偏移12M以此类推第6个字符又回到偏移11。于是明文字符k在不同位置可能被加密成v或f原本显著的频率特征被摊平单表替换里的频率分析法立刻失效。下面用ATTACK AT DAWN和密钥LEMON演示加密过程空格保留但不参与运算。这个例子在维吉尼亚密码相关的经典解释里经常出现能直接验证后续代码的输出。| 明文 | A | T | T | A | C | K | A | T | D | A | W | N | | 密钥 | L | E | M | O | N | L | E | M | O | N | L | E | | 偏移 | 11 | 4 | 12 | 14 | 13 | 11 | 4 | 12 | 14 | 13 | 11 | 4 | | 密文 | L | X | F | O | P | V | E | F | R | N | H | R |明文字母序号按 A0 到 Z25 计算加密公式写作 C_i (P_i K_{i mod len(K)}) mod 26。这里的下标 i 只对字母计数空格、标点不参与运算密钥序列也不因空格而推进。解密是它的逆运算P_i (C_i - K_{i mod len(K)}) mod 26。这两条公式构成了整个维吉尼亚密码实现的核心。2.2 密钥流生成从关键词到周期偏移序列维吉尼亚密码的密钥不是直接参与运算的字符串而是被映射成一组偏移量。常见的做法是把密钥转成大写用ord(c.upper()) - ord(A)得到 0 到 25 的偏移。加密时密钥字符循环复用实际参与运算的序列是“密钥重复若干次后截取到明文长度”的周期序列。实现上不需要真的把密钥扩展到明文长度只需要维护一个key_idx变量每处理一个字母就取key[key_idx % len(key)]然后key_idx加一遇到非字母字符时不推进key_idx。def char_to_shift(c): return ord(c.upper()) - ord(A)这个映射函数是所有后续代码的基石。传入大小写混合的字符都能正确映射比如ord(L) - ord(A) 11ord(l)也经过upper()转成L同样得到 11。密钥开头可以有空格但使用时建议先剔除否则len(key)会把空格算进去产生的偏移量会是非法值。2.3 第一版加密函数保留非字母字符的两种选择写加密函数前要先决定边界空格、标点和数字怎么处理两种常见方案保留并写入密文或直接丢弃。在线转换器大多默认保留因为结果可读性高密码分析时常用丢弃因为分析对象是连续字母串。我一般写工具时保留非字母字符但把“是否推进密钥指针”和“是否保留字符”分开控制。def vigenere_encrypt(plaintext, key, skip_non_alphaTrue): cipher [] key_idx 0 for ch in plaintext: if ch.isalpha(): shift char_to_shift(key[key_idx % len(key)]) base ord(A) if ch.isupper() else ord(a) cipher.append(chr((ord(ch) - base shift) % 26 base)) key_idx 1 else: if skip_non_alpha: continue cipher.append(ch) return .join(cipher)这里的逻辑是按字符遍历明文遇到字母才做偏移运算并同步推进key_idx遇到非字母时如果skip_non_alpha为 True 就跳过为 False 就原样加入结果。需要注意无论哪个分支非字母都不消耗密钥这样HELLO WORLD里的空格不会导致后面的密钥错位手工用表格验算也方便。比如vigenere_encrypt(ATTACK AT DAWN, LEMON, skip_non_alphaFalse)的输出是LXFOP VEFRNHR改成skip_non_alphaTrue则输出LXFOPVEFRNHR。为什么这样设计如果让空格参与偏移假设空格是第26个字符那么解密时必须知道空格也参与运算否则结果会错位而保留但不消耗密钥的方式更接近维吉尼亚密码的古典定义也是大多数在线转换器采用的行为。另一个容易踩的坑是密钥包含空格或标点char_to_shift不做合法性校验遇到-会返回负数导致取模结果异常。实现工具时应在加密前过滤密钥只保留 A-Z 并转大写。3. 用Python实现维吉尼亚密码的加密、解密与在线转换3.1 解密函数只改一个符号的对称实现解密是加密的逆运算核心公式从加偏移变成减偏移。实现时几乎可以复用加密函数把加号改成减号。为了避免代码重复更常见的做法是写一个通用函数用mode参数控制操作方向。def vigenere(text, key, modeencrypt, skip_non_alphaTrue): result [] key_idx 0 op 1 if mode encrypt else -1 for ch in text: if ch.isalpha(): shift char_to_shift(key[key_idx % len(key)]) base ord(A) if ch.isupper() else ord(a) result.append(chr((ord(ch) - base op * shift) % 26 base)) key_idx 1 else: if skip_non_alpha: continue result.append(ch) return .join(result)对比加密函数唯一的实质变化是op变量加密时op1解密时op-1。Python 的%运算对负数也会返回非负余数所以(code - shift) % 26在shift大于code时依然能得到正确结果。这个函数实际是上一节加密函数的重构版后面所有的工具和破解脚本都基于它。验证加解密对称性的典型写法key LEMON plain ATTACK AT DAWN cipher vigenere(plain, key, modeencrypt, skip_non_alphaFalse) print(cipher) print(vigenere(cipher, key, modedecrypt, skip_non_alphaFalse))两行输出应该分别是LXFOP VEFRNHR和ATTACK AT DAWN。注意加解密时skip_non_alpha必须一致否则空格被丢弃后长度不对无法还原。3.2 命令行工具把转换器跑成本地的可执行命令在线转换器的核心逻辑其实就是一个输入文本、一个密钥、一个开关。把它落成本地命令后便于批量测试和集成到脚本里。下面是一个用 argparse 实现的最小命令行工具#!/usr/bin/env python3 import argparse def char_to_shift(c): return ord(c.upper()) - ord(A) def vigenere(text, key, modeencrypt, skip_non_alphaTrue): result [] key_idx 0 op 1 if mode encrypt else -1 for ch in text: if ch.isalpha(): shift char_to_shift(key[key_idx % len(key)]) base ord(A) if ch.isupper() else ord(a) result.append(chr((ord(ch) - base op * shift) % 26 base)) key_idx 1 else: if skip_non_alpha: continue result.append(ch) return .join(result) if __name__ __main__: parser argparse.ArgumentParser(descriptionVigenere cipher converter) parser.add_argument(--text, requiredTrue, helpinput text) parser.add_argument(--key, requiredTrue, helpcipher key, only letters) parser.add_argument(--decrypt, actionstore_true, helpswitch to decrypt mode) parser.add_argument(--keep, actionstore_true, helpkeep non-alpha characters) args parser.parse_args() mode decrypt if args.decrypt else encrypt print(vigenere(args.text, args.key, modemode, skip_non_alphanot args.keep))注意参数映射命令行的--keep对应skip_non_alphaFalse也就是“保留非字母字符”默认行为是丢弃非字母。这样设计能让常规使用直接输出连续字母串便于复制到在线转换器里继续处理。用法示例python3 vigenere_cli.py --text ATTACK AT DAWN --key LEMON --keep python3 vigenere_cli.py --text LXFOP VEFRNHR --key LEMON --decrypt --keep第一行加密并保留空格输出LXFOP VEFRNHR第二行解密并保留空格还原ATTACK AT DAWN。如果去掉--keep空格会被丢弃输出的就是LXFOPVEFRNHR和ATTACKATDAWN。3.3 用Flask搭一个维吉尼亚密码在线转换器的最小Web接口所谓在线转换器本质就是把核心函数包一层 HTTP 接口。常见做法是用 Flask 或 Django 提供一个表单页这里只给出最简 API 路由前端页面可以留给自己扩展。from flask import Flask, request, jsonify from vigenere_cli import vigenere app Flask(__name__) app.route(/convert) def convert(): text request.args.get(text, ) key request.args.get(key, ) mode request.args.get(mode, encrypt) keep request.args.get(keep, false).lower() true return jsonify({ text: text, key: key, mode: mode, result: vigenere(text, key, modemode, skip_non_alphanot keep) }) if __name__ __main__: app.run(port5000)这里假设vigenere_cli.py在同一目录且导出的vigenere函数接受skip_non_alpha参数。请求示例curl http://localhost:5000/convert?textHELLOWORLDkeyKEYmodeencryptkeepfalse返回的 JSON 里result字段是加密结果。在线转换器最核心的参数就四个文本、密钥、加解密方向、是否保留非字母字符。许多网上的维吉尼亚密码在线转换界面背后就是这么一层薄封装真正的算法不超过三十行。3.4 参数设置密钥长度、大小写、非字母处理的边界情况不同在线转换器对参数的默认值并不一致这是导致“同一个密文在不同网站结果不同”的主要原因。下面列出需要重点关注的四个参数在做转换器或者对照别人输出时应该先确认。参数常见设置影响非字母处理保留 / 丢弃影响密文长度和解密还原密钥大小写统一转大写大小写混用不影响偏移量密钥长度通常 1~32长度越长越难破解但周期暴露风险也越高文本编码UTF-8 / ASCII中文等多字节字符会走非字母分支表格里“密钥长度越长越难破解”需要补充一句在维吉尼亚密码里密钥长度等于明文长度且随机生成时等价于一次一密但实际使用中人们喜欢用单词做密钥周期结构很容易被卡西斯基测试和索引重合分析识别。在线转换器不会限制密钥长度但实现时最好显式校验密钥只含字母、非空否则char_to_shift遇到非法字符会下标越界或产生负偏移。4. 维吉尼亚密码的密码分析不知道密钥也能破解4.1 卡西斯基测试从重复子串反推密钥长度卡西斯基测试的原理是如果明文中出现重复片段且两段之间的间隔恰好是密钥长度的整数倍那么它们会被同一段密钥加密密文中也会出现重复片段。因此找出密文里重复出现的子串计算相邻重复位置的间隔再取这些间隔的最大公约数就能得到密钥长度的一个估计。import re from math import gcd from functools import reduce def kasiski_repeat_gaps(text, min_len3): text re.sub(r[^A-Za-z], , text).upper() gaps [] for size in range(min_len, min_len 5): seen {} for i in range(len(text) - size): frag text[i:i size] if frag in seen: gaps.append(i - seen[frag]) seen[frag] i return gaps这段代码把所有长度为 3 到 7 的重叠子串记录第一次出现位置一旦再次出现就计算间隔并存入gaps。拿到间隔列表后可以用reduce(gcd, gaps)汇总比如所有间隔的最大公约数通常密钥长度是它的因数。代码简化了重复查找逻辑用字典记住上一次位置实际效果接近原理解释。4.2 索引重合与平均IC验证密钥长度的更稳定方法卡西斯基测试在短密文里噪声很大所以实际破解脚本更常用索引重合Index of Coincidence, IC。IC 表示从一段文本中随机抽取两个字母两者恰好相同的概率。英文单表文本的 IC 约为 0.067完全随机字母的 IC 约为 0.038。对维吉尼亚密文如果猜的密钥长度等于真实长度把密文按位置模密钥长度分成若干组每组内是同一个凯撒位移IC 会明显高于随机水平。实现如下def index_of_coincidence(text): n len(text) if n 2: return 0.0 freq {} for ch in text.upper(): if ch.isalpha(): freq[ch] freq.get(ch, 0) 1 return sum(v * (v - 1) for v in freq.values()) / (n * (n - 1)) def average_ic_by_key_len(text, key_len): total 0.0 cnt 0 for start in range(key_len): group text[start::key_len] if len(group) 2: total index_of_coincidence(group) cnt 1 return total / cnt if cnt else 0.0使用方法是循环key_len从 2 到 20计算平均 IC。例如对一段 300 个字母的英文密文各假设密钥长度下的平均 IC 可能如下假设密钥长度平均IC10.04020.03830.03940.06250.04160.038密钥长度 4 对应的 IC 明显高出一截真实长度就是 4 的整数倍。注意倍数的干扰2 倍真实长度会把每组的间隔拉大但频率分布依然接近所以平均 IC 也较高实际处理时优先选择 IC 第一次冲上去的位置而不是最大值。4.3 按位置分组后做凯撒频率分析还原密钥字母一旦得到密钥长度 m就把密文按位置模 m 分成 m 组。第 i 组里的每个字符都是明文被同一个偏移量加密的结果等价于一个凯撒密码。只要用字母频率分析猜出这个偏移就能得到密钥第 i 个字母。这里用卡方拟合优度作为偏移的评分指标解码后每个字母的频率与标准英文频率的卡方和越小偏移越可能是真正的密钥。from collections import Counter ENGLISH_FREQ { A: 8.2, B: 1.5, C: 2.8, D: 4.3, E: 12.7, F: 2.2, G: 2.0, H: 6.1, I: 7.0, J: 0.15, K: 0.77, L: 4.0, M: 2.4, N: 6.7, O: 7.5, P: 1.9, Q: 0.095, R: 6.0, S: 6.3, T: 9.1, U: 2.8, V: 0.98, W: 2.4, X: 0.15, Y: 2.0, Z: 0.074 } def chi_square_fit(text): n len(text) if n 0: return float(inf) counts Counter(text) return sum( (counts.get(ch, 0) * 100.0 / n - ENGLISH_FREQ[ch]) ** 2 / ENGLISH_FREQ[ch] for ch in ENGLISH_FREQ ) def find_shift_for_group(group): best_shift 0 best_score float(inf) for shift in range(26): decoded .join(chr((ord(c) - ord(A) - shift) % 26 ord(A)) for c in group) score chi_square_fit(decoded) if score best_score: best_score score best_shift shift return best_shift注意这里group必须是大写连续字母。对每个候选shift解码公式是 C - shift因为加密是 C P shift反推 P C - shift。最终best_shift就是该组对应的密钥偏移转成字母用chr(ord(A) best_shift)。4.4 自动破解脚本把IC和卡方串成一条流水线把前面几步串起来就得到一个完整可运行的破解脚本。IC 负责长度卡方负责逐字母还原省去了人工观察。import re from collections import Counter def clean_text(text): return re.sub(r[^A-Za-z], , text).upper() def index_of_coincidence(text): n len(text) if n 2: return 0.0 freq Counter(text) return sum(v * (v - 1) for v in freq.values()) / (n * (n - 1)) def average_ic_by_key_len(text, key_len): return sum( index_of_coincidence(text[start::key_len]) for start in range(key_len) ) / key_len ENGLISH_FREQ { A: 8.2, B: 1.5, C: 2.8, D: 4.3, E: 12.7, F: 2.2, G: 2.0, H: 6.1, I: 7.0, J: 0.15, K: 0.77, L: 4.0, M: 2.4, N: 6.7, O: 7.5, P: 1.9, Q: 0.095, R: 6.0, S: 6.3, T: 9.1, U: 2.8, V: 0.98, W: 2.4, X: 0.15, Y: 2.0, Z: 0.074 } def chi_square_fit(text): n len(text) if n 0: return float(inf) counts Counter(text) return sum( (counts.get(ch, 0) * 100.0 / n - ENGLISH_FREQ[ch]) ** 2 / ENGLISH_FREQ[ch] for ch in ENGLISH_FREQ ) def break_vigenere(ciphertext, max_key_len20): text clean_text(ciphertext) best_len 1 best_ic 0.0 for key_len in range(1, max_key_len 1): ic average_ic_by_key_len(text, key_len) if ic best_ic: best_ic ic best_len key_len key_chars [] for i in range(best_len): group text[i::best_len] best_shift 0 best_score float(inf) for shift in range(26): decoded .join(chr((ord(c) - ord(A) - shift) % 26 ord(A)) for c in group) score chi_square_fit(decoded) if score best_score: best_score score best_shift shift key_chars.append(chr(ord(A) best_shift)) return .join(key_chars), best_len这个脚本对几百个字符以上的英文密文通常能在几秒内给出正确的密钥字母返回的密钥统一为大写。它也有明显的边界如果密文本身是短句比如只有几十个字符IC 判断长度会不可靠卡方统计也会因样本太少而错判。这类短文本更适合配合卡西斯基重复片段人工观察或者在已知密钥长度范围时手动指定key_len再跑分组频率分析。注意短密文下 IC 与卡方统计都不稳定脚本会自动选第一个 IC 峰值建议先打印 0~10 的 IC 曲线再决定是否信任自动长度。5. 维吉尼亚密码的参数调整、误用与在线转换器的验证技巧5.1 用固定向量自检转换器不管写的是命令行工具还是在线转换器都要先过一个固定的自检向量。推荐用维吉尼亚密码教材里的经典样例明文ATTACKATDAWN密钥LEMON加密结果LXFOPVEFRNHR。注意这里明文没有空格输出是连续大写字母容易核对。assert vigenere(ATTACKATDAWN, LEMON, modeencrypt, skip_non_alphaTrue) LXFOPVEFRNHR assert vigenere(LXFOPVEFRNHR, LEMON, modedecrypt, skip_non_alphaTrue) ATTACKATDAWN很多在线转换器默认保留空格所以跑同样的输入会得到带空格的版本这不代表实现错误只是参数选择不同。因此在自检时还要额外验证带空格的版本确保skip_non_alpha参数在加解密两侧一致。5.2 三个必调参数与常见误用在线转换器和脚本里最容易出错的是三个参数skip_non_alpha、密钥的清洗、mode 的传递。下面的表汇总了它们的推荐设置参数推荐设置误用后果skip_non_alpha加解密两侧保持一致空格被丢弃或保留不一致解密文本错位key 清洗只保留 A-Z转大写带标点的密钥产生负偏移取模异常mode用变量而不是两个函数同一段逻辑维护两份容易在修改时不同步密钥清洗的常见做法是在调用char_to_shift之前先执行re.sub(r[^A-Za-z], , key).upper()这样密钥里的空格和连字符都会消失len(key)也符合预期。如果忘记这一步用户输入lemon-key时char_to_shift(-)会返回负数加密结果直接错乱。5.3 密钥长度与周期泄露一个容易被忽略的验证技巧维吉尼亚密码的密钥若重复使用密文中不同位置会共享同一段偏移这给了攻击者交叉分析的机会。验证一个密文是否来自维吉尼亚密码的笨办法是画平均 IC 随假设密钥长度的曲线如果某长度对应的 IC 突然跃升到 0.06 附近就说明存在周期性如果所有长度 IC 都在 0.04 上下说明要么密钥接近明文长度要么文本太短统计不出来。验证破解结果是否正确的技巧是用还原出的密钥重新加密明文比对是否与原密文相同而不是只看解密明文是否“像英文”。因为卡方统计在短文本上可能选到错误偏移解出来的文本偶尔也能拼出几个可读单词但重加密比对可以精确暴露个别字母的错误。比如对长度比较短的密文可以打印每个分组的卡方分数排行定位到置信度低的密钥字母。本文还有配套的精品资源点击获取
返回列表