VigenereView.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <template>
  2. <div class="vigenere-container">
  3. <el-card class="box-card" shadow="hover">
  4. <template #header><strong>维吉尼亚密码加解密与破解模拟</strong></template>
  5. <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
  6. <el-tab-pane label="原理说明" name="principle">
  7. <div class="principle-content">
  8. <h3>维吉尼亚密码工作原理</h3>
  9. <p>维吉尼亚密码(Vigenère Cipher)是一种多表代换密码。凯撒密码对所有字母使用同一个位移,维吉尼亚密码则由密钥词决定每个位置的位移,并循环使用密钥。因此,同一个明文字母出现在不同位置时,可能变成不同的密文字母;密钥只有一个字母时,它就退化为凯撒密码。</p>
  10. <h4>1. 字母编码与循环密钥</h4>
  11. <p>先将 A~Z 编码为 0~25。设明文第 i 个字母的数值为 Pᵢ,密钥长度为 L,密钥第 j 个字母的数值为 Kⱼ;下标从 0 开始。明文第 i 个位置使用 K[i mod L],即不断重复密钥,直到覆盖整个明文。</p>
  12. <pre><code>字母:A B C D E … Z
  13. 数值:0 1 2 3 4 … 25
  14. 加密:Cᵢ = (Pᵢ + K[i mod L]) mod 26
  15. 解密:Pᵢ = (Cᵢ - K[i mod L] + 26) mod 26</code></pre>
  16. <p>模 26 表示超过 Z 后从 A 重新开始。例如 Z + B 对应 (25 + 1) mod 26 = 0,结果为 A。解密时加 26 是为了避免相减为负数;在 JavaScript 中,负数的 % 运算结果仍可能为负。</p>
  17. <h4>2. 完整加解密示例</h4>
  18. <p>明文为 ATTACK AT DAWN,密钥为 LEMON。移除空格后,明文有 12 个字母,密钥循环对齐如下:</p>
  19. <pre><code>位置: 0 1 2 3 4 5 6 7 8 9 10 11
  20. 明文: A T T A C K A T D A W N
  21. 密钥: L E M O N L E M O N L E
  22. 位移: 11 4 12 14 13 11 4 12 14 13 11 4
  23. 密文: L X F O P V E F R N H R</code></pre>
  24. <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>
  25. <h4>3. 本页面的输入规则</h4>
  26. <p>本页面仅对 A–Z 字母进行运算,并统一转换为大写;空格、标点、数字、中文和其他字符均被过滤。密钥只允许英文字母。结果不能还原被过滤的字符。</p>
  27. <p>界面要求密钥为 1~20 个英文字母,且明文或密文至少包含一个英文字母。过滤发生在密钥对齐之前,例如 A B 与 AB 使用完全相同的密钥位置。这里使用的是重复密钥版本,不是自动密钥密码。</p>
  28. <h3>统计破解原理</h3>
  29. <h4>1. 为什么可以拆成多个凯撒密码</h4>
  30. <p>若密钥长度为 L,则下标为 j、j+L、j+2L……的字母都使用同一个密钥字母。因此,将密文按“下标 mod L”分成 L 组后,每组都只是一个固定移位的凯撒密码。例如 LEMON 长度为 5,第 0、5、10 位使用 L,第 1、6、11 位使用 E。破解可以分为“推测长度”和“逐组推测位移”两步。</p>
  31. <h4>2. 用重合指数(IC)推测密钥长度</h4>
  32. <pre><code>IC = Σ fⱼ(fⱼ - 1) / [N(N - 1)],j = 0…25
  33. N:组内字母总数;fⱼ:第 j 个字母在该组中的次数
  34. 平均 IC = 各组 IC 之和 / 分组数 L</code></pre>
  35. <p>IC 可以理解为从同一组中不放回地抽取两个字母,两者相同的概率。均匀随机字母的参考值约为 1/26 ≈ 0.0385,普通英语文本约为 0.065。固定的凯撒移位只改变字母名称,不改变次数分布,所以正确分组往往能保留英语文本的 IC 特征。</p>
  36. <p>本页面尝试 1~min(20, ⌊密文字母数/5⌋) 的长度,按平均 IC 与 0.065 的距离从小到大保留前 5 个候选;距离相同则优先较短长度。这只是控制候选范围的启发式规则,并不意味着每组 5 个字母就足够可靠。正确长度的倍数也可能获得相似 IC。</p>
  37. <h4>3. 用卡方统计量恢复密钥字母</h4>
  38. <pre><code>对每组分别尝试解密位移 s = 0…25:
  39. Oⱼ = 按位移 s 解密后,第 j 个字母的实际次数
  40. Eⱼ = 该组长度 × 英语中第 j 个字母的参考概率
  41. χ²(s) = Σ (Oⱼ - Eⱼ)² / Eⱼ
  42. 取 χ² 最小的 s 作为该组候选位移;0→A,1→B,…,25→Z。</code></pre>
  43. <p>英语字母出现概率并不均匀,例如 E 的参考频率约为 12.702%,T 约为 9.056%。卡方值衡量候选解密结果与该参考分布的偏离程度,越小表示频率越接近。将各组的候选位移转回字母并按组序拼接,就得到候选密钥,再解密整段密文并检查内容是否可读。</p>
  44. <h4>4. 如何理解破解结果</h4>
  45. <p>“平均卡方”是各组最小卡方值的算术平均,不是正确概率,也不适合作为不同密钥长度之间的严格置信度。页面先展示手选长度,再附加已有的候选长度,最多生成 5 行结果;每个长度只输出逐组最优位移组成的一把密钥,不会穷举全部 26ᴸ 种密钥,也未实现 Kasiski 重复片段分析或词典评分。</p>
  46. <el-alert title="统计分析不能保证恢复正确密钥;短密文、非自然英语文本以及密钥长度的倍数都可能产生误判。" type="warning" :closable="false" show-icon />
  47. <p>建议使用 100 个以上的英语字母,并结合候选明文的可读性手工选择长度、调整密钥。维吉尼亚密码适合学习古典密码与统计分析,不适合保护现代敏感数据。</p>
  48. <div class="flowchart-placeholder"><div ref="mermaidContainer" class="mermaid"></div></div>
  49. </div>
  50. </el-tab-pane>
  51. <el-tab-pane label="算法实现" name="implementation">
  52. <div class="code-content">
  53. <h3>1. 输入标准化与校验</h3>
  54. <p>算法使用 JavaScript 字符编码进行运算,A 的编码为 65。文本先过滤再转大写;密钥必须先通过完整校验,不能把非法密钥中的字符静默删除。以下代码与本页实际计算逻辑对应,为便于阅读展开了部分表达式。</p>
  55. <pre><code>const extractLetters = text =>
  56. String(text ?? '').replace(/[^a-zA-Z]/g, '').toUpperCase()
  57. const validKey = key => /^[A-Za-z]{1,20}$/.test(key)
  58. // 界面在调用 encrypt / decrypt 前执行:
  59. if (!validKey(keyword)) throw new Error('密钥必须是 1–20 位英文字母')
  60. if (!extractLetters(text)) throw new Error('至少需要一个英文字母')</code></pre>
  61. <h3>2. 加密与解密</h3>
  62. <p>通过 i % k.length 获取循环密钥下标,将字母转换为 0~25,完成模运算后加回 65 转为字母。解密与加密的区别在于减去密钥位移。</p>
  63. <pre><code>function encrypt(text, key) {
  64. const p = extractLetters(text), k = extractLetters(key)
  65. if (!k) throw new Error('请输入合法的英文字母密钥')
  66. return Array.from(p, (ch, i) => {
  67. const plain = ch.charCodeAt(0) - 65
  68. const shift = k.charCodeAt(i % k.length) - 65
  69. return String.fromCharCode(65 + (plain + shift) % 26)
  70. }).join('')
  71. }
  72. function decrypt(text, key) {
  73. const c = extractLetters(text), k = extractLetters(key)
  74. if (!k) throw new Error('请输入合法的英文字母密钥')
  75. return Array.from(c, (ch, i) => {
  76. const cipher = ch.charCodeAt(0) - 65
  77. const shift = k.charCodeAt(i % k.length) - 65
  78. return String.fromCharCode(65 + (cipher - shift + 26) % 26)
  79. }).join('')
  80. }
  81. encrypt('ATTACK AT DAWN', 'LEMON') // LXFOPVEFRNHR
  82. decrypt('LXFOPVEFRNHR', 'LEMON') // ATTACKATDAWN</code></pre>
  83. <h3>3. 分组与重合指数</h3>
  84. <p>下列统计函数接收已经标准化的 A~Z 密文。每个字母按下标取模分配到对应组;不足两个字母的组无法抽取两个不同位置,IC 按 0 处理。</p>
  85. <pre><code>function splitColumns(cipher, length) {
  86. const groups = Array.from({ length }, () => '')
  87. for (let i = 0; i &lt; cipher.length; i++) {
  88. groups[i % length] += cipher[i]
  89. }
  90. return groups
  91. }
  92. function countLetters(text) {
  93. const counts = Array(26).fill(0)
  94. for (const ch of text) counts[ch.charCodeAt(0) - 65]++
  95. return counts
  96. }
  97. function calcIC(text) {
  98. const n = text.length
  99. if (n &lt; 2) return 0
  100. return countLetters(text).reduce((sum, f) => sum + f * (f - 1), 0)
  101. / (n * (n - 1))
  102. }</code></pre>
  103. <h3>4. 候选密钥长度排序</h3>
  104. <p>对每个长度重新分组,计算各组 IC 的算术平均,再按与英语参考值的距离排序;返回的 length 和 ic 用于显示候选按钮。</p>
  105. <pre><code>function guessLengths(cipher) {
  106. const max = Math.min(20, Math.floor(cipher.length / 5))
  107. if (max &lt; 1) return []
  108. return Array.from({ length: max }, (_, i) => {
  109. const length = i + 1
  110. const groups = splitColumns(cipher, length)
  111. const ic = groups.reduce((sum, g) => sum + calcIC(g), 0) / length
  112. return { length, ic }
  113. }).sort((a, b) =>
  114. Math.abs(a.ic - 0.065) - Math.abs(b.ic - 0.065)
  115. || a.length - b.length
  116. ).slice(0, 5)
  117. }</code></pre>
  118. <h3>5. 卡方评分与候选密钥恢复</h3>
  119. <p>englishFreq 按 A~Z 的顺序保存英语字母参考频率,单位是百分比。直接统计“按某个位移解密后”的字母次数,无需为每次尝试构造完整明文。</p>
  120. <pre><code>const englishFreq = [
  121. 8.167, 1.492, 2.782, 4.253, 12.702, 2.228, 2.015,
  122. 6.094, 6.966, 0.153, 0.772, 4.025, 2.406, 6.749,
  123. 7.507, 1.929, 0.095, 5.987, 6.327, 9.056, 2.758,
  124. 0.978, 2.360, 0.150, 1.974, 0.074
  125. ]
  126. function chiSquare(text, shift) {
  127. const n = text.length
  128. if (!n) return Infinity
  129. const counts = Array(26).fill(0)
  130. for (const ch of text) {
  131. counts[(ch.charCodeAt(0) - 65 - shift + 26) % 26]++
  132. }
  133. return counts.reduce((sum, observed, i) => {
  134. const expected = n * englishFreq[i] / 100
  135. return sum + (observed - expected) ** 2 / expected
  136. }, 0)
  137. }
  138. function crackKeyword(cipher, length) {
  139. const shifts = splitColumns(cipher, length).map(group => {
  140. let best = 0, score = Infinity
  141. for (let s = 0; s &lt; 26; s++) {
  142. const candidate = chiSquare(group, s)
  143. if (candidate &lt; score) { best = s; score = candidate }
  144. }
  145. return { shift: best, score }
  146. })
  147. return {
  148. keyword: shifts.map(v => String.fromCharCode(65 + v.shift)).join(''),
  149. score: shifts.reduce((sum, v) => sum + v.score, 0) / length
  150. }
  151. }</code></pre>
  152. <h3>6. 结果生成与验证</h3>
  153. <ol>
  154. <li>破解输入至少需要 20 个英文字母;手选长度必须是 1~20 的整数,且不能大于密文字母数。</li>
  155. <li>合并手选长度与已有的候选长度,去掉与手选长度重复的项,最多处理 5 个长度。</li>
  156. <li>对每个长度调用 crackKeyword,再用返回的密钥调用 decrypt,展示密钥、平均卡方和候选明文。</li>
  157. <li>点击“使用候选”后,将结果回填到加解密表单;解密比对针对标准化后的明文,不包含原始空格与标点,也不能据此证明破解结果真实。</li>
  158. </ol>
  159. <p>若有效字母数为 N,单次加解密时间复杂度为 O(N)。固定长度 L 的密钥恢复约需 O(26N + 26²L) 运算;页面限制长度不超过 20,适合交互式教学。统计分数仅辅助分析,最终仍需检查语言内容和密钥合理性。</p>
  160. </div>
  161. </el-tab-pane>
  162. <el-tab-pane label="加解密与破解" name="calculation">
  163. <el-radio-group v-model="mode" class="mode-group">
  164. <el-radio value="encrypt">加解密模式</el-radio>
  165. <el-radio value="crack">破解模式</el-radio>
  166. </el-radio-group>
  167. <el-form v-if="mode === 'encrypt'" :model="encryptForm" label-width="100px" class="calc-form">
  168. <el-form-item label="明文"><el-input v-model="encryptForm.plainText" type="textarea" :rows="3" placeholder="示例:ATTACK AT DAWN" /></el-form-item>
  169. <el-form-item label="密钥词"><el-input v-model="encryptForm.keyword" maxlength="20" placeholder="仅英文字母,如 LEMON" /></el-form-item>
  170. <el-form-item label="密文"><el-input v-model="encryptForm.cipherText" type="textarea" :rows="3" placeholder="加密结果,也可粘贴待解密密文" /></el-form-item>
  171. <el-form-item label-width="0" class="action-item"><div class="actions">
  172. <el-button type="primary" :icon="Lock" @click="handleEncrypt">加密计算</el-button>
  173. <el-button type="success" :icon="Unlock" @click="handleDecrypt">解密计算</el-button>
  174. <el-button :icon="Delete" @click="clearEncrypt">清空</el-button>
  175. </div></el-form-item>
  176. <el-form-item v-if="decryptedResult !== null" label="解密结果"><el-input :model-value="decryptedResult" type="textarea" :rows="3" readonly /></el-form-item>
  177. <el-form-item v-if="decryptedResult !== null"><el-alert :title="compareMessage" :type="compareType" :closable="false" show-icon /></el-form-item>
  178. </el-form>
  179. <el-form v-else :model="crackForm" label-width="120px" class="calc-form">
  180. <el-form-item label="待破解密文"><el-input v-model="crackForm.cipherText" type="textarea" :rows="4" placeholder="建议至少 100 个英语字母,文本越长越容易分析" /></el-form-item>
  181. <el-form-item label-width="0" class="action-item"><div class="actions">
  182. <el-button type="warning" :icon="Search" @click="handleGuessLength">推测密钥长度</el-button>
  183. <el-button :icon="Delete" @click="clearCrack">清空破解</el-button>
  184. </div></el-form-item>
  185. <el-form-item v-if="lengthOptions.length" label="候选密钥长度">
  186. <div class="length-options">
  187. <el-button v-for="item in lengthOptions" :key="item.length" :type="item.length === crackForm.targetLength ? 'primary' : 'default'" size="small" @click="selectLength(item.length)">
  188. {{ item.length }} (IC {{ item.ic.toFixed(4) }})
  189. </el-button>
  190. <div class="form-tips">按接近英语 IC 参考值排列;也可以手工选择其他长度。</div>
  191. </div>
  192. </el-form-item>
  193. <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>
  194. <div v-if="crackResults.length" class="crack-results">
  195. <h4>统计破解候选(并非保证正确)</h4>
  196. <el-table :data="crackResults" border stripe style="width:100%">
  197. <el-table-column prop="length" label="长度" width="74" />
  198. <el-table-column prop="keyword" label="候选密钥" width="160" />
  199. <el-table-column prop="score" label="平均卡方" width="106"><template #default="{ row }">{{ row.score.toFixed(2) }}</template></el-table-column>
  200. <el-table-column prop="plainText" label="候选明文" min-width="230" show-overflow-tooltip />
  201. <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>
  202. </el-table>
  203. </div>
  204. </el-form>
  205. </el-tab-pane>
  206. </el-tabs>
  207. </el-card>
  208. </div>
  209. </template>
  210. <script setup>
  211. import { computed, nextTick, reactive, ref, watch } from 'vue'
  212. import { ElMessage } from 'element-plus'
  213. import { Lock, Unlock, Delete, Search, MagicStick } from '@element-plus/icons-vue'
  214. import mermaid from 'mermaid'
  215. const activeTab = ref('calculation')
  216. const mode = ref('encrypt')
  217. const encryptForm = reactive({ plainText: '', cipherText: '', keyword: '' })
  218. const crackForm = reactive({ cipherText: '', targetLength: 3 })
  219. const decryptedResult = ref(null)
  220. const lengthOptions = ref([])
  221. const crackResults = ref([])
  222. const mermaidContainer = ref(null)
  223. const extractLetters = text => String(text ?? '').replace(/[^a-zA-Z]/g, '').toUpperCase()
  224. const validKey = key => /^[A-Za-z]{1,20}$/.test(key)
  225. 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]
  226. const encrypt = (text,key) => {
  227. const p = extractLetters(text), k = extractLetters(key)
  228. if (!k) throw new Error('请输入合法的英文字母密钥')
  229. return Array.from(p,(c,i)=>String.fromCharCode(65+(c.charCodeAt(0)-65+k.charCodeAt(i%k.length)-65)%26)).join('')
  230. }
  231. const decrypt = (text,key) => {
  232. const p = extractLetters(text), k = extractLetters(key)
  233. if (!k) throw new Error('请输入合法的英文字母密钥')
  234. return Array.from(p,(c,i)=>String.fromCharCode(65+(c.charCodeAt(0)-k.charCodeAt(i%k.length)+26)%26)).join('')
  235. }
  236. const countLetters = text => {
  237. const counts = Array(26).fill(0)
  238. for (const c of text) counts[c.charCodeAt(0)-65]++
  239. return counts
  240. }
  241. const calcIC = text => {
  242. const n = text.length
  243. if (n < 2) return 0
  244. return countLetters(text).reduce((sum,c)=>sum+c*(c-1),0)/(n*(n-1))
  245. }
  246. const splitColumns = (cipher,length) => {
  247. const groups = Array.from({length},()=> '')
  248. for (let i=0;i<cipher.length;i++) groups[i%length]+=cipher[i]
  249. return groups
  250. }
  251. const guessLengths = cipher => {
  252. const max = Math.min(20,Math.floor(cipher.length/5))
  253. if (max < 1) return []
  254. return Array.from({length:max},(_,i)=>{
  255. const length=i+1
  256. const groups=splitColumns(cipher,length)
  257. const ic=groups.reduce((sum,g)=>sum+calcIC(g),0)/length
  258. return {length,ic}
  259. }).sort((a,b)=>Math.abs(a.ic-0.065)-Math.abs(b.ic-0.065) || a.length-b.length).slice(0,5)
  260. }
  261. const chiSquare = (text,shift) => {
  262. const n=text.length
  263. if (!n) return Infinity
  264. const counts=Array(26).fill(0)
  265. for (const ch of text) counts[(ch.charCodeAt(0)-65-shift+26)%26]++
  266. return counts.reduce((sum,v,i)=>{
  267. const expected=n*englishFreq[i]/100
  268. return sum+(v-expected)**2/expected
  269. },0)
  270. }
  271. const crackKeyword = (cipher,length) => {
  272. const groups=splitColumns(cipher,length)
  273. const shifts=groups.map(group=>{
  274. let best=0,score=Infinity
  275. for(let s=0;s<26;s++) {
  276. const candidate=chiSquare(group,s)
  277. if(candidate<score){best=s;score=candidate}
  278. }
  279. return {shift:best,score}
  280. })
  281. return {keyword:shifts.map(v=>String.fromCharCode(65+v.shift)).join(''),score:shifts.reduce((s,v)=>s+v.score,0)/length}
  282. }
  283. const compareMessage = computed(()=> {
  284. const original=extractLetters(encryptForm.plainText)
  285. if (!original) return '已输出解密结果;未提供原明文,无法比较。'
  286. return original === decryptedResult.value ? '解密结果与标准化后的原始明文一致。' : '解密结果与标准化后的原始明文不一致,请核对密钥和密文。'
  287. })
  288. const compareType = computed(()=> !extractLetters(encryptForm.plainText) ? 'info' : extractLetters(encryptForm.plainText)===decryptedResult.value ? 'success' : 'warning')
  289. watch(()=>[encryptForm.plainText,encryptForm.cipherText,encryptForm.keyword],()=>{decryptedResult.value=null})
  290. watch(()=>crackForm.cipherText,()=>{lengthOptions.value=[];crackResults.value=[]})
  291. watch(()=>crackForm.targetLength,()=>{crackResults.value=[]})
  292. function handleEncrypt(){
  293. if(!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
  294. if(!extractLetters(encryptForm.plainText)) return ElMessage.warning('明文至少包含一个英文字母')
  295. encryptForm.cipherText=encrypt(encryptForm.plainText,encryptForm.keyword)
  296. ElMessage.success('加密完成')
  297. }
  298. function handleDecrypt(){
  299. if(!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
  300. if(!extractLetters(encryptForm.cipherText)) return ElMessage.warning('密文至少包含一个英文字母')
  301. decryptedResult.value=decrypt(encryptForm.cipherText,encryptForm.keyword)
  302. ElMessage.success('解密完成,请查看比较结果')
  303. }
  304. function clearEncrypt(){Object.assign(encryptForm,{plainText:'',cipherText:'',keyword:''});decryptedResult.value=null}
  305. function handleGuessLength(){
  306. const cipher=extractLetters(crackForm.cipherText)
  307. if(cipher.length<20) return ElMessage.warning('至少需要 20 个英文字母,建议 100 个以上')
  308. lengthOptions.value=guessLengths(cipher)
  309. if(lengthOptions.value.length) crackForm.targetLength=lengthOptions.value[0].length
  310. ElMessage.info('已生成统计候选长度,建议结合明文可读性判断')
  311. }
  312. function selectLength(length){crackForm.targetLength=length}
  313. function handleAutoCrack(){
  314. const cipher=extractLetters(crackForm.cipherText)
  315. const length=Number(crackForm.targetLength)
  316. if(cipher.length<20) return ElMessage.warning('破解至少需要 20 个英文字母')
  317. if(!Number.isInteger(length)||length<1||length>20||length>cipher.length) return ElMessage.warning('密钥长度必须为 1–20 的整数,且不能大于密文字母数')
  318. const lengths=[length,...lengthOptions.value.map(o=>o.length).filter(n=>n!==length)].slice(0,5)
  319. crackResults.value=lengths.map(l=>{
  320. const candidate=crackKeyword(cipher,l)
  321. return {length:l,keyword:candidate.keyword,score:candidate.score,plainText:decrypt(cipher,candidate.keyword)}
  322. })
  323. ElMessage.info('生成的是统计候选结果,正确性需人工判断')
  324. }
  325. function useCandidate(row){
  326. Object.assign(encryptForm,{plainText:row.plainText,cipherText:extractLetters(crackForm.cipherText),keyword:row.keyword})
  327. mode.value='encrypt'
  328. decryptedResult.value=null
  329. ElMessage.info('已回填候选,可继续修改或校验')
  330. }
  331. function clearCrack(){crackForm.cipherText='';crackForm.targetLength=3;lengthOptions.value=[];crackResults.value=[]}
  332. const diagramCode=`flowchart TD
  333. A[输入明文和密钥] --> B[仅保留英文字母并大写]
  334. B --> C[密钥循环对齐明文]
  335. C --> D[逐字母计算 P 加 K 模 26]
  336. D --> E[输出大写密文]
  337. E --> F[解密时逐字母计算 C 减 K 模 26]`
  338. let chartRendered=false
  339. async function renderMermaid(){
  340. if(chartRendered || !mermaidContainer.value) return
  341. try {
  342. mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:'default'})
  343. await nextTick()
  344. const {svg}=await mermaid.render('vigenere-flow-'+Math.random().toString(36).slice(2),diagramCode)
  345. if(mermaidContainer.value){mermaidContainer.value.innerHTML=svg;chartRendered=true}
  346. } catch(error){console.error('Mermaid render error',error);if(mermaidContainer.value) mermaidContainer.value.textContent='流程图暂时无法显示,请参阅上方文字步骤。'}
  347. }
  348. async function onTabChange(tab){if(tab==='principle'){await nextTick();await renderMermaid()}}
  349. </script>
  350. <style scoped>
  351. .vigenere-container{min-height:100vh}
  352. .box-card{border-radius:8px}
  353. .principle-content,.code-content{line-height:1.8;color:var(--el-text-color-regular)}
  354. .principle-content h3,.code-content h3{color:var(--el-text-color-primary)}
  355. .principle-content h3,.code-content h3{margin:24px 0 12px}
  356. .principle-content h3:first-child,.code-content h3:first-child{margin-top:0}
  357. .principle-content h4{margin:20px 0 8px;color:var(--el-text-color-primary)}
  358. .principle-content p,.code-content p{margin:10px 0}
  359. .principle-content pre,.code-content pre{margin:12px 0 16px}
  360. .code-content ol{padding-left:24px;margin:12px 0}
  361. .code-content li{margin:6px 0}
  362. .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}
  363. pre{background:#282c34;color:#e5e7eb;padding:16px;border-radius:6px;overflow-x:auto;line-height:1.6;font-size:13px}
  364. .calc-form{max-width:900px;margin:18px auto 0}
  365. .mode-group{margin-bottom:12px}
  366. .action-item :deep(.el-form-item__content){margin-left:0!important}
  367. .actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;width:100%}
  368. .actions :deep(.el-button){margin-left:0}
  369. .length-options{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
  370. .length-options :deep(.el-button){margin-left:0}
  371. .form-tips{font-size:12px;color:var(--el-text-color-secondary);width:100%}
  372. .crack-results{margin-top:25px;padding:15px;border:1px solid var(--el-border-color);border-radius:6px}
  373. @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%}}
  374. </style>