维吉尼亚密码(Vigenère Cipher)是一种多表代换密码。凯撒密码对所有字母使用同一个位移,维吉尼亚密码则由密钥词决定每个位置的位移,并循环使用密钥。因此,同一个明文字母出现在不同位置时,可能变成不同的密文字母;密钥只有一个字母时,它就退化为凯撒密码。
先将 A~Z 编码为 0~25。设明文第 i 个字母的数值为 Pᵢ,密钥长度为 L,密钥第 j 个字母的数值为 Kⱼ;下标从 0 开始。明文第 i 个位置使用 K[i mod L],即不断重复密钥,直到覆盖整个明文。
字母:A B C D E … Z
数值:0 1 2 3 4 … 25
加密:Cᵢ = (Pᵢ + K[i mod L]) mod 26
解密:Pᵢ = (Cᵢ - K[i mod L] + 26) mod 26
模 26 表示超过 Z 后从 A 重新开始。例如 Z + B 对应 (25 + 1) mod 26 = 0,结果为 A。解密时加 26 是为了避免相减为负数;在 JavaScript 中,负数的 % 运算结果仍可能为负。
明文为 ATTACK AT DAWN,密钥为 LEMON。移除空格后,明文有 12 个字母,密钥循环对齐如下:
位置: 0 1 2 3 4 5 6 7 8 9 10 11
明文: 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
第 0 位:A(0) + L(11) → L(11);第 1 位:T(19) + E(4) → X(23);第 2 位:T(19) + M(12) → F(5)。解密第 2 位时,(5 - 12 + 26) mod 26 = 19,恢复为 T。最终密文为 LXFOPVEFRNHR,解密结果为 ATTACKATDAWN。
本页面仅对 A–Z 字母进行运算,并统一转换为大写;空格、标点、数字、中文和其他字符均被过滤。密钥只允许英文字母。结果不能还原被过滤的字符。
界面要求密钥为 1~20 个英文字母,且明文或密文至少包含一个英文字母。过滤发生在密钥对齐之前,例如 A B 与 AB 使用完全相同的密钥位置。这里使用的是重复密钥版本,不是自动密钥密码。
若密钥长度为 L,则下标为 j、j+L、j+2L……的字母都使用同一个密钥字母。因此,将密文按“下标 mod L”分成 L 组后,每组都只是一个固定移位的凯撒密码。例如 LEMON 长度为 5,第 0、5、10 位使用 L,第 1、6、11 位使用 E。破解可以分为“推测长度”和“逐组推测位移”两步。
IC = Σ fⱼ(fⱼ - 1) / [N(N - 1)],j = 0…25
N:组内字母总数;fⱼ:第 j 个字母在该组中的次数
平均 IC = 各组 IC 之和 / 分组数 L
IC 可以理解为从同一组中不放回地抽取两个字母,两者相同的概率。均匀随机字母的参考值约为 1/26 ≈ 0.0385,普通英语文本约为 0.065。固定的凯撒移位只改变字母名称,不改变次数分布,所以正确分组往往能保留英语文本的 IC 特征。
本页面尝试 1~min(20, ⌊密文字母数/5⌋) 的长度,按平均 IC 与 0.065 的距离从小到大保留前 5 个候选;距离相同则优先较短长度。这只是控制候选范围的启发式规则,并不意味着每组 5 个字母就足够可靠。正确长度的倍数也可能获得相似 IC。
对每组分别尝试解密位移 s = 0…25:
Oⱼ = 按位移 s 解密后,第 j 个字母的实际次数
Eⱼ = 该组长度 × 英语中第 j 个字母的参考概率
χ²(s) = Σ (Oⱼ - Eⱼ)² / Eⱼ
取 χ² 最小的 s 作为该组候选位移;0→A,1→B,…,25→Z。
英语字母出现概率并不均匀,例如 E 的参考频率约为 12.702%,T 约为 9.056%。卡方值衡量候选解密结果与该参考分布的偏离程度,越小表示频率越接近。将各组的候选位移转回字母并按组序拼接,就得到候选密钥,再解密整段密文并检查内容是否可读。
“平均卡方”是各组最小卡方值的算术平均,不是正确概率,也不适合作为不同密钥长度之间的严格置信度。页面先展示手选长度,再附加已有的候选长度,最多生成 5 行结果;每个长度只输出逐组最优位移组成的一把密钥,不会穷举全部 26ᴸ 种密钥,也未实现 Kasiski 重复片段分析或词典评分。
建议使用 100 个以上的英语字母,并结合候选明文的可读性手工选择长度、调整密钥。维吉尼亚密码适合学习古典密码与统计分析,不适合保护现代敏感数据。
算法使用 JavaScript 字符编码进行运算,A 的编码为 65。文本先过滤再转大写;密钥必须先通过完整校验,不能把非法密钥中的字符静默删除。以下代码与本页实际计算逻辑对应,为便于阅读展开了部分表达式。
const extractLetters = text =>
String(text ?? '').replace(/[^a-zA-Z]/g, '').toUpperCase()
const validKey = key => /^[A-Za-z]{1,20}$/.test(key)
// 界面在调用 encrypt / decrypt 前执行:
if (!validKey(keyword)) throw new Error('密钥必须是 1–20 位英文字母')
if (!extractLetters(text)) throw new Error('至少需要一个英文字母')
通过 i % k.length 获取循环密钥下标,将字母转换为 0~25,完成模运算后加回 65 转为字母。解密与加密的区别在于减去密钥位移。
function encrypt(text, key) {
const p = extractLetters(text), k = extractLetters(key)
if (!k) throw new Error('请输入合法的英文字母密钥')
return Array.from(p, (ch, i) => {
const plain = ch.charCodeAt(0) - 65
const shift = k.charCodeAt(i % k.length) - 65
return String.fromCharCode(65 + (plain + shift) % 26)
}).join('')
}
function decrypt(text, key) {
const c = extractLetters(text), k = extractLetters(key)
if (!k) throw new Error('请输入合法的英文字母密钥')
return Array.from(c, (ch, i) => {
const cipher = ch.charCodeAt(0) - 65
const shift = k.charCodeAt(i % k.length) - 65
return String.fromCharCode(65 + (cipher - shift + 26) % 26)
}).join('')
}
encrypt('ATTACK AT DAWN', 'LEMON') // LXFOPVEFRNHR
decrypt('LXFOPVEFRNHR', 'LEMON') // ATTACKATDAWN
下列统计函数接收已经标准化的 A~Z 密文。每个字母按下标取模分配到对应组;不足两个字母的组无法抽取两个不同位置,IC 按 0 处理。
function splitColumns(cipher, length) {
const groups = Array.from({ length }, () => '')
for (let i = 0; i < cipher.length; i++) {
groups[i % length] += cipher[i]
}
return groups
}
function countLetters(text) {
const counts = Array(26).fill(0)
for (const ch of text) counts[ch.charCodeAt(0) - 65]++
return counts
}
function calcIC(text) {
const n = text.length
if (n < 2) return 0
return countLetters(text).reduce((sum, f) => sum + f * (f - 1), 0)
/ (n * (n - 1))
}
对每个长度重新分组,计算各组 IC 的算术平均,再按与英语参考值的距离排序;返回的 length 和 ic 用于显示候选按钮。
function guessLengths(cipher) {
const max = Math.min(20, Math.floor(cipher.length / 5))
if (max < 1) return []
return Array.from({ length: max }, (_, i) => {
const length = i + 1
const groups = splitColumns(cipher, length)
const ic = groups.reduce((sum, g) => sum + calcIC(g), 0) / length
return { length, ic }
}).sort((a, b) =>
Math.abs(a.ic - 0.065) - Math.abs(b.ic - 0.065)
|| a.length - b.length
).slice(0, 5)
}
englishFreq 按 A~Z 的顺序保存英语字母参考频率,单位是百分比。直接统计“按某个位移解密后”的字母次数,无需为每次尝试构造完整明文。
const englishFreq = [
8.167, 1.492, 2.782, 4.253, 12.702, 2.228, 2.015,
6.094, 6.966, 0.153, 0.772, 4.025, 2.406, 6.749,
7.507, 1.929, 0.095, 5.987, 6.327, 9.056, 2.758,
0.978, 2.360, 0.150, 1.974, 0.074
]
function chiSquare(text, shift) {
const n = text.length
if (!n) return Infinity
const counts = Array(26).fill(0)
for (const ch of text) {
counts[(ch.charCodeAt(0) - 65 - shift + 26) % 26]++
}
return counts.reduce((sum, observed, i) => {
const expected = n * englishFreq[i] / 100
return sum + (observed - expected) ** 2 / expected
}, 0)
}
function crackKeyword(cipher, length) {
const shifts = splitColumns(cipher, length).map(group => {
let best = 0, score = Infinity
for (let s = 0; s < 26; s++) {
const candidate = chiSquare(group, s)
if (candidate < score) { best = s; score = candidate }
}
return { shift: best, score }
})
return {
keyword: shifts.map(v => String.fromCharCode(65 + v.shift)).join(''),
score: shifts.reduce((sum, v) => sum + v.score, 0) / length
}
}
若有效字母数为 N,单次加解密时间复杂度为 O(N)。固定长度 L 的密钥恢复约需 O(26N + 26²L) 运算;页面限制长度不超过 20,适合交互式教学。统计分数仅辅助分析,最终仍需检查语言内容和密钥合理性。