|
|
@@ -1,219 +1,212 @@
|
|
|
<template>
|
|
|
<div class="vigenere-container">
|
|
|
- <el-card
|
|
|
- class="box-card"
|
|
|
- shadow="never"
|
|
|
- >
|
|
|
+ <el-card class="box-card" shadow="hover">
|
|
|
<template #header><strong>维吉尼亚密码加解密与破解模拟</strong></template>
|
|
|
- <el-tabs
|
|
|
- v-model="activeTab"
|
|
|
- type="border-card"
|
|
|
- @tab-change="onTabChange"
|
|
|
- >
|
|
|
- <el-tab-pane
|
|
|
- label="原理说明"
|
|
|
- name="principle"
|
|
|
- >
|
|
|
+ <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
|
|
|
+ <el-tab-pane label="原理说明" name="principle">
|
|
|
<div class="principle-content">
|
|
|
<h3>维吉尼亚密码工作原理</h3>
|
|
|
- <p>维吉尼亚密码使用英文字母密钥进行重复位移。加密 Cᵢ = (Pᵢ + Kᵢ) mod 26,解密 Pᵢ = (Cᵢ - Kᵢ + 26) mod 26。密钥字母 A 对应位移 0,B 对应位移 1。</p>
|
|
|
+ <p>维吉尼亚密码(Vigenère Cipher)是一种多表代换密码。凯撒密码对所有字母使用同一个位移,维吉尼亚密码则由密钥词决定每个位置的位移,并循环使用密钥。因此,同一个明文字母出现在不同位置时,可能变成不同的密文字母;密钥只有一个字母时,它就退化为凯撒密码。</p>
|
|
|
+ <h4>1. 字母编码与循环密钥</h4>
|
|
|
+ <p>先将 A~Z 编码为 0~25。设明文第 i 个字母的数值为 Pᵢ,密钥长度为 L,密钥第 j 个字母的数值为 Kⱼ;下标从 0 开始。明文第 i 个位置使用 K[i mod L],即不断重复密钥,直到覆盖整个明文。</p>
|
|
|
+ <pre><code>字母: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</code></pre>
|
|
|
+ <p>模 26 表示超过 Z 后从 A 重新开始。例如 Z + B 对应 (25 + 1) mod 26 = 0,结果为 A。解密时加 26 是为了避免相减为负数;在 JavaScript 中,负数的 % 运算结果仍可能为负。</p>
|
|
|
+ <h4>2. 完整加解密示例</h4>
|
|
|
+ <p>明文为 ATTACK AT DAWN,密钥为 LEMON。移除空格后,明文有 12 个字母,密钥循环对齐如下:</p>
|
|
|
+ <pre><code>位置: 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</code></pre>
|
|
|
+ <p>第 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。</p>
|
|
|
+ <h4>3. 本页面的输入规则</h4>
|
|
|
<p>本页面仅对 A–Z 字母进行运算,并统一转换为大写;空格、标点、数字、中文和其他字符均被过滤。密钥只允许英文字母。结果不能还原被过滤的字符。</p>
|
|
|
+ <p>界面要求密钥为 1~20 个英文字母,且明文或密文至少包含一个英文字母。过滤发生在密钥对齐之前,例如 A B 与 AB 使用完全相同的密钥位置。这里使用的是重复密钥版本,不是自动密钥密码。</p>
|
|
|
<h3>统计破解原理</h3>
|
|
|
- <p>首先尝试不同的密钥长度,将密文按位置分组,计算各组的重合指数(IC);再针对每组测试 26 种凯撒位移,用英语字母频率的卡方统计量选择较合理的位移。</p>
|
|
|
- <el-alert
|
|
|
- title="统计分析不能保证恢复正确密钥;短密文、非自然英语文本以及密钥长度的倍数都可能产生误判。"
|
|
|
- type="warning"
|
|
|
- :closable="false"
|
|
|
- show-icon
|
|
|
- />
|
|
|
- <div class="flowchart-placeholder"><div
|
|
|
- ref="mermaidContainer"
|
|
|
- class="mermaid"
|
|
|
- /></div>
|
|
|
+ <h4>1. 为什么可以拆成多个凯撒密码</h4>
|
|
|
+ <p>若密钥长度为 L,则下标为 j、j+L、j+2L……的字母都使用同一个密钥字母。因此,将密文按“下标 mod L”分成 L 组后,每组都只是一个固定移位的凯撒密码。例如 LEMON 长度为 5,第 0、5、10 位使用 L,第 1、6、11 位使用 E。破解可以分为“推测长度”和“逐组推测位移”两步。</p>
|
|
|
+ <h4>2. 用重合指数(IC)推测密钥长度</h4>
|
|
|
+ <pre><code>IC = Σ fⱼ(fⱼ - 1) / [N(N - 1)],j = 0…25
|
|
|
+N:组内字母总数;fⱼ:第 j 个字母在该组中的次数
|
|
|
+平均 IC = 各组 IC 之和 / 分组数 L</code></pre>
|
|
|
+ <p>IC 可以理解为从同一组中不放回地抽取两个字母,两者相同的概率。均匀随机字母的参考值约为 1/26 ≈ 0.0385,普通英语文本约为 0.065。固定的凯撒移位只改变字母名称,不改变次数分布,所以正确分组往往能保留英语文本的 IC 特征。</p>
|
|
|
+ <p>本页面尝试 1~min(20, ⌊密文字母数/5⌋) 的长度,按平均 IC 与 0.065 的距离从小到大保留前 5 个候选;距离相同则优先较短长度。这只是控制候选范围的启发式规则,并不意味着每组 5 个字母就足够可靠。正确长度的倍数也可能获得相似 IC。</p>
|
|
|
+ <h4>3. 用卡方统计量恢复密钥字母</h4>
|
|
|
+ <pre><code>对每组分别尝试解密位移 s = 0…25:
|
|
|
+ Oⱼ = 按位移 s 解密后,第 j 个字母的实际次数
|
|
|
+ Eⱼ = 该组长度 × 英语中第 j 个字母的参考概率
|
|
|
+ χ²(s) = Σ (Oⱼ - Eⱼ)² / Eⱼ
|
|
|
+取 χ² 最小的 s 作为该组候选位移;0→A,1→B,…,25→Z。</code></pre>
|
|
|
+ <p>英语字母出现概率并不均匀,例如 E 的参考频率约为 12.702%,T 约为 9.056%。卡方值衡量候选解密结果与该参考分布的偏离程度,越小表示频率越接近。将各组的候选位移转回字母并按组序拼接,就得到候选密钥,再解密整段密文并检查内容是否可读。</p>
|
|
|
+ <h4>4. 如何理解破解结果</h4>
|
|
|
+ <p>“平均卡方”是各组最小卡方值的算术平均,不是正确概率,也不适合作为不同密钥长度之间的严格置信度。页面先展示手选长度,再附加已有的候选长度,最多生成 5 行结果;每个长度只输出逐组最优位移组成的一把密钥,不会穷举全部 26ᴸ 种密钥,也未实现 Kasiski 重复片段分析或词典评分。</p>
|
|
|
+ <el-alert title="统计分析不能保证恢复正确密钥;短密文、非自然英语文本以及密钥长度的倍数都可能产生误判。" type="warning" :closable="false" show-icon />
|
|
|
+ <p>建议使用 100 个以上的英语字母,并结合候选明文的可读性手工选择长度、调整密钥。维吉尼亚密码适合学习古典密码与统计分析,不适合保护现代敏感数据。</p>
|
|
|
+ <div class="flowchart-placeholder"><div ref="mermaidContainer" class="mermaid"></div></div>
|
|
|
</div>
|
|
|
</el-tab-pane>
|
|
|
- <el-tab-pane
|
|
|
- label="算法实现"
|
|
|
- name="implementation"
|
|
|
- >
|
|
|
+ <el-tab-pane label="算法实现" name="implementation">
|
|
|
<div class="code-content">
|
|
|
- <h3>算法概要</h3>
|
|
|
- <pre><code>加密:C[i] = (P[i] + K[i % L]) % 26
|
|
|
-解密:P[i] = (C[i] - K[i % L] + 26) % 26
|
|
|
-重合指数:IC = Σ f(c) × (f(c)-1) / (N × (N-1))
|
|
|
-卡方值:χ² = Σ (观察次数 - 预期次数)² / 预期次数
|
|
|
-对每个候选密钥长度 L:
|
|
|
- 1. 按下标模 L 分成 L 组
|
|
|
- 2. 计算平均 IC,按英语 IC 参考值排序
|
|
|
- 3. 每组尝试 26 种解密位移,取卡方值最小者
|
|
|
- 4. 拼接候选密钥并解密,供人工判断</code></pre>
|
|
|
- <p>参考值 0.065 是英语自然文本的近似 IC,不是保证正确的判定阈值;卡方值也仅用于排序。</p>
|
|
|
+ <h3>1. 输入标准化与校验</h3>
|
|
|
+ <p>算法使用 JavaScript 字符编码进行运算,A 的编码为 65。文本先过滤再转大写;密钥必须先通过完整校验,不能把非法密钥中的字符静默删除。以下代码与本页实际计算逻辑对应,为便于阅读展开了部分表达式。</p>
|
|
|
+ <pre><code>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('至少需要一个英文字母')</code></pre>
|
|
|
+ <h3>2. 加密与解密</h3>
|
|
|
+ <p>通过 i % k.length 获取循环密钥下标,将字母转换为 0~25,完成模运算后加回 65 转为字母。解密与加密的区别在于减去密钥位移。</p>
|
|
|
+ <pre><code>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</code></pre>
|
|
|
+ <h3>3. 分组与重合指数</h3>
|
|
|
+ <p>下列统计函数接收已经标准化的 A~Z 密文。每个字母按下标取模分配到对应组;不足两个字母的组无法抽取两个不同位置,IC 按 0 处理。</p>
|
|
|
+ <pre><code>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))
|
|
|
+}</code></pre>
|
|
|
+ <h3>4. 候选密钥长度排序</h3>
|
|
|
+ <p>对每个长度重新分组,计算各组 IC 的算术平均,再按与英语参考值的距离排序;返回的 length 和 ic 用于显示候选按钮。</p>
|
|
|
+ <pre><code>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)
|
|
|
+}</code></pre>
|
|
|
+ <h3>5. 卡方评分与候选密钥恢复</h3>
|
|
|
+ <p>englishFreq 按 A~Z 的顺序保存英语字母参考频率,单位是百分比。直接统计“按某个位移解密后”的字母次数,无需为每次尝试构造完整明文。</p>
|
|
|
+ <pre><code>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
|
|
|
+ }
|
|
|
+}</code></pre>
|
|
|
+ <h3>6. 结果生成与验证</h3>
|
|
|
+ <ol>
|
|
|
+ <li>破解输入至少需要 20 个英文字母;手选长度必须是 1~20 的整数,且不能大于密文字母数。</li>
|
|
|
+ <li>合并手选长度与已有的候选长度,去掉与手选长度重复的项,最多处理 5 个长度。</li>
|
|
|
+ <li>对每个长度调用 crackKeyword,再用返回的密钥调用 decrypt,展示密钥、平均卡方和候选明文。</li>
|
|
|
+ <li>点击“使用候选”后,将结果回填到加解密表单;解密比对针对标准化后的明文,不包含原始空格与标点,也不能据此证明破解结果真实。</li>
|
|
|
+ </ol>
|
|
|
+ <p>若有效字母数为 N,单次加解密时间复杂度为 O(N)。固定长度 L 的密钥恢复约需 O(26N + 26²L) 运算;页面限制长度不超过 20,适合交互式教学。统计分数仅辅助分析,最终仍需检查语言内容和密钥合理性。</p>
|
|
|
</div>
|
|
|
</el-tab-pane>
|
|
|
- <el-tab-pane
|
|
|
- label="加解密与破解"
|
|
|
- name="calculation"
|
|
|
- >
|
|
|
- <el-radio-group
|
|
|
- v-model="mode"
|
|
|
- class="mode-group"
|
|
|
- >
|
|
|
+ <el-tab-pane label="加解密与破解" name="calculation">
|
|
|
+ <el-radio-group v-model="mode" class="mode-group">
|
|
|
<el-radio value="encrypt">加解密模式</el-radio>
|
|
|
<el-radio value="crack">破解模式</el-radio>
|
|
|
</el-radio-group>
|
|
|
- <el-form
|
|
|
- v-if="mode === 'encrypt'"
|
|
|
- :model="encryptForm"
|
|
|
- label-width="100px"
|
|
|
- class="calc-form"
|
|
|
- >
|
|
|
- <el-form-item label="明文"><el-input
|
|
|
- v-model="encryptForm.plainText"
|
|
|
- type="textarea"
|
|
|
- :rows="3"
|
|
|
- placeholder="示例:ATTACK AT DAWN"
|
|
|
- /></el-form-item>
|
|
|
- <el-form-item label="密钥词"><el-input
|
|
|
- v-model="encryptForm.keyword"
|
|
|
- maxlength="20"
|
|
|
- placeholder="仅英文字母,如 LEMON"
|
|
|
- /></el-form-item>
|
|
|
- <el-form-item label="密文"><el-input
|
|
|
- v-model="encryptForm.cipherText"
|
|
|
- type="textarea"
|
|
|
- :rows="3"
|
|
|
- placeholder="加密结果,也可粘贴待解密密文"
|
|
|
- /></el-form-item>
|
|
|
- <el-form-item
|
|
|
- label-width="0"
|
|
|
- class="action-item"
|
|
|
- ><div class="actions">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- :icon="Lock"
|
|
|
- @click="handleEncrypt"
|
|
|
- >加密计算</el-button>
|
|
|
- <el-button
|
|
|
- type="success"
|
|
|
- :icon="Unlock"
|
|
|
- @click="handleDecrypt"
|
|
|
- >解密计算</el-button>
|
|
|
- <el-button
|
|
|
- :icon="Delete"
|
|
|
- @click="clearEncrypt"
|
|
|
- >清空</el-button>
|
|
|
+ <el-form v-if="mode === 'encrypt'" :model="encryptForm" label-width="100px" class="calc-form">
|
|
|
+ <el-form-item label="明文"><el-input v-model="encryptForm.plainText" type="textarea" :rows="3" placeholder="示例:ATTACK AT DAWN" /></el-form-item>
|
|
|
+ <el-form-item label="密钥词"><el-input v-model="encryptForm.keyword" maxlength="20" placeholder="仅英文字母,如 LEMON" /></el-form-item>
|
|
|
+ <el-form-item label="密文"><el-input v-model="encryptForm.cipherText" type="textarea" :rows="3" placeholder="加密结果,也可粘贴待解密密文" /></el-form-item>
|
|
|
+ <el-form-item label-width="0" class="action-item"><div class="actions">
|
|
|
+ <el-button type="primary" :icon="Lock" @click="handleEncrypt">加密计算</el-button>
|
|
|
+ <el-button type="success" :icon="Unlock" @click="handleDecrypt">解密计算</el-button>
|
|
|
+ <el-button :icon="Delete" @click="clearEncrypt">清空</el-button>
|
|
|
</div></el-form-item>
|
|
|
- <el-form-item
|
|
|
- v-if="decryptedResult !== null"
|
|
|
- label="解密结果"
|
|
|
- ><el-input
|
|
|
- :model-value="decryptedResult"
|
|
|
- type="textarea"
|
|
|
- :rows="3"
|
|
|
- readonly
|
|
|
- /></el-form-item>
|
|
|
- <el-form-item v-if="decryptedResult !== null"><el-alert
|
|
|
- :title="compareMessage"
|
|
|
- :type="compareType"
|
|
|
- :closable="false"
|
|
|
- show-icon
|
|
|
- /></el-form-item>
|
|
|
+ <el-form-item v-if="decryptedResult !== null" label="解密结果"><el-input :model-value="decryptedResult" type="textarea" :rows="3" readonly /></el-form-item>
|
|
|
+ <el-form-item v-if="decryptedResult !== null"><el-alert :title="compareMessage" :type="compareType" :closable="false" show-icon /></el-form-item>
|
|
|
</el-form>
|
|
|
- <el-form
|
|
|
- v-else
|
|
|
- :model="crackForm"
|
|
|
- label-width="120px"
|
|
|
- class="calc-form"
|
|
|
- >
|
|
|
- <el-form-item label="待破解密文"><el-input
|
|
|
- v-model="crackForm.cipherText"
|
|
|
- type="textarea"
|
|
|
- :rows="4"
|
|
|
- placeholder="建议至少 100 个英语字母,文本越长越容易分析"
|
|
|
- /></el-form-item>
|
|
|
- <el-form-item
|
|
|
- label-width="0"
|
|
|
- class="action-item"
|
|
|
- ><div class="actions">
|
|
|
- <el-button
|
|
|
- type="warning"
|
|
|
- :icon="Search"
|
|
|
- @click="handleGuessLength"
|
|
|
- >推测密钥长度</el-button>
|
|
|
- <el-button
|
|
|
- :icon="Delete"
|
|
|
- @click="clearCrack"
|
|
|
- >清空破解</el-button>
|
|
|
+ <el-form v-else :model="crackForm" label-width="120px" class="calc-form">
|
|
|
+ <el-form-item label="待破解密文"><el-input v-model="crackForm.cipherText" type="textarea" :rows="4" placeholder="建议至少 100 个英语字母,文本越长越容易分析" /></el-form-item>
|
|
|
+ <el-form-item label-width="0" class="action-item"><div class="actions">
|
|
|
+ <el-button type="warning" :icon="Search" @click="handleGuessLength">推测密钥长度</el-button>
|
|
|
+ <el-button :icon="Delete" @click="clearCrack">清空破解</el-button>
|
|
|
</div></el-form-item>
|
|
|
- <el-form-item
|
|
|
- v-if="lengthOptions.length"
|
|
|
- label="候选密钥长度"
|
|
|
- >
|
|
|
+ <el-form-item v-if="lengthOptions.length" label="候选密钥长度">
|
|
|
<div class="length-options">
|
|
|
- <el-button
|
|
|
- v-for="item in lengthOptions"
|
|
|
- :key="item.length"
|
|
|
- :type="item.length === crackForm.targetLength ? 'primary' : 'default'"
|
|
|
- size="small"
|
|
|
- @click="selectLength(item.length)"
|
|
|
- >
|
|
|
+ <el-button v-for="item in lengthOptions" :key="item.length" :type="item.length === crackForm.targetLength ? 'primary' : 'default'" size="small" @click="selectLength(item.length)">
|
|
|
{{ item.length }} (IC {{ item.ic.toFixed(4) }})
|
|
|
</el-button>
|
|
|
<div class="form-tips">按接近英语 IC 参考值排列;也可以手工选择其他长度。</div>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="目标密钥长度"><div class="actions"><el-input-number
|
|
|
- v-model="crackForm.targetLength"
|
|
|
- :min="1"
|
|
|
- :max="20"
|
|
|
- :step="1"
|
|
|
- step-strictly
|
|
|
- /><el-button
|
|
|
- type="primary"
|
|
|
- :icon="MagicStick"
|
|
|
- @click="handleAutoCrack"
|
|
|
- >生成破解候选</el-button></div></el-form-item>
|
|
|
- <div
|
|
|
- v-if="crackResults.length"
|
|
|
- class="crack-results"
|
|
|
- >
|
|
|
+ <el-form-item label="目标密钥长度"><div class="actions"><el-input-number v-model="crackForm.targetLength" :min="1" :max="20" :step="1" step-strictly /><el-button type="primary" :icon="MagicStick" @click="handleAutoCrack">生成破解候选</el-button></div></el-form-item>
|
|
|
+ <div v-if="crackResults.length" class="crack-results">
|
|
|
<h4>统计破解候选(并非保证正确)</h4>
|
|
|
- <el-table
|
|
|
- :data="crackResults"
|
|
|
- border
|
|
|
- stripe
|
|
|
- style="width:100%"
|
|
|
- >
|
|
|
- <el-table-column
|
|
|
- prop="length"
|
|
|
- label="长度"
|
|
|
- width="74"
|
|
|
- />
|
|
|
- <el-table-column
|
|
|
- prop="keyword"
|
|
|
- label="候选密钥"
|
|
|
- width="160"
|
|
|
- />
|
|
|
- <el-table-column
|
|
|
- prop="score"
|
|
|
- label="平均卡方"
|
|
|
- width="106"
|
|
|
- ><template #default="{ row }">{{ row.score.toFixed(2) }}</template></el-table-column>
|
|
|
- <el-table-column
|
|
|
- prop="plainText"
|
|
|
- label="候选明文"
|
|
|
- min-width="230"
|
|
|
- show-overflow-tooltip
|
|
|
- />
|
|
|
- <el-table-column
|
|
|
- label="操作"
|
|
|
- width="105"
|
|
|
- ><template #default="{ row }"><el-button
|
|
|
- size="small"
|
|
|
- type="primary"
|
|
|
- link
|
|
|
- @click="useCandidate(row)"
|
|
|
- >使用候选</el-button></template></el-table-column>
|
|
|
+ <el-table :data="crackResults" border stripe style="width:100%">
|
|
|
+ <el-table-column prop="length" label="长度" width="74" />
|
|
|
+ <el-table-column prop="keyword" label="候选密钥" width="160" />
|
|
|
+ <el-table-column prop="score" label="平均卡方" width="106"><template #default="{ row }">{{ row.score.toFixed(2) }}</template></el-table-column>
|
|
|
+ <el-table-column prop="plainText" label="候选明文" min-width="230" show-overflow-tooltip />
|
|
|
+ <el-table-column label="操作" width="105"><template #default="{ row }"><el-button size="small" type="primary" link @click="useCandidate(row)">使用候选</el-button></template></el-table-column>
|
|
|
</el-table>
|
|
|
</div>
|
|
|
</el-form>
|
|
|
@@ -239,130 +232,130 @@ const crackResults = ref([])
|
|
|
const mermaidContainer = ref(null)
|
|
|
const extractLetters = text => String(text ?? '').replace(/[^a-zA-Z]/g, '').toUpperCase()
|
|
|
const validKey = key => /^[A-Za-z]{1,20}$/.test(key)
|
|
|
-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]
|
|
|
-const encrypt = (text, key) => {
|
|
|
- const p = extractLetters(text); const k = extractLetters(key)
|
|
|
+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]
|
|
|
+const encrypt = (text,key) => {
|
|
|
+ const p = extractLetters(text), k = extractLetters(key)
|
|
|
if (!k) throw new Error('请输入合法的英文字母密钥')
|
|
|
- return Array.from(p, (c, i) => String.fromCharCode(65 + (c.charCodeAt(0) - 65 + k.charCodeAt(i % k.length) - 65) % 26)).join('')
|
|
|
+ return Array.from(p,(c,i)=>String.fromCharCode(65+(c.charCodeAt(0)-65+k.charCodeAt(i%k.length)-65)%26)).join('')
|
|
|
}
|
|
|
-const decrypt = (text, key) => {
|
|
|
- const p = extractLetters(text); const k = extractLetters(key)
|
|
|
+const decrypt = (text,key) => {
|
|
|
+ const p = extractLetters(text), k = extractLetters(key)
|
|
|
if (!k) throw new Error('请输入合法的英文字母密钥')
|
|
|
- return Array.from(p, (c, i) => String.fromCharCode(65 + (c.charCodeAt(0) - k.charCodeAt(i % k.length) + 26) % 26)).join('')
|
|
|
+ return Array.from(p,(c,i)=>String.fromCharCode(65+(c.charCodeAt(0)-k.charCodeAt(i%k.length)+26)%26)).join('')
|
|
|
}
|
|
|
const countLetters = text => {
|
|
|
const counts = Array(26).fill(0)
|
|
|
- for (const c of text) counts[c.charCodeAt(0) - 65]++
|
|
|
+ for (const c of text) counts[c.charCodeAt(0)-65]++
|
|
|
return counts
|
|
|
}
|
|
|
const calcIC = text => {
|
|
|
const n = text.length
|
|
|
if (n < 2) return 0
|
|
|
- return countLetters(text).reduce((sum, c) => sum + c * (c - 1), 0) / (n * (n - 1))
|
|
|
+ return countLetters(text).reduce((sum,c)=>sum+c*(c-1),0)/(n*(n-1))
|
|
|
}
|
|
|
-const splitColumns = (cipher, length) => {
|
|
|
- const groups = Array.from({ length }, () => '')
|
|
|
- for (let i = 0; i < cipher.length; i++) groups[i % length] += cipher[i]
|
|
|
+const splitColumns = (cipher,length) => {
|
|
|
+ const groups = Array.from({length},()=> '')
|
|
|
+ for (let i=0;i<cipher.length;i++) groups[i%length]+=cipher[i]
|
|
|
return groups
|
|
|
}
|
|
|
const guessLengths = cipher => {
|
|
|
- const max = Math.min(20, Math.floor(cipher.length / 5))
|
|
|
+ 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)
|
|
|
+ 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)
|
|
|
}
|
|
|
-const chiSquare = (text, shift) => {
|
|
|
- const n = text.length
|
|
|
+const 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, v, i) => {
|
|
|
- const expected = n * englishFreq[i] / 100
|
|
|
- return sum + (v - expected) ** 2 / expected
|
|
|
- }, 0)
|
|
|
+ const counts=Array(26).fill(0)
|
|
|
+ for (const ch of text) counts[(ch.charCodeAt(0)-65-shift+26)%26]++
|
|
|
+ return counts.reduce((sum,v,i)=>{
|
|
|
+ const expected=n*englishFreq[i]/100
|
|
|
+ return sum+(v-expected)**2/expected
|
|
|
+ },0)
|
|
|
}
|
|
|
-const crackKeyword = (cipher, length) => {
|
|
|
- const groups = splitColumns(cipher, length)
|
|
|
- const shifts = groups.map(group => {
|
|
|
- let best = 0; let score = Infinity
|
|
|
- for (let s = 0; s < 26; s++) {
|
|
|
- const candidate = chiSquare(group, s)
|
|
|
- if (candidate < score) { best = s; score = candidate }
|
|
|
+const crackKeyword = (cipher,length) => {
|
|
|
+ const groups=splitColumns(cipher,length)
|
|
|
+ const shifts=groups.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 {shift:best,score}
|
|
|
})
|
|
|
- return { keyword: shifts.map(v => String.fromCharCode(65 + v.shift)).join(''), score: shifts.reduce((s, v) => s + v.score, 0) / length }
|
|
|
+ return {keyword:shifts.map(v=>String.fromCharCode(65+v.shift)).join(''),score:shifts.reduce((s,v)=>s+v.score,0)/length}
|
|
|
}
|
|
|
-const compareMessage = computed(() => {
|
|
|
- const original = extractLetters(encryptForm.plainText)
|
|
|
+const compareMessage = computed(()=> {
|
|
|
+ const original=extractLetters(encryptForm.plainText)
|
|
|
if (!original) return '已输出解密结果;未提供原明文,无法比较。'
|
|
|
return original === decryptedResult.value ? '解密结果与标准化后的原始明文一致。' : '解密结果与标准化后的原始明文不一致,请核对密钥和密文。'
|
|
|
})
|
|
|
-const compareType = computed(() => !extractLetters(encryptForm.plainText) ? 'info' : extractLetters(encryptForm.plainText) === decryptedResult.value ? 'success' : 'warning')
|
|
|
-watch(() => [encryptForm.plainText, encryptForm.cipherText, encryptForm.keyword], () => { decryptedResult.value = null })
|
|
|
-watch(() => crackForm.cipherText, () => { lengthOptions.value = []; crackResults.value = [] })
|
|
|
-watch(() => crackForm.targetLength, () => { crackResults.value = [] })
|
|
|
-function handleEncrypt() {
|
|
|
- if (!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
|
|
|
- if (!extractLetters(encryptForm.plainText)) return ElMessage.warning('明文至少包含一个英文字母')
|
|
|
- encryptForm.cipherText = encrypt(encryptForm.plainText, encryptForm.keyword)
|
|
|
+const compareType = computed(()=> !extractLetters(encryptForm.plainText) ? 'info' : extractLetters(encryptForm.plainText)===decryptedResult.value ? 'success' : 'warning')
|
|
|
+watch(()=>[encryptForm.plainText,encryptForm.cipherText,encryptForm.keyword],()=>{decryptedResult.value=null})
|
|
|
+watch(()=>crackForm.cipherText,()=>{lengthOptions.value=[];crackResults.value=[]})
|
|
|
+watch(()=>crackForm.targetLength,()=>{crackResults.value=[]})
|
|
|
+function handleEncrypt(){
|
|
|
+ if(!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
|
|
|
+ if(!extractLetters(encryptForm.plainText)) return ElMessage.warning('明文至少包含一个英文字母')
|
|
|
+ encryptForm.cipherText=encrypt(encryptForm.plainText,encryptForm.keyword)
|
|
|
ElMessage.success('加密完成')
|
|
|
}
|
|
|
-function handleDecrypt() {
|
|
|
- if (!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
|
|
|
- if (!extractLetters(encryptForm.cipherText)) return ElMessage.warning('密文至少包含一个英文字母')
|
|
|
- decryptedResult.value = decrypt(encryptForm.cipherText, encryptForm.keyword)
|
|
|
+function handleDecrypt(){
|
|
|
+ if(!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
|
|
|
+ if(!extractLetters(encryptForm.cipherText)) return ElMessage.warning('密文至少包含一个英文字母')
|
|
|
+ decryptedResult.value=decrypt(encryptForm.cipherText,encryptForm.keyword)
|
|
|
ElMessage.success('解密完成,请查看比较结果')
|
|
|
}
|
|
|
-function clearEncrypt() { Object.assign(encryptForm, { plainText: '', cipherText: '', keyword: '' }); decryptedResult.value = null }
|
|
|
-function handleGuessLength() {
|
|
|
- const cipher = extractLetters(crackForm.cipherText)
|
|
|
- if (cipher.length < 20) return ElMessage.warning('至少需要 20 个英文字母,建议 100 个以上')
|
|
|
- lengthOptions.value = guessLengths(cipher)
|
|
|
- if (lengthOptions.value.length) crackForm.targetLength = lengthOptions.value[0].length
|
|
|
+function clearEncrypt(){Object.assign(encryptForm,{plainText:'',cipherText:'',keyword:''});decryptedResult.value=null}
|
|
|
+function handleGuessLength(){
|
|
|
+ const cipher=extractLetters(crackForm.cipherText)
|
|
|
+ if(cipher.length<20) return ElMessage.warning('至少需要 20 个英文字母,建议 100 个以上')
|
|
|
+ lengthOptions.value=guessLengths(cipher)
|
|
|
+ if(lengthOptions.value.length) crackForm.targetLength=lengthOptions.value[0].length
|
|
|
ElMessage.info('已生成统计候选长度,建议结合明文可读性判断')
|
|
|
}
|
|
|
-function selectLength(length) { crackForm.targetLength = length }
|
|
|
-function handleAutoCrack() {
|
|
|
- const cipher = extractLetters(crackForm.cipherText)
|
|
|
- const length = Number(crackForm.targetLength)
|
|
|
- if (cipher.length < 20) return ElMessage.warning('破解至少需要 20 个英文字母')
|
|
|
- if (!Number.isInteger(length) || length < 1 || length > 20 || length > cipher.length) return ElMessage.warning('密钥长度必须为 1–20 的整数,且不能大于密文字母数')
|
|
|
- const lengths = [length, ...lengthOptions.value.map(o => o.length).filter(n => n !== length)].slice(0, 5)
|
|
|
- crackResults.value = lengths.map(l => {
|
|
|
- const candidate = crackKeyword(cipher, l)
|
|
|
- return { length: l, keyword: candidate.keyword, score: candidate.score, plainText: decrypt(cipher, candidate.keyword) }
|
|
|
+function selectLength(length){crackForm.targetLength=length}
|
|
|
+function handleAutoCrack(){
|
|
|
+ const cipher=extractLetters(crackForm.cipherText)
|
|
|
+ const length=Number(crackForm.targetLength)
|
|
|
+ if(cipher.length<20) return ElMessage.warning('破解至少需要 20 个英文字母')
|
|
|
+ if(!Number.isInteger(length)||length<1||length>20||length>cipher.length) return ElMessage.warning('密钥长度必须为 1–20 的整数,且不能大于密文字母数')
|
|
|
+ const lengths=[length,...lengthOptions.value.map(o=>o.length).filter(n=>n!==length)].slice(0,5)
|
|
|
+ crackResults.value=lengths.map(l=>{
|
|
|
+ const candidate=crackKeyword(cipher,l)
|
|
|
+ return {length:l,keyword:candidate.keyword,score:candidate.score,plainText:decrypt(cipher,candidate.keyword)}
|
|
|
})
|
|
|
ElMessage.info('生成的是统计候选结果,正确性需人工判断')
|
|
|
}
|
|
|
-function useCandidate(row) {
|
|
|
- Object.assign(encryptForm, { plainText: row.plainText, cipherText: extractLetters(crackForm.cipherText), keyword: row.keyword })
|
|
|
- mode.value = 'encrypt'
|
|
|
- decryptedResult.value = null
|
|
|
+function useCandidate(row){
|
|
|
+ Object.assign(encryptForm,{plainText:row.plainText,cipherText:extractLetters(crackForm.cipherText),keyword:row.keyword})
|
|
|
+ mode.value='encrypt'
|
|
|
+ decryptedResult.value=null
|
|
|
ElMessage.info('已回填候选,可继续修改或校验')
|
|
|
}
|
|
|
-function clearCrack() { crackForm.cipherText = ''; crackForm.targetLength = 3; lengthOptions.value = []; crackResults.value = [] }
|
|
|
-const diagramCode = `flowchart TD
|
|
|
+function clearCrack(){crackForm.cipherText='';crackForm.targetLength=3;lengthOptions.value=[];crackResults.value=[]}
|
|
|
+const diagramCode=`flowchart TD
|
|
|
A[输入明文和密钥] --> B[仅保留英文字母并大写]
|
|
|
B --> C[密钥循环对齐明文]
|
|
|
C --> D[逐字母计算 P 加 K 模 26]
|
|
|
D --> E[输出大写密文]
|
|
|
E --> F[解密时逐字母计算 C 减 K 模 26]`
|
|
|
-let chartRendered = false
|
|
|
-async function renderMermaid() {
|
|
|
- if (chartRendered || !mermaidContainer.value) return
|
|
|
+let chartRendered=false
|
|
|
+async function renderMermaid(){
|
|
|
+ if(chartRendered || !mermaidContainer.value) return
|
|
|
try {
|
|
|
- mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
|
|
|
+ mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:'default'})
|
|
|
await nextTick()
|
|
|
- const { svg } = await mermaid.render('vigenere-flow-' + Math.random().toString(36).slice(2), diagramCode)
|
|
|
- if (mermaidContainer.value) { mermaidContainer.value.innerHTML = svg; chartRendered = true }
|
|
|
- } catch (error) { console.error('Mermaid render error', error); if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图暂时无法显示,请参阅上方文字步骤。' }
|
|
|
+ const {svg}=await mermaid.render('vigenere-flow-'+Math.random().toString(36).slice(2),diagramCode)
|
|
|
+ if(mermaidContainer.value){mermaidContainer.value.innerHTML=svg;chartRendered=true}
|
|
|
+ } catch(error){console.error('Mermaid render error',error);if(mermaidContainer.value) mermaidContainer.value.textContent='流程图暂时无法显示,请参阅上方文字步骤。'}
|
|
|
}
|
|
|
-async function onTabChange(tab) { if (tab === 'principle') { await nextTick(); await renderMermaid() } }
|
|
|
+async function onTabChange(tab){if(tab==='principle'){await nextTick();await renderMermaid()}}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -370,6 +363,13 @@ async function onTabChange(tab) { if (tab === 'principle') { await nextTick(); a
|
|
|
.box-card{border-radius:8px}
|
|
|
.principle-content,.code-content{line-height:1.8;color:var(--el-text-color-regular)}
|
|
|
.principle-content h3,.code-content h3{color:var(--el-text-color-primary)}
|
|
|
+.principle-content h3,.code-content h3{margin:24px 0 12px}
|
|
|
+.principle-content h3:first-child,.code-content h3:first-child{margin-top:0}
|
|
|
+.principle-content h4{margin:20px 0 8px;color:var(--el-text-color-primary)}
|
|
|
+.principle-content p,.code-content p{margin:10px 0}
|
|
|
+.principle-content pre,.code-content pre{margin:12px 0 16px}
|
|
|
+.code-content ol{padding-left:24px;margin:12px 0}
|
|
|
+.code-content li{margin:6px 0}
|
|
|
.flowchart-placeholder{margin-top:20px;padding:20px;background:var(--el-fill-color-light);border:1px dashed var(--el-border-color);border-radius:6px;overflow-x:auto}
|
|
|
pre{background:#282c34;color:#e5e7eb;padding:16px;border-radius:6px;overflow-x:auto;line-height:1.6;font-size:13px}
|
|
|
.calc-form{max-width:900px;margin:18px auto 0}
|
|
|
@@ -382,4 +382,4 @@ pre{background:#282c34;color:#e5e7eb;padding:16px;border-radius:6px;overflow-x:a
|
|
|
.form-tips{font-size:12px;color:var(--el-text-color-secondary);width:100%}
|
|
|
.crack-results{margin-top:25px;padding:15px;border:1px solid var(--el-border-color);border-radius:6px}
|
|
|
@media(max-width:640px){.vigenere-container{padding:8px}.calc-form :deep(.el-form-item__label){width:100%!important;text-align:left}.calc-form :deep(.el-form-item__content){margin-left:0!important;width:100%}}
|
|
|
-</style>
|
|
|
+</style>
|