소스 검색

1.增加实验模块

wangkun 3 주 전
부모
커밋
2a15b0eb84

+ 263 - 0
src/view/sysApplication/experiment/asymmetricKeyAlgorithm/RsaEnc.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="rsa-container">
+    <el-card class="box-card" shadow="never">
+      <template #header><strong>RSA 算法加解密实验</strong></template>
+      <el-tabs v-model="activeTab" type="border-card">
+        <el-tab-pane label="原理说明" name="principle">
+          <section class="information">
+            <h3>RSA 公钥密码算法</h3>
+            <p>RSA 基于大整数分解问题:使用公钥加密、私钥解密。实际消息加密需要安全的编码填充,不能直接把教材中的模幂公式当成安全加密方案。</p>
+            <ol>
+              <li>选择大素数 p 和 q,得到 n = p × q;选择公钥指数 e(常用 65537)并求私钥指数 d。</li>
+              <li>核心数学变换是 c = m^e mod n、m = c^d mod n;OAEP / PKCS#1 v1.5 将消息编码为合适的整数再执行变换。</li>
+              <li>RSA 只能加密较短的数据;实际应用通常用 RSA-OAEP 加密随机 AES 密钥,再用 AES-GCM 加密业务数据(混合加密)。</li>
+            </ol>
+            <el-alert title="安全提醒" type="warning" :closable="false" show-icon>
+              本页用于教学。推荐 RSA-OAEP + 2048 位及以上密钥;PKCS#1 v1.5 加密属于兼容模式,RAW 没有安全填充,不可用于真实保密需求。私钥只在当前页面内使用,不要粘贴生产环境的真实私钥。
+            </el-alert>
+            <div ref="mermaidContainer" class="mermaid-box"></div>
+          </section>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <section class="information">
+            <h3>关键实现(node-forge)</h3>
+            <p>推荐使用 RSA-OAEP(SHA-256),消息及密文均按字节处理。</p>
+            <pre><code>{{ exampleCode }}</code></pre>
+            <p>RSA-OAEP 最大明文字节数 = 密钥字节数 − 2 × 哈希摘要字节数 − 2。2048 位 RSA 配合 SHA-256 时最多 190 字节。</p>
+          </section>
+        </el-tab-pane>
+        <el-tab-pane label="加密计算与解密验证" name="calculation">
+          <el-form :model="form" label-width="112px" class="calc-form">
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 字符串</el-radio>
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="3" placeholder="输入待加密的消息;支持空消息" />
+            </el-form-item>
+            <el-form-item label="密钥长度">
+              <el-select v-model="form.keyLength" class="full-width">
+                <el-option label="2048 位(推荐)" value="2048" />
+                <el-option label="3072 位" value="3072" />
+                <el-option label="1024 位(仅兼容教学,不安全)" value="1024" />
+              </el-select>
+              <div class="form-tips">密钥长度仅控制新生成的密钥;加解密限制以实际导入的 PEM 密钥为准。</div>
+            </el-form-item>
+            <el-form-item label="公钥 (PEM)">
+              <el-input v-model="form.publicKey" type="textarea" :rows="5" placeholder="-----BEGIN PUBLIC KEY-----" />
+            </el-form-item>
+            <el-form-item label="私钥 (PEM)">
+              <el-input v-model="form.privateKey" type="textarea" :rows="6" placeholder="-----BEGIN RSA PRIVATE KEY-----" show-word-limit />
+              <div class="form-tips">仅供本地实验。不要将真实私钥填写到不可信网页。</div>
+            </el-form-item>
+            <el-form-item label-width="0" class="action-form-item">
+              <el-button :icon="Refresh" :loading="generating" @click="generateKeyPair">{{ generating ? '正在生成…' : '生成密钥对' }}</el-button>
+            </el-form-item>
+            <el-form-item label="填充方式">
+              <el-select v-model="form.padding" class="full-width">
+                <el-option label="RSA-OAEP / SHA-256(推荐)" value="oaep" />
+                <el-option label="PKCS#1 v1.5(兼容教学)" value="pkcs1" />
+                <el-option label="RAW / 无填充(仅数学实验)" value="raw" />
+              </el-select>
+              <div v-if="form.padding === 'raw'" class="form-tips warning">RAW 使用完整模长分组,解密时保留前导零;无法从数据本身恢复原始长度。禁止用于实际保密。</div>
+            </el-form-item>
+            <el-form-item label="密文编码">
+              <el-radio-group v-model="form.outputFormat">
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="密文值">
+              <el-input v-model="form.ciphertext" type="textarea" :rows="4" placeholder="加密输出,也可以粘贴待解密的密文" />
+            </el-form-item>
+            <el-form-item label="解密结果">
+              <el-input :model-value="decryptedResult" type="textarea" :rows="3" readonly placeholder="解密结果将在这里单独显示" />
+              <div v-if="verificationMessage" class="form-tips">{{ verificationMessage }}</div>
+            </el-form-item>
+            <el-form-item label-width="0" class="action-form-item">
+              <div class="action-row">
+                <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="handleClear">清空数据</el-button>
+              </div>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Refresh, Lock, Unlock, Delete } from '@element-plus/icons-vue'
+import forge from 'node-forge'
+import mermaid from 'mermaid'
+
+const activeTab = ref('calculation')
+const generating = ref(false)
+const decryptedResult = ref('')
+const verificationMessage = ref('')
+const mermaidContainer = ref(null)
+const form = reactive({
+  inputFormat: 'string', message: '', keyLength: '2048',
+  publicKey: '', privateKey: '', padding: 'oaep', outputFormat: 'hex', ciphertext: ''
+})
+
+const exampleCode = `import forge from 'node-forge';
+const pair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
+const bytes = forge.util.encodeUtf8('你好 RSA');
+const options = { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha256.create() } };
+const ciphertext = pair.publicKey.encrypt(bytes, 'RSA-OAEP', options);
+const plaintextBytes = pair.privateKey.decrypt(ciphertext, 'RSA-OAEP',
+  { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha256.create() } });
+console.log(forge.util.decodeUtf8(plaintextBytes));`
+
+const diagramCode = `flowchart LR
+  A[生成 RSA 公私钥] --> B[输入短消息]
+  B --> C[OAEP / PKCS1 编码]
+  C --> D[公钥加密]
+  D --> E[密文]
+  E --> F[私钥解密]
+  F --> G[恢复消息]`
+let mermaidId = 0
+async function renderDiagram() {
+  await nextTick()
+  if (activeTab.value !== 'principle' || !mermaidContainer.value) return
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    const { svg } = await mermaid.render(`rsa-flow-${++mermaidId}`, diagramCode)
+    if (mermaidContainer.value) mermaidContainer.value.innerHTML = svg
+  } catch (error) {
+    if (mermaidContainer.value) mermaidContainer.value.textContent = `流程图渲染失败:${error.message}`
+  }
+}
+watch(activeTab, value => { if (value === 'principle') renderDiagram() })
+
+// 输入输出统一采用 forge 的二进制字节字符串,长度即真实字节数。
+function parseEncoded(value, format, field) {
+  if (format === 'string') return forge.util.encodeUtf8(value)
+  if (format === 'hex') {
+    if (value.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(value)) throw new Error(`${field}必须是偶数位合法十六进制字符`)
+    return forge.util.hexToBytes(value)
+  }
+  const data = value.replace(/\s/g, '')
+  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data)) {
+    throw new Error(`${field}不是合法的 Base64`)
+  }
+  return forge.util.decode64(data)
+}
+function encodeBytes(bytes, format) {
+  if (format === 'hex') return forge.util.bytesToHex(bytes)
+  if (format === 'base64') return forge.util.encode64(bytes)
+  return forge.util.decodeUtf8(bytes)
+}
+function parsePublicKey() {
+  if (!form.publicKey.trim()) throw new Error('请先输入或生成 RSA 公钥')
+  return forge.pki.publicKeyFromPem(form.publicKey.trim())
+}
+function parsePrivateKey() {
+  if (!form.privateKey.trim()) throw new Error('请先输入或生成 RSA 私钥')
+  return forge.pki.privateKeyFromPem(form.privateKey.trim())
+}
+function modulusBytes(key) { return Math.ceil(key.n.bitLength() / 8) }
+function paddingOptions() {
+  return { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha256.create() } }
+}
+function paddingName() {
+  return form.padding === 'oaep' ? 'RSA-OAEP' : form.padding === 'pkcs1' ? 'RSAES-PKCS1-V1_5' : 'RAW'
+}
+function maxMessageLength(k) {
+  return form.padding === 'oaep' ? k - 66 : form.padding === 'pkcs1' ? k - 11 : k
+}
+function clearDecrypted() { decryptedResult.value = ''; verificationMessage.value = '' }
+watch(() => [form.inputFormat, form.message, form.publicKey, form.privateKey, form.padding, form.outputFormat, form.ciphertext], clearDecrypted)
+
+function generateKeyPair() {
+  if (generating.value) return
+  generating.value = true
+  const bits = Number(form.keyLength)
+  // callback 形式避免同步生成时长时间阻塞主线程(具体行为依赖 node-forge 版本与运行环境)。
+  try {
+    forge.pki.rsa.generateKeyPair({ bits, e: 0x10001, workers: -1 }, (error, pair) => {
+      generating.value = false
+      if (error) { ElMessage.error(`生成密钥失败:${error.message}`); return }
+      form.publicKey = forge.pki.publicKeyToPem(pair.publicKey)
+      form.privateKey = forge.pki.privateKeyToPem(pair.privateKey)
+      form.ciphertext = ''
+      clearDecrypted()
+      ElMessage.success(`已生成 RSA ${bits} 位密钥对`)
+    })
+  } catch (error) {
+    generating.value = false
+    ElMessage.error(`生成密钥失败:${error.message}`)
+  }
+}
+function handleEncrypt() {
+  try {
+    const key = parsePublicKey()
+    const k = modulusBytes(key)
+    const bytes = parseEncoded(form.message, form.inputFormat, '明文')
+    const max = maxMessageLength(k)
+    if (max < 0 || bytes.length > max) throw new Error(`当前填充方式最多加密 ${max} 字节,实际为 ${bytes.length} 字节。请缩短明文或改用混合加密。`)
+    // RAW 是字节整数;编码后的消息必须严格小于模数 n。
+    if (form.padding === 'raw' && new forge.jsbn.BigInteger(forge.util.bytesToHex(bytes) || '0', 16).compareTo(key.n) >= 0) {
+      throw new Error('RAW 模式要求明文代表的整数严格小于 RSA 模数 n')
+    }
+    const encrypted = key.encrypt(bytes, paddingName(), form.padding === 'oaep' ? paddingOptions() : undefined)
+    form.ciphertext = encodeBytes(encrypted, form.outputFormat)
+    clearDecrypted()
+    ElMessage.success('加密成功')
+  } catch (error) { ElMessage.error(`加密失败:${error.message}`) }
+}
+function handleDecrypt() {
+  try {
+    const key = parsePrivateKey()
+    const k = modulusBytes(key)
+    const cipher = parseEncoded(form.ciphertext, form.outputFormat, '密文')
+    if (cipher.length !== k) throw new Error(`密文长度必须为 ${k} 字节,当前为 ${cipher.length} 字节`)
+    const bytes = key.decrypt(cipher, paddingName(), form.padding === 'oaep' ? paddingOptions() : undefined)
+    let readable = bytes
+    if (form.padding === 'raw') {
+      // RAW 解密给出整个模长的数值,保留全部前导零字节(不会擅自猜测长度)。
+      decryptedResult.value = forge.util.bytesToHex(readable.padStart(k, '\x00'))
+      verificationMessage.value = 'RAW 结果以完整模长 Hex 显示:前导零是整数编码的一部分,原始消息长度不可从 RAW 密文唯一确定。'
+    } else {
+      decryptedResult.value = encodeBytes(readable, form.inputFormat)
+      try {
+        const original = parseEncoded(form.message, form.inputFormat, '明文')
+        if (form.message !== '') {
+          verificationMessage.value = original === bytes ? '解密结果与当前原文一致(仅本地比对,不代表密文认证)' : '解密完成,但与当前原文不一致(可能是外部输入的密文)'
+        } else verificationMessage.value = '解密完成;未提供非空原文进行比对。RSA 加密本身不提供发送方身份认证。'
+      } catch { verificationMessage.value = '解密完成;当前原文无法解析,未进行一致性比较。' }
+    }
+    ElMessage.success('解密计算完成')
+  } catch (error) { clearDecrypted(); ElMessage.error(`解密失败:${error.message}`) }
+}
+function handleClear() {
+  form.message = ''; form.publicKey = ''; form.privateKey = ''; form.ciphertext = ''
+  clearDecrypted()
+  ElMessage.info('已清空消息和密钥')
+}
+</script>
+
+<style scoped>
+.rsa-container {  background: #f0f2f5; }
+.box-card {  border-radius: 8px; }
+.information { line-height: 1.8; color: #606266; }
+.information h3 { color: #303133; }
+.calc-form { max-width: 880px; margin: 20px auto 0; }
+.full-width { width: 100%; }
+.form-tips { color: #909399; font-size: 12px; line-height: 1.6; margin-top: 5px; width: 100%; }
+.warning { color: #b88230; }
+.action-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
+.action-row .el-button { margin-left: 0; }
+.action-form-item :deep(.el-form-item__content) { margin-left: 0 !important; }
+.mermaid-box { margin-top: 18px; padding: 16px; background: #fafafa; border: 1px solid #dcdfe6; border-radius: 6px; overflow-x: auto; }
+pre { background: #282c34; color: #d8dee9; padding: 16px; border-radius: 6px; overflow-x: auto; line-height: 1.6; }
+@media (max-width: 650px) { .rsa-container { padding: 8px; } .calc-form :deep(.el-form-item) { display: block; } .calc-form :deep(.el-form-item__label) { justify-content: flex-start; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
+</style>

+ 389 - 0
src/view/sysApplication/experiment/asymmetricKeyAlgorithm/RsaSign.vue

@@ -0,0 +1,389 @@
+<template>
+  <div class="rsa-sign-container">
+    <el-card class="box-card" shadow="never">
+      <!-- Vue 3 推荐使用 template #header 替代 div slot="header" -->
+      <template #header>
+        <div class="clearfix">
+          <span style="font-weight: bold; font-size: 16px;">RSA 算法签名验签</span>
+        </div>
+      </template>
+
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- ==================== 1. 原理说明 ==================== -->
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>RSA 数字签名验签算法工作原理</h3>
+            <p>
+              RSA 数字签名通过<strong>私钥签名</strong>与<strong>公钥验签</strong>检测消息是否被更改,并验证签名与公钥的对应关系。实际身份可信度还需要可信的公钥分发或证书体系;签名本身不提供保密性。
+            </p>
+
+            <h4>签名运算步骤拆解:</h4>
+            <ol>
+              <li>
+                <strong>选择压缩函数(哈希算法):</strong>
+                优先选择 SHA256 / SHA384 / SHA512;SHA1 仅用于旧系统兼容教学。
+              </li>
+              <li>
+                <strong>计算消息摘要:</strong>
+                使用选定的哈希算法对消息原文 $M$ 进行哈希运算,得到固定长度的消息摘要 $H(M)$。
+              </li>
+              <li>
+                <strong>私钥签名:</strong>
+                本页面采用 RSASSA-PKCS1-v1_5:先按照所选哈希算法计算摘要,再进行 DigestInfo 编码与 PKCS#1 v1.5 填充,最后使用私钥执行 RSA 签名运算。
+              </li>
+            </ol>
+
+            <h4>验签运算步骤拆解:</h4>
+            <ol>
+              <li>
+                <strong>计算消息摘要:</strong>
+                使用相同的哈希算法对收到的消息原文 $M'$ 计算摘要 $H(M')$。
+              </li>
+              <li>
+                <strong>公钥解密签名:</strong>
+                公钥验签会验证 RSA 签名的 PKCS#1 v1.5 编码及消息摘要是否与收到的消息一致。
+              </li>
+              <li>
+                <strong>比对验证:</strong>
+                验签通过表示:给定公钥、消息、算法与签名彼此匹配;它不意味着已经核实公钥持有人的真实身份。
+              </li>
+            </ol>
+
+            <el-alert
+                title="安全提示"
+                type="warning"
+                :closable="false"
+                show-icon
+                style="margin-top: 15px;"
+            >
+              <template #title>
+                SHA-1 不建议用于新签名;1024 位 RSA 也不适合新部署。教学保留 SHA-1 / 1024 位选项,推荐 RSA-2048 或以上与 SHA-256。
+              </template>
+            </el-alert>
+
+            <div class="flowchart-placeholder">
+
+              <div ref="mermaidContainer" class="mermaid">{{ diagramCode }}</div>
+            </div>
+          </div>
+        </el-tab-pane>
+
+        <!-- ==================== 2. 算法实现 ==================== -->
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现 (基于 node-forge)</h3>
+            <p>
+              以下为前端核心计算逻辑代码,引入 <code>node-forge</code> 库实现 RSA 签名验签(支持 SHA1/SHA256/SHA384/SHA512 压缩函数):
+            </p>
+            <pre><code>
+// 引入 node-forge 库
+import forge from 'node-forge';
+
+// 1. 生成 RSA 密钥对
+const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
+const publicKeyPem  = forge.pki.publicKeyToPem(keypair.publicKey);
+const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey);
+
+// 2. RSA 数字签名
+const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
+const md = forge.md.sha256.create();          // 压缩函数:SHA256
+md.update(messageBytes, 'binary');            // 更新消息摘要
+const signature = privateKey.sign(md);        // 私钥签名
+
+// 3. RSA 签名验证
+const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
+const mdVerify = forge.md.sha256.create();
+mdVerify.update(messageBytes, 'binary');
+const isValid = publicKey.verify(
+  mdVerify.digest().bytes(),                  // 消息摘要
+  signature                                   // 签名值
+);
+console.log('验签结果:', isValid);            // true / false
+            </code></pre>
+          </div>
+        </el-tab-pane>
+
+        <!-- ==================== 3. 签名计算与验证 ==================== -->
+        <el-tab-pane label="签名计算与验证" name="calculation">
+          <el-form :model="form" label-width="120px" class="calc-form">
+
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">字符串</el-radio>
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="消息原文" required>
+              <el-input
+                  type="textarea"
+                  v-model="form.message"
+                  :rows="3"
+                  placeholder="请输入待签名的消息原文"
+              ></el-input>
+            </el-form-item>
+
+            <el-form-item label="密钥长度">
+              <el-select v-model="form.keyLength" placeholder="请选择密钥长度" style="width: 100%;">
+                <el-option label="1024 位" value="1024"></el-option>
+                <el-option label="2048 位(推荐)" value="2048"></el-option>
+                <el-option label="3072 位" value="3072"></el-option>
+              </el-select>
+            </el-form-item>
+
+            <el-form-item label="公钥值 (PEM)" required>
+              <el-input
+                  type="textarea"
+                  v-model="form.publicKey"
+                  :rows="4"
+                  placeholder="请输入或生成公钥 (PEM格式)"
+              ></el-input>
+            </el-form-item>
+
+            <el-form-item label="私钥值 (PEM)" required>
+              <el-input type="textarea" v-model="form.privateKey" :rows="5" placeholder="请输入或生成私钥 (PEM格式)" />
+              <div class="field-actions">
+                <el-button :icon="Refresh" :loading="generating" @click="generateKeyPair">密钥对生成</el-button>
+                <span class="hint">仅限实验用密钥;真实私钥不要粘贴到不可信的网页。</span>
+              </div>
+            </el-form-item>
+
+            <el-form-item label="压缩函数">
+              <el-select v-model="form.digestAlgorithm" placeholder="请选择压缩函数" style="width: 100%;">
+                <el-option label="SHA1withRSA"   value="sha1"></el-option>
+                <el-option label="SHA256withRSA"  value="sha256"></el-option>
+                <el-option label="SHA384withRSA"  value="sha384"></el-option>
+                <el-option label="SHA512withRSA"  value="sha512"></el-option>
+              </el-select>
+            </el-form-item>
+
+            <el-form-item label="输出格式">
+              <el-radio-group v-model="form.outputFormat">
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="签名值 / 待验签">
+              <el-input
+                  type="textarea"
+                  v-model="form.signature"
+                  :rows="3"
+                  placeholder="签名结果 / 待验签输入的签名值"
+              ></el-input>
+            </el-form-item>
+
+            <el-form-item label-width="0">
+              <div class="button-row">
+                <el-button type="primary" :icon="Edit" @click="handleSign">数字签名</el-button>
+                <el-button type="success" :icon="CircleCheck" @click="handleVerify">签名验证</el-button>
+                <el-button :icon="Delete" @click="handleClear">清空数据</el-button>
+              </div>
+            </el-form-item>
+            <el-alert v-if="verifyStatus" :title="verifyStatus" :type="verifyPassed ? 'success' : 'error'" show-icon :closable="false" />
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Refresh, Edit, CircleCheck, Delete } from '@element-plus/icons-vue'
+import forge from 'node-forge'
+import mermaid from 'mermaid'
+
+const activeTab = ref('calculation')
+const mermaidContainer = ref(null)
+const generating = ref(false)
+const verifyStatus = ref('')
+const verifyPassed = ref(false)
+const form = reactive({
+  inputFormat: 'string', message: '', keyLength: '2048', publicKey: '',
+  privateKey: '', digestAlgorithm: 'sha256', outputFormat: 'hex', signature: ''
+})
+const diagramCode = `flowchart TD
+  A[准备消息和 RSA 密钥对] --> B[将消息按指定格式解码为字节]
+  B --> C[计算 SHA 摘要]
+  C --> D[PKCS1 v1.5 编码与私钥签名]
+  D --> E[签名编码为 Hex 或 Base64]
+  E --> F[消息、签名和公钥发送给验证方]
+  F --> G[解码消息及签名]
+  G --> H[使用公钥验证签名编码和摘要]
+  H --> I{验证通过?}
+  I -->|是| J[签名与所给公钥和消息匹配]
+  I -->|否| K[签名无效或参数不匹配]`
+let chartId = 0
+const renderMermaid = async () => {
+  await nextTick()
+  if (!mermaidContainer.value || activeTab.value !== 'principle') return
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    const { svg } = await mermaid.render(`rsa-sign-diagram-${++chartId}`, diagramCode)
+    if (mermaidContainer.value && activeTab.value === 'principle') mermaidContainer.value.innerHTML = svg
+  } catch (error) {
+    console.error('流程图渲染失败:', error)
+    if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图渲染失败'
+  }
+}
+watch(activeTab, (tab) => { if (tab === 'principle') renderMermaid() })
+const clearVerification = () => { verifyStatus.value = ''; verifyPassed.value = false }
+watch(() => [form.message, form.inputFormat, form.publicKey, form.signature,
+  form.outputFormat, form.digestAlgorithm], clearVerification)
+
+const validHex = (value) => /^(?:[0-9a-fA-F]{2})+$/.test(value)
+const validBase64 = (value) => /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)
+const decodeBytes = (value, format, name) => {
+  if (format === 'hex') {
+    if (!validHex(value)) throw new Error(`${name}必须是非空、偶数位合法 Hex`)
+    return forge.util.hexToBytes(value)
+  }
+  if (format === 'base64') {
+    if (!validBase64(value)) throw new Error(`${name}不是合法 Base64`)
+    return forge.util.decode64(value)
+  }
+  return forge.util.encodeUtf8(value)
+}
+const createDigest = (algorithm) => {
+  if (!['sha1', 'sha256', 'sha384', 'sha512'].includes(algorithm)) throw new Error('不支持的摘要算法')
+  return forge.md[algorithm].create()
+}
+const readPrivate = () => {
+  try { return forge.pki.privateKeyFromPem(form.privateKey) }
+  catch { throw new Error('RSA 私钥 PEM 格式无效或不受支持') }
+}
+const readPublic = () => {
+  try { return forge.pki.publicKeyFromPem(form.publicKey) }
+  catch { throw new Error('RSA 公钥 PEM 格式无效或不受支持') }
+}
+const rsaBytes = key => Math.ceil(key.n.bitLength() / 8)
+let disposed = false
+onBeforeUnmount(() => { disposed = true })
+const generateKeyPair = () => {
+  if (generating.value) return
+  const bits = Number(form.keyLength)
+  if (![1024, 2048, 3072].includes(bits)) return ElMessage.error('密钥位数不合法')
+  generating.value = true
+  // node-forge 异步生成避免同步阻塞主线程;在部分环境仍可能较慢。
+  try {
+    forge.pki.rsa.generateKeyPair({ bits, e: 0x10001, workers: -1 }, (error, pair) => {
+      if (disposed) return
+      generating.value = false
+      if (error) return ElMessage.error('密钥生成失败:' + error.message)
+      form.publicKey = forge.pki.publicKeyToPem(pair.publicKey)
+      form.privateKey = forge.pki.privateKeyToPem(pair.privateKey)
+      form.signature = ''
+      ElMessage.success(`已生成 ${bits} 位 RSA 实验密钥对`)
+    })
+  } catch (error) {
+    generating.value = false
+    ElMessage.error('密钥生成失败:' + error.message)
+  }
+}
+const handleSign = () => {
+  try {
+    if (!form.message) throw new Error('请输入待签名消息')
+    if (!form.privateKey.trim()) throw new Error('请填写 RSA 私钥')
+    const bytes = decodeBytes(form.message, form.inputFormat, '消息')
+    const key = readPrivate()
+    const md = createDigest(form.digestAlgorithm)
+    md.update(bytes, 'raw')
+    const signatureBytes = key.sign(md, 'RSASSA-PKCS1-V1_5')
+    form.signature = form.outputFormat === 'base64'
+        ? forge.util.encode64(signatureBytes) : forge.util.bytesToHex(signatureBytes)
+    clearVerification()
+    ElMessage.success(`签名成功(RSA ${key.n.bitLength()} 位,PKCS#1 v1.5)`)
+  } catch (error) { ElMessage.error('签名失败:' + error.message) }
+}
+const handleVerify = () => {
+  try {
+    if (!form.message) throw new Error('请输入待验证消息')
+    if (!form.publicKey.trim()) throw new Error('请填写 RSA 公钥')
+    if (!form.signature.trim()) throw new Error('请输入待验证的签名')
+    const bytes = decodeBytes(form.message, form.inputFormat, '消息')
+    const signatureBytes = decodeBytes(form.signature.trim(), form.outputFormat, '签名值')
+    const key = readPublic()
+    if (signatureBytes.length !== rsaBytes(key)) throw new Error(`签名字节数必须为 ${rsaBytes(key)},实际为 ${signatureBytes.length}`)
+    const md = createDigest(form.digestAlgorithm)
+    md.update(bytes, 'raw')
+    let valid = false
+    try { valid = key.verify(md.digest().bytes(), signatureBytes, 'RSASSA-PKCS1-V1_5') }
+    catch { valid = false }
+    verifyPassed.value = valid
+    verifyStatus.value = valid ? '验证通过:消息、签名与所给公钥匹配' : '验证失败:签名与消息、公钥或算法不匹配'
+    if (valid) ElMessage.success('签名验证通过')
+    else ElMessage.error('签名验证失败')
+  } catch (error) {
+    verifyPassed.value = false
+    verifyStatus.value = '验证失败:' + error.message
+    ElMessage.error(verifyStatus.value)
+  }
+}
+const handleClear = () => {
+  form.message = ''; form.publicKey = ''; form.privateKey = ''; form.signature = ''
+  clearVerification()
+  ElMessage.info('数据已清空')
+}
+</script>
+
+<style scoped>
+.rsa-sign-container {
+  min-height: 100vh;
+}
+.box-card {
+  border-radius: 8px;
+}
+.principle-content,
+.code-content {
+  line-height: 1.8;
+  color: #606266;
+}
+.principle-content h3,
+.code-content h3 {
+  color: #303133;
+  margin-bottom: 15px;
+}
+.principle-content h4 {
+  color: #303133;
+  margin-top: 18px;
+  margin-bottom: 8px;
+}
+.principle-content ol {
+  padding-left: 20px;
+}
+.flowchart-placeholder {
+  margin-top: 20px;
+  padding: 40px;
+  background-color: #fafafa;
+  border: 1px dashed #dcdfe6;
+  text-align: center;
+  color: #909399;
+  border-radius: 4px;
+}
+pre {
+  background-color: #282c34;
+  color: #abb2bf;
+  padding: 15px;
+  border-radius: 4px;
+  overflow-x: auto;
+  font-size: 14px;
+  line-height: 1.5;
+}
+code {
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+}
+.calc-form {
+  max-width: 850px;
+  margin: 20px auto 0;
+}
+.field-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 9px; }
+.hint { font-size: 12px; color: #909399; }
+.button-row { display: flex; gap: 10px; flex-wrap: wrap; width: 100%; }
+.button-row :deep(.el-button) { margin-left: 0; }
+.flowchart-placeholder { overflow-x: auto; padding: 16px; }
+.flowchart-placeholder :deep(svg) { max-width: 100%; height: auto; }
+@media (max-width: 650px) { .rsa-sign-container { padding: 8px; } .calc-form :deep(.el-form-item__label) { width: 100% !important; justify-content: flex-start; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
+</style>

+ 277 - 0
src/view/sysApplication/experiment/asymmetricKeyAlgorithm/Sm2EncView.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="sm2-container">
+    <el-card class="box-card" shadow="never">
+      <template #header><strong>SM2 算法加解密1</strong></template>
+      <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>SM2 公钥加解密原理</h3>
+            <p>SM2 是基于椭圆曲线的国产公钥密码算法。接收者使用私钥生成公钥;发送者使用公钥加密,接收者使用私钥解密。SM2 加密包含椭圆曲线点运算、KDF 密钥派生、异或和 SM3 摘要。</p>
+            <ol>
+              <li>生成随机数 k,计算椭圆曲线点 C1 = kG。</li>
+              <li>检查接收者公钥并计算共享点 (x2, y2) = kP。</li>
+              <li>由 KDF(x2 || y2, 消息比特长度) 派生掩码 t;若 t 全为零则重新选择 k。</li>
+              <li>计算 C2 = M XOR t 和 C3 = SM3(x2 || M || y2)。</li>
+              <li>按选择的格式输出 C1 || C3 || C2(推荐)或 C1 || C2 || C3(兼容旧系统)。</li>
+            </ol>
+            <p><el-alert title="SM2 加密使用随机数,同一明文和公钥多次加密通常得到不同密文;两种密文布局不能直接混用。" type="info" :closable="false" show-icon /></p>
+            <div class="flowchart-placeholder"><div ref="mermaidContainer" class="mermaid-view"></div></div>
+          </div>
+        </el-tab-pane>
+
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>基于 sm-crypto 的核心代码</h3>
+            <pre><code>{{ sampleCode }}</code></pre>
+            <p>此页面使用 <code>import smCrypto from 'sm-crypto'</code> 兼容 CommonJS 包的默认导入方式。密文采用十六进制编码;SM2 密文不是 Base64。</p>
+          </div>
+        </el-tab-pane>
+
+        <el-tab-pane label="加密与解密" name="calculation">
+          <el-form :model="form" label-width="110px" class="calc-form">
+            <el-form-item label="明文格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">Hex 字节</el-radio>
+                <el-radio value="base64">Base64 字节</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="3" placeholder="输入待加密的明文(Hex/Base64 表示原始字节)" />
+            </el-form-item>
+            <el-form-item label="公钥值">
+              <el-input v-model="form.publicKey" type="textarea" :rows="2" placeholder="128 位 Hex 公钥,或带 04 前缀的 130 位 Hex 公钥" />
+            </el-form-item>
+            <el-form-item label="私钥值">
+              <el-input v-model="form.privateKey" type="textarea" :rows="2" placeholder="64 位 Hex 私钥(32 字节;请勿使用真实业务私钥)" />
+            </el-form-item>
+            <el-form-item label-width="0">
+              <div class="button-group"><el-button :icon="Refresh" @click="generateKeyPair">生成密钥对</el-button></div>
+            </el-form-item>
+            <el-form-item label="密文布局">
+              <el-radio-group v-model="form.cipherMode" @change="handleCipherModeChange">
+                <el-radio value="C1C3C2">C1C3C2(推荐)</el-radio>
+                <el-radio value="C1C2C3">C1C2C3(兼容)</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-alert class="mode-notice" type="info" :closable="false" show-icon title="切换密文布局会自动调整现有密文的 C2、C3 顺序;直接编辑密文不会重新加密,需点击解密计算。" />
+            <el-form-item label="密文 (Hex)">
+              <el-input v-model="form.ciphertext" type="textarea" :rows="4" placeholder="加密结果,或粘贴待解密的十六进制密文" />
+            </el-form-item>
+            <el-form-item label="解密结果">
+              <el-input :model-value="decryptedText" type="textarea" :rows="3" readonly placeholder="解密后显示,与当前明文格式一致" />
+            </el-form-item>
+            <el-form-item label="比对结果" v-if="comparisonStatus">
+              <el-tag :type="comparisonStatus === 'match' ? 'success' : 'warning'">
+                {{ comparisonStatus === 'match' ? '与当前消息原文一致' : '与当前消息原文不一致' }}
+              </el-tag>
+            </el-form-item>
+            <el-form-item label-width="0">
+              <div class="button-group">
+                <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="handleClear">清空数据</el-button>
+              </div>
+            </el-form-item>
+            <el-alert title="本页面属于教学演示:请勿输入生产环境私钥。密文验证失败可能是密钥、密文、布局或数据损坏造成的,不能仅凭解密结果确认消息身份。" type="warning" :closable="false" show-icon />
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Refresh, Lock, Unlock, Delete } from '@element-plus/icons-vue'
+import smCrypto from 'sm-crypto'
+import mermaid from 'mermaid'
+
+const { sm2 } = smCrypto
+const activeTab = ref('calculation')
+const form = reactive({ inputFormat: 'string', message: '', publicKey: '', privateKey: '', cipherMode: 'C1C3C2', ciphertext: '' })
+const decryptedText = ref('')
+const comparisonStatus = ref('')
+const mermaidContainer = ref(null)
+let renderSerial = 0
+let previousCipherMode = form.cipherMode
+
+const diagramCode = `flowchart TD
+ A[输入明文 M 和公钥 P] --> B[随机生成 k]
+ B --> C[计算 C1 = kG]
+ C --> D[计算共享点 kP]
+ D --> E[通过 KDF 派生 t]
+ E --> F{t 是否全零}
+ F -->|是| B
+ F -->|否| G[计算 C2 = M XOR t]
+ G --> H[计算 C3 = SM3 x2 M y2]
+ H --> I[输出 C1 C3 C2 或 C1 C2 C3]
+ I --> J[接收方使用私钥解密并验证 C3]`
+const sampleCode = `import smCrypto from 'sm-crypto';
+const { sm2 } = smCrypto;
+const { publicKey, privateKey } = sm2.generateKeyPairHex();
+const mode = 1; // 1: C1C3C2; 0: C1C2C3
+const ciphertextHex = sm2.doEncrypt('你好 SM2', publicKey, mode);
+const plaintext = sm2.doDecrypt(ciphertextHex, privateKey, mode);`
+
+const isHex = value => /^(?:[0-9a-fA-F]{2})+$/.test(value)
+const hexToBytes = hex => Uint8Array.from(hex.match(/../g).map(v => parseInt(v, 16)))
+const bytesToHex = bytes => Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
+const normalizeBase64 = value => {
+  const v = value.trim()
+  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(v)) throw new Error('Base64 格式不正确')
+  const binary = atob(v)
+  const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0))
+  let canonical = ''
+  for (const b of bytes) canonical += String.fromCharCode(b)
+  if (btoa(canonical) !== v) throw new Error('Base64 编码不规范')
+  return bytes
+}
+const encodeBase64 = bytes => {
+  let binary = ''
+  for (const byte of bytes) binary += String.fromCharCode(byte)
+  return btoa(binary)
+}
+const parseMessage = (data, format) => {
+  if (format === 'string') return new TextEncoder().encode(data)
+  if (format === 'hex') {
+    if (data === '') return new Uint8Array()
+    if (!isHex(data)) throw new Error('Hex 必须为偶数位十六进制字符')
+    return hexToBytes(data)
+  }
+  if (format === 'base64') return normalizeBase64(data)
+  throw new Error('不支持的明文格式')
+}
+const formatBytes = (bytes, format) => {
+  if (format === 'hex') return bytesToHex(bytes)
+  if (format === 'base64') return encodeBase64(bytes)
+  return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
+}
+const normalizePublicKey = value => {
+  const key = value.trim()
+  if (!/^(?:04)?[0-9a-fA-F]{128}$/.test(key)) throw new Error('公钥必须是 128 位 Hex(可带 04 前缀)')
+  return key.startsWith('04') && key.length === 130 ? key : `04${key}`
+}
+const normalizePrivateKey = value => {
+  const key = value.trim()
+  if (!/^[0-9a-fA-F]{64}$/.test(key) || /^0+$/.test(key)) throw new Error('私钥必须是非零的 64 位 Hex')
+  return key
+}
+// sm-crypto 输出的 C1 不含 04 前缀:64字节(128 Hex)。C3 是 32字节(64 Hex)。
+// 在两种密文布局间转换时仅重排字段,不重新生成随机密文。
+const convertCipherMode = (ciphertext, fromMode, toMode) => {
+  const text = normalizeCiphertext(ciphertext)
+  if (fromMode === toMode) return text
+  const c1 = text.slice(0, 128)
+  const body = text.slice(128)
+  if (fromMode === 'C1C3C2' && toMode === 'C1C2C3') {
+    return c1 + body.slice(64) + body.slice(0, 64)
+  }
+  if (fromMode === 'C1C2C3' && toMode === 'C1C3C2') {
+    return c1 + body.slice(-64) + body.slice(0, -64)
+  }
+  throw new Error('不支持的密文格式转换')
+}
+const handleCipherModeChange = mode => {
+  const from = previousCipherMode
+  previousCipherMode = mode
+  decryptedText.value = ''
+  comparisonStatus.value = ''
+  if (!form.ciphertext.trim()) return
+  try {
+    form.ciphertext = convertCipherMode(form.ciphertext, from, mode)
+    ElMessage.success(`已将现有密文转换为 ${mode} 布局;可直接点击“解密计算”`)
+  } catch (e) {
+    ElMessage.warning(`切换了布局,但现有密文未转换:${e.message}`)
+  }
+}
+const normalizeCiphertext = value => {
+  const text = value.trim()
+  // sm-crypto 使用不带 04 前缀的 C1(64字节)+C3(32字节)+C2
+  if (!isHex(text) || text.length < 194) throw new Error('密文至少需要 194 位 Hex(C1 128 位 + C3 64 位 + 非空 C2)')
+  return text
+}
+
+watch(() => [form.message, form.inputFormat, form.publicKey, form.privateKey, form.cipherMode, form.ciphertext], () => {
+  decryptedText.value = ''
+  comparisonStatus.value = ''
+}, { flush: 'sync' })
+
+const generateKeyPair = () => {
+  try {
+    const pair = sm2.generateKeyPairHex()
+    form.publicKey = pair.publicKey
+    form.privateKey = pair.privateKey
+    ElMessage.success('SM2 密钥对生成成功')
+  } catch (e) { ElMessage.error(`生成失败:${e.message}`) }
+}
+const handleEncrypt = () => {
+  try {
+    const publicKey = normalizePublicKey(form.publicKey)
+    const data = parseMessage(form.message, form.inputFormat)
+    if (!data.length) throw new Error('请输入非空明文')
+    // sm-crypto 接受字节数组;不要先转成 Hex 字符串,否则 '1' 会被加密为 '31'。
+    const mode = form.cipherMode === 'C1C3C2' ? 1 : 0
+    const ciphertext = sm2.doEncrypt(Array.from(data), publicKey, mode)
+    if (!isHex(ciphertext) || ciphertext.length < 194) throw new Error('加密库返回了异常的密文')
+    form.ciphertext = ciphertext
+    previousCipherMode = form.cipherMode
+    ElMessage.success('加密成功;SM2 随机加密可能每次产生不同密文')
+  } catch (e) { ElMessage.error(`加密失败:${e.message}`) }
+}
+const handleDecrypt = () => {
+  try {
+    const privateKey = normalizePrivateKey(form.privateKey)
+    const ciphertext = normalizeCiphertext(form.ciphertext)
+    const mode = form.cipherMode === 'C1C3C2' ? 1 : 0
+    const result = sm2.doDecrypt(ciphertext, privateKey, mode, { output: 'array' })
+    if (!Array.isArray(result) && !(result instanceof Uint8Array)) throw new Error('解密失败或密文校验未通过')
+    const bytes = Uint8Array.from(result)
+    decryptedText.value = formatBytes(bytes, form.inputFormat)
+    try {
+      const original = parseMessage(form.message, form.inputFormat)
+      comparisonStatus.value = original.length === bytes.length && original.every((x, i) => x === bytes[i]) ? 'match' : 'mismatch'
+    } catch { comparisonStatus.value = '' }
+    ElMessage.success('解密完成;请检查解密结果及明文比对')
+  } catch (e) { decryptedText.value = ''; comparisonStatus.value = ''; ElMessage.error(`解密失败:${e.message}`) }
+}
+const handleClear = () => {
+  Object.assign(form, { inputFormat: 'string', message: '', publicKey: '', privateKey: '', cipherMode: 'C1C3C2', ciphertext: '' })
+  decryptedText.value = ''
+  comparisonStatus.value = ''
+  previousCipherMode = 'C1C3C2'
+  ElMessage.info('数据已清空')
+}
+const renderMermaid = async () => {
+  const serial = ++renderSerial
+  await nextTick()
+  if (activeTab.value !== 'principle' || !mermaidContainer.value) return
+  try {
+    mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'strict' })
+    const { svg, bindFunctions } = await mermaid.render(`sm2-flow-${Date.now()}-${serial}`, diagramCode)
+    if (serial !== renderSerial || activeTab.value !== 'principle' || !mermaidContainer.value) return
+    mermaidContainer.value.innerHTML = svg
+    bindFunctions?.(mermaidContainer.value)
+  } catch (e) {
+    if (mermaidContainer.value) mermaidContainer.value.textContent = `流程图渲染失败:${e.message}`
+  }
+}
+const onTabChange = name => { if (name === 'principle') renderMermaid() }
+</script>
+
+<style scoped>
+.sm2-container {  min-height: 100vh; }
+.box-card {  border-radius: 8px; }
+.principle-content, .code-content { line-height: 1.8; color: #606266; }
+.principle-content h3, .code-content h3 { color: #303133; }
+.flowchart-placeholder { margin-top: 18px; padding: 20px; overflow-x: auto; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 6px; }
+.mermaid-view { min-width: 540px; text-align: center; }
+pre { background: #282c34; color: #e5e7eb; padding: 16px; border-radius: 6px; overflow-x: auto; line-height: 1.6; }
+code { font-family: Consolas, Monaco, monospace; }
+.calc-form { max-width: 850px; margin: 20px auto 0; }
+.mode-notice { margin-bottom: 16px; }
+.button-group { display: flex; flex-wrap: wrap; gap: 12px; width: 100%; }
+.button-group :deep(.el-button) { margin-left: 0; }
+@media (max-width: 640px) { .sm2-container { padding: 8px; } .calc-form :deep(.el-form-item__label) { float: none; } .button-group { justify-content: flex-start; } }
+</style>

+ 240 - 0
src/view/sysApplication/experiment/asymmetricKeyAlgorithm/Sm2SignView.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="sm2-sign-container">
+    <el-card class="box-card" shadow="never">
+      <template #header><strong>SM2 数字签名与验签实验</strong></template>
+      <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>SM2 数字签名原理</h3>
+            <p>SM2 签名不是“使用私钥加密摘要”。它使用私钥、椭圆曲线运算和 SM3 摘要算法生成签名,使用公钥验证签名与消息、用户标识是否匹配。</p>
+            <ol>
+              <li>结合用户标识、椭圆曲线参数与公钥计算 ZA。</li>
+              <li>计算消息摘要 e = SM3(ZA || M)。</li>
+              <li>随机选择 k,并计算椭圆曲线点 [k]G。</li>
+              <li>计算签名分量 r、s,输出 64 字节 r||s 或 ASN.1 DER 编码。</li>
+              <li>验签时重新计算摘要并验证椭圆曲线关系;消息、用户 ID 或签名变化都应导致验签失败。</li>
+            </ol>
+            <div class="flowchart-placeholder">
+              <div ref="mermaidContainer" class="diagram">{{ diagramCode }}</div>
+            </div>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现(sm-crypto)</h3>
+            <pre><code>{{ exampleCode }}</code></pre>
+            <el-alert title="注意:签名用于真实性与完整性验证,并不加密消息。默认用户 ID 为 1234567812345678;签名和验签必须使用一致的 ID。" type="info" :closable="false" show-icon />
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="签名计算与验证" name="calculation">
+          <el-form :model="form" label-width="110px" class="calc-form">
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">Hex</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="3" placeholder="可输入空消息进行标准测试;Hex/Base64 输入代表原始字节" />
+            </el-form-item>
+            <el-form-item label="用户 ID">
+              <el-input v-model="form.userId" placeholder="默认 1234567812345678" />
+            </el-form-item>
+            <el-form-item label="公钥 (Hex)">
+              <el-input v-model="form.publicKey" type="textarea" :rows="2" placeholder="128 位 Hex,或带 04 前缀的 130 位 Hex" />
+            </el-form-item>
+            <el-form-item label="私钥 (Hex)">
+              <el-input v-model="form.privateKey" type="textarea" :rows="2" placeholder="64 位 Hex 私钥(仅用于本地教学,请勿粘贴生产密钥)" />
+            </el-form-item>
+            <el-form-item label-width="0">
+              <div class="button-row"><el-button :icon="Refresh" @click="generateKeyPair">生成密钥对</el-button></div>
+            </el-form-item>
+            <el-form-item label="签名格式">
+              <el-radio-group v-model="form.signatureFormat">
+                <el-radio value="raw">r||s(128 位 Hex)</el-radio>
+                <el-radio value="der">ASN.1 DER(Hex)</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="签名 (Hex)">
+              <el-input v-model="form.signature" type="textarea" :rows="3" placeholder="签名结果或待验证签名,可以粘贴其他工具生成的值" />
+            </el-form-item>
+            <el-form-item label-width="0">
+              <div class="button-row">
+                <el-button type="primary" :icon="EditPen" @click="handleSign">数字签名</el-button>
+                <el-button type="success" :icon="CircleCheck" @click="handleVerify">签名验证</el-button>
+                <el-button :icon="Delete" @click="handleClear">清空数据</el-button>
+              </div>
+            </el-form-item>
+            <el-form-item v-if="verifyStatus" label="验签结果">
+              <el-alert :title="verifyStatus === 'valid' ? '验签通过:签名与当前消息、公钥、用户 ID 匹配' : '验签未通过:请检查消息、公钥、用户 ID 与签名'" :type="verifyStatus === 'valid' ? 'success' : 'error'" :closable="false" show-icon />
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Refresh, EditPen, CircleCheck, Delete } from '@element-plus/icons-vue'
+import smCrypto from 'sm-crypto'
+import mermaid from 'mermaid'
+
+const { sm2 } = smCrypto
+const activeTab = ref('calculation')
+const mermaidContainer = ref(null)
+const verifyStatus = ref('')
+const form = reactive({
+  inputFormat: 'string', message: '', userId: '1234567812345678',
+  publicKey: '', privateKey: '', signatureFormat: 'raw', signature: ''
+})
+const exampleCode = `import smCrypto from 'sm-crypto';
+const { sm2 } = smCrypto;
+const keys = sm2.generateKeyPairHex();
+const signature = sm2.doSignature('Hello', keys.privateKey, {
+  hash: true, publicKey: keys.publicKey, userId: '1234567812345678'
+});
+const valid = sm2.doVerifySignature('Hello', signature, keys.publicKey, {
+  hash: true, userId: '1234567812345678'
+});`
+const diagramCode = `flowchart TD
+A[消息及用户 ID] --> B[计算 ZA]
+B --> C[SM3 计算消息摘要 e]
+C --> D[使用私钥和随机数 k 计算签名 r s]
+D --> E[输出签名]
+E --> F[使用公钥及相同用户 ID 验签]
+F --> G{签名与消息匹配?}
+G -->|是| H[验证通过]
+G -->|否| I[验证失败]`
+
+const isHex = (v) => /^(?:[0-9a-fA-F]{2})*$/.test(v)
+const toBytes = (hex) => Array.from(hex.match(/../g) || [], h => parseInt(h, 16))
+const normalizePublicKey = (key) => {
+  const v = key.trim()
+  if (!/^(?:04)?[0-9a-fA-F]{128}$/.test(v)) throw new Error('公钥须为 128 位 Hex,或以 04 开头的 130 位 Hex')
+  return v.length === 128 ? `04${v}` : v
+}
+const normalizePrivateKey = (key) => {
+  const v = key.trim()
+  if (!/^[0-9a-fA-F]{64}$/.test(v)) throw new Error('私钥须为 64 位 Hex')
+  return v
+}
+const decodeMessage = () => {
+  if (form.inputFormat === 'string') return form.message
+  const source = form.message.trim()
+  if (form.inputFormat === 'hex') {
+    if (!isHex(source)) throw new Error('Hex 消息须为偶数位十六进制字符')
+    return toBytes(source)
+  }
+  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(source)) {
+    throw new Error('Base64 格式不合法')
+  }
+  if (!source) return []
+  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+  const bytes = []
+  let buffer = 0, bits = 0
+  for (const ch of source.replace(/=+$/, '')) {
+    buffer = (buffer << 6) | alphabet.indexOf(ch)
+    bits += 6
+    if (bits >= 8) { bits -= 8; bytes.push((buffer >>> bits) & 255) }
+  }
+  // 确保没有使用不规范的尾部填充位
+  if (bits > 0 && (buffer & ((1 << bits) - 1)) !== 0) throw new Error('Base64 尾部填充位无效')
+  return bytes
+}
+const getUserId = () => {
+  if (!form.userId) throw new Error('用户 ID 不能为空')
+  const byteLength = new TextEncoder().encode(form.userId).length
+  if (byteLength > 8191) throw new Error('用户 ID 长度不能超过 8191 字节')
+  return form.userId
+}
+const validateSignature = (signature) => {
+  const value = signature.trim()
+  if (!value || !isHex(value)) throw new Error('签名必须为合法且非空的 Hex')
+  if (form.signatureFormat === 'raw' && value.length !== 128) {
+    throw new Error('r||s 签名必须是 128 位 Hex')
+  }
+  if (form.signatureFormat === 'der' && (value.length < 16 || !value.toLowerCase().startsWith('30'))) {
+    throw new Error('DER 签名必须是合法的 ASN.1 SEQUENCE Hex')
+  }
+  return value
+}
+watch(() => [form.message, form.inputFormat, form.userId, form.publicKey, form.privateKey, form.signature, form.signatureFormat], () => { verifyStatus.value = '' })
+
+function generateKeyPair() {
+  try {
+    const keys = sm2.generateKeyPairHex()
+    form.publicKey = keys.publicKey
+    form.privateKey = keys.privateKey
+    form.signature = ''
+    ElMessage.success('SM2 密钥对已生成')
+  } catch (err) { ElMessage.error(err.message || '密钥对生成失败') }
+}
+function handleSign() {
+  try {
+    const message = decodeMessage()
+    const privateKey = normalizePrivateKey(form.privateKey)
+    const publicKey = normalizePublicKey(form.publicKey)
+    const userId = getUserId()
+    const options = { hash: true, publicKey, userId, der: form.signatureFormat === 'der' }
+    form.signature = sm2.doSignature(message, privateKey, options)
+    ElMessage.success('签名已生成,可点击“签名验证”检查')
+  } catch (err) { ElMessage.error(err.message || '签名失败') }
+}
+function handleVerify() {
+  try {
+    const message = decodeMessage()
+    const publicKey = normalizePublicKey(form.publicKey)
+    const signature = validateSignature(form.signature)
+    const userId = getUserId()
+    const valid = sm2.doVerifySignature(message, signature, publicKey, {
+      hash: true, userId, der: form.signatureFormat === 'der'
+    })
+    verifyStatus.value = valid ? 'valid' : 'invalid'
+    valid ? ElMessage.success('验签通过') : ElMessage.error('验签失败')
+  } catch (err) {
+    verifyStatus.value = 'invalid'
+    ElMessage.error(err.message || '验签失败')
+  }
+}
+function handleClear() {
+  Object.assign(form, { inputFormat: 'string', message: '', userId: '1234567812345678',
+    publicKey: '', privateKey: '', signatureFormat: 'raw', signature: '' })
+  verifyStatus.value = ''
+  ElMessage.info('已清空')
+}
+let renderGeneration = 0
+const onTabChange = async (name) => {
+  if (name !== 'principle') return
+  const generation = ++renderGeneration
+  await nextTick()
+  if (!mermaidContainer.value) return
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    const { svg } = await mermaid.render(`sm2-sign-diagram-${generation}`, diagramCode)
+    if (generation === renderGeneration && mermaidContainer.value) mermaidContainer.value.innerHTML = svg
+  } catch (err) {
+    if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图渲染失败,请检查 Mermaid 版本'
+    console.error(err)
+  }
+}
+onBeforeUnmount(() => { renderGeneration++ })
+</script>
+
+<style scoped>
+.sm2-sign-container {  min-height: 100vh; }
+.box-card {  border-radius: 8px; }
+.principle-content, .code-content { line-height: 1.8; color: #606266; }
+.principle-content h3, .code-content h3 { color: #303133; }
+.flowchart-placeholder { margin-top: 20px; padding: 20px; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 4px; overflow-x: auto; }
+.diagram { min-width: 0; }
+pre { background: #282c34; color: #d5d9e0; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 14px; line-height: 1.5; }
+code { font-family: Consolas, Monaco, monospace; }
+.calc-form { max-width: 850px; margin: 20px auto 0; }
+.button-row { display: flex; flex-wrap: wrap; gap: 12px; width: 100%; }
+.button-row :deep(.el-button) { margin-left: 0; }
+@media (max-width: 600px) { .sm2-sign-container { padding: 8px; } .button-row { gap: 8px; } }
+</style>

+ 527 - 0
src/view/sysApplication/experiment/classicalCiphers/CaesarView.vue

@@ -0,0 +1,527 @@
+<template>
+  <div class="caesar-container">
+    <el-card class="box-card" shadow="never">
+      <template #header>
+        <div class="clearfix">
+          <span style="font-weight: bold; font-size: 16px;">凯撒密码破解模拟</span>
+        </div>
+      </template>
+
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- ==================== 1. 原理说明 ==================== -->
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>凯撒密码工作原理</h3>
+            <p>
+              凯撒密码(Caesar Cipher)是一种最简单且最广为人知的<strong>替换密码</strong>。
+              它通过将字母表中的每个字母按固定位数(密钥K)进行位移来实现加密。
+            </p>
+
+            <h4>加密原理:</h4>
+            <ol>
+              <li>
+                <strong>字母位移:</strong>
+                将明文中的每个字母在字母表中向后(或向前)移动固定的位数 K(1 ≤ K ≤ 25)。
+              </li>
+              <li>
+                <strong>循环处理:</strong>
+                当位移超出字母表范围时,从另一端循环回来。
+                例如:'Z' 向后移 1 位变成 'A'。
+              </li>
+              <li>
+                <strong>数学公式:</strong>
+                <ul>
+                  <li>加密:$C = (P + K) \mod 26$</li>
+                  <li>解密:$P = (C - K + 26) \mod 26$</li>
+                </ul>
+                其中 P 为明文字母位置,C 为密文字母位置,K 为位移量。
+              </li>
+            </ol>
+
+            <h4>破解原理:</h4>
+            <ol>
+              <li>
+                <strong>暴力破解:</strong>
+                由于凯撒密码只有 25 种可能的密钥(位移量 1-25),
+                可以尝试所有可能的位移量进行解密。
+              </li>
+              <li>
+                <strong>频率分析(可选):</strong>
+                分析密文中字母出现的频率,与标准英语字母频率对比,
+                推测最可能的位移量。
+              </li>
+              <li>
+                <strong>识别明文:</strong>
+                从 25 种解密结果中,找出最像正常文本的结果作为破解答案。
+              </li>
+            </ol>
+
+            <el-alert
+                type="warning"
+                :closable="false"
+                show-icon
+                style="margin-top: 15px;"
+            >
+              <template #title>
+                <strong>安全性提示:</strong>凯撒密码由于密钥空间过小(仅25种可能),极易被暴力破解,不适合用于任何实际的安全场景,仅用于教学演示。
+              </template>
+            </el-alert>
+
+            <div class="flowchart-placeholder">
+              <div ref="mermaidContainer" class="mermaid">{{ diagramCode }}</div>
+            </div>
+          </div>
+        </el-tab-pane>
+
+        <!-- ==================== 2. 算法实现 ==================== -->
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现</h3>
+            <p>以下为凯撒密码加解密与暴力破解的核心算法代码:</p>
+            <pre><code>
+/**
+ * 凯撒密码加密
+ * @param {string} plaintext 明文
+ * @param {number} shift 位移量 (1-25)
+ * @returns {string} 密文
+ */
+function caesarEncrypt(plaintext, shift) {
+  let ciphertext = '';
+  for (let i = 0; i < plaintext.length; i++) {
+    const char = plaintext[i];
+
+    // 处理大写字母
+    if (char >= 'A' && char <= 'Z') {
+      const code = ((char.charCodeAt(0) - 65 + shift) % 26) + 65;
+      ciphertext += String.fromCharCode(code);
+    }
+    // 处理小写字母
+    else if (char >= 'a' && char <= 'z') {
+      const code = ((char.charCodeAt(0) - 97 + shift) % 26) + 97;
+      ciphertext += String.fromCharCode(code);
+    }
+    // 非字母字符保持不变
+    else {
+      ciphertext += char;
+    }
+  }
+  return ciphertext;
+}
+
+/**
+ * 凯撒密码解密
+ * @param {string} ciphertext 密文
+ * @param {number} shift 位移量 (1-25)
+ * @returns {string} 明文
+ */
+function caesarDecrypt(ciphertext, shift) {
+  return caesarEncrypt(ciphertext, 26 - shift);
+}
+
+/**
+ * 凯撒密码暴力破解
+ * @param {string} ciphertext 密文
+ * @returns {Array} 所有可能的解密结果
+ */
+function caesarBruteForce(ciphertext) {
+  const results = [];
+  for (let shift = 1; shift <= 25; shift++) {
+    results.push({
+      shift: shift,
+      plaintext: caesarDecrypt(ciphertext, shift)
+    });
+  }
+  return results;
+}
+            </code></pre>
+          </div>
+        </el-tab-pane>
+
+        <!-- ==================== 3. 加解密与破解 ==================== -->
+        <el-tab-pane label="加解密与破解" name="calculation">
+
+          <!-- 模式切换 -->
+          <el-radio-group v-model="operationMode" style="margin-bottom: 20px;">
+            <el-radio value="encrypt">加解密模式</el-radio>
+            <el-radio value="crack">暴力破解模式</el-radio>
+          </el-radio-group>
+
+          <!-- 加解密模式 -->
+          <div v-if="operationMode === 'encrypt'">
+            <el-form :model="encryptForm" label-width="100px" class="calc-form">
+
+              <el-form-item label="输入格式">
+                <el-radio-group v-model="encryptForm.inputFormat">
+                  <el-radio value="string">字符串</el-radio>
+                  <el-radio value="hex">十六进制</el-radio>
+                  <el-radio value="base64">Base64</el-radio>
+                </el-radio-group>
+              </el-form-item>
+
+              <el-form-item label="明文" required>
+                <el-input
+                    type="textarea"
+                    v-model="encryptForm.plaintext"
+                    :rows="3"
+                    :placeholder="inputPlaceholder"
+                ></el-input>
+              </el-form-item>
+
+              <el-form-item label="位移量" required>
+                <el-input-number
+                    v-model="encryptForm.shift"
+                    :min="1"
+                    :max="25"
+                    :step="1"
+                    :precision="0"
+                    style="width: 100%;"
+                ></el-input-number>
+                <div class="form-tips">位移量范围:1-25</div>
+              </el-form-item>
+
+              <el-form-item label="密文">
+                <el-input
+                    type="textarea"
+                    v-model="encryptForm.ciphertext"
+                    :rows="3"
+                    placeholder="加密结果,或粘贴待解密的密文(始终为文本格式)"
+                ></el-input>
+              </el-form-item>
+
+              <el-form-item label="解密结果">
+                <el-input
+                    v-model="encryptForm.decryptedText"
+                    type="textarea"
+                    :rows="3"
+                    readonly
+                    placeholder="点击解密后显示结果,不会覆盖原始明文"
+                />
+                <div class="form-tips">输入格式仅用于解析明文;密文和解密结果均显示为普通文本。</div>
+              </el-form-item>
+
+              <el-form-item label-width="0">
+                <div class="action-buttons">
+                  <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="clearEncryptForm">清空</el-button>
+                </div>
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <!-- 暴力破解模式 -->
+          <div v-if="operationMode === 'crack'">
+            <el-form :model="crackForm" label-width="100px" class="calc-form">
+
+              <el-form-item label="密文" required>
+                <el-input
+                    type="textarea"
+                    v-model="crackForm.ciphertext"
+                    :rows="3"
+                    placeholder="请输入待破解的密文"
+                ></el-input>
+              </el-form-item>
+
+              <el-form-item label-width="0">
+                <div class="action-buttons">
+                  <el-button type="warning" :icon="Search" @click="handleBruteForce">暴力破解</el-button>
+                  <el-button :icon="Delete" @click="clearCrackForm">清空</el-button>
+                </div>
+              </el-form-item>
+
+              <!-- 破解结果展示 -->
+              <div v-if="crackResults.length > 0" class="crack-results">
+                <h4 style="margin-bottom: 15px; color: #303133;">
+                  破解结果(共 {{ crackResults.length }} 种候选,不代表自动识别出正确明文)
+                </h4>
+                <el-table
+                    :data="crackResults"
+                    border
+                    stripe
+                    style="width: 100%;"
+                    :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
+                >
+                  <el-table-column
+                      prop="shift"
+                      label="位移量"
+                      width="100"
+                      align="center"
+                  ></el-table-column>
+                  <el-table-column
+                      prop="plaintext"
+                      label="解密明文"
+                      show-overflow-tooltip
+                  ></el-table-column>
+                  <el-table-column
+                      label="操作"
+                      width="120"
+                      align="center"
+                  >
+                    <template #default="scope">
+                      <el-button
+                          link
+                          type="primary"
+                          size="small"
+                          @click="useThisResult(scope.row)"
+                      >
+                        使用此结果
+                      </el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </div>
+            </el-form>
+          </div>
+
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, watch, nextTick } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Lock, Unlock, Delete, Search } from '@element-plus/icons-vue';
+import mermaid from 'mermaid';
+
+const activeTab = ref('calculation');
+const operationMode = ref('encrypt');
+const crackResults = ref([]);
+const mermaidContainer = ref(null);
+const diagramCode = `
+flowchart TD
+    A([开始]) --> B[输入明文 P 和位移量 K]
+    B --> C[初始化结果字符串]
+    C --> D{还有未处理字符吗?}
+    D -- 是 --> E[读取字符]
+    E --> F{是英文字母吗?}
+    F -- 是 --> G[按 K 位循环位移]
+    F -- 否 --> H[保持原字符不变]
+    G --> I[添加到结果]
+    H --> I
+    I --> D
+    D -- 否 --> J[输出密文]
+    J --> L([结束])
+`;
+
+let mermaidInitialized = false;
+let mermaidRendered = false;
+const renderMermaid = async () => {
+  if (mermaidRendered || activeTab.value !== 'principle') return;
+  await nextTick();
+  const target = mermaidContainer.value;
+  if (!target) return;
+  try {
+    if (!mermaidInitialized) {
+      mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'strict' });
+      mermaidInitialized = true;
+    }
+    // 不将外部输入传给 Mermaid;唯一 ID 避免多个组件实例冲突。
+    const id = `mermaid-caesar-${Math.random().toString(36).slice(2)}`;
+    const { svg, bindFunctions } = await mermaid.render(id, diagramCode);
+    if (mermaidContainer.value !== target) return;
+    target.innerHTML = svg;
+    bindFunctions?.(target);
+    mermaidRendered = true;
+  } catch (error) {
+    console.error('Mermaid 渲染失败:', error);
+    target.textContent = '流程图渲染失败,请检查 Mermaid 依赖及版本';
+  }
+};
+watch(activeTab, (tab) => {
+  if (tab === 'principle') void renderMermaid();
+}, { flush: 'post' });
+
+const encryptForm = reactive({
+  inputFormat: 'string',
+  plaintext: '',
+  shift: 3,
+  ciphertext: '',
+  decryptedText: ''
+});
+const crackForm = reactive({ ciphertext: '' });
+const inputPlaceholder = computed(() => ({
+  string: '请输入明文(只对英文字母 A-Z、a-z 位移)',
+  hex: '请输入 UTF-8 文本对应的十六进制字节,例如 48656c6c6f',
+  base64: '请输入 UTF-8 文本对应的 Base64,例如 SGVsbG8='
+}[encryptForm.inputFormat]));
+
+/** 位移量必须是 1—25 的整数,防止小数、NaN 和空值导致异常字符。 */
+const validateShift = (shift) => {
+  if (!Number.isInteger(shift) || shift < 1 || shift > 25) {
+    throw new Error('位移量必须是 1–25 的整数');
+  }
+  return shift;
+};
+
+/** 仅 ASCII 英文字母参与位移;其余字符,包括中文和 emoji,保留原样。 */
+const caesarEncrypt = (text, shift) => {
+  validateShift(shift);
+  return text.replace(/[A-Za-z]/g, (ch) => {
+    const first = ch <= 'Z' ? 65 : 97;
+    return String.fromCharCode((ch.charCodeAt(0) - first + shift) % 26 + first);
+  });
+};
+const caesarDecrypt = (text, shift) => {
+  validateShift(shift);
+  return caesarEncrypt(text, 26 - shift);
+};
+const caesarBruteForce = (ciphertext) =>
+    Array.from({ length: 25 }, (_, index) => {
+      const shift = index + 1;
+      return { shift, plaintext: caesarDecrypt(ciphertext, shift) };
+    });
+
+const decodeUtf8 = (bytes) => new TextDecoder('utf-8', { fatal: true }).decode(bytes);
+const decodeInput = (value, format) => {
+  if (format === 'string') return value;
+  if (format === 'hex') {
+    if (!/^(?:[0-9a-fA-F]{2})+$/.test(value)) {
+      throw new Error('十六进制输入必须是非空、偶数位的合法 Hex 字节');
+    }
+    const pairs = value.match(/.{2}/g);
+    return decodeUtf8(Uint8Array.from(pairs, (pair) => parseInt(pair, 16)));
+  }
+  if (format === 'base64') {
+    if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value) || !value) {
+      throw new Error('请输入合法的标准 Base64 字符串');
+    }
+    const binary = atob(value);
+    return decodeUtf8(Uint8Array.from(binary, (ch) => ch.charCodeAt(0)));
+  }
+  throw new Error('不支持的输入格式');
+};
+const handleEncrypt = () => {
+  if (!encryptForm.plaintext) return ElMessage.warning('请输入明文');
+  try {
+    const plaintext = decodeInput(encryptForm.plaintext, encryptForm.inputFormat);
+    encryptForm.ciphertext = caesarEncrypt(plaintext, encryptForm.shift);
+    encryptForm.decryptedText = '';
+    ElMessage.success('加密完成');
+  } catch (error) {
+    ElMessage.error(`加密失败:${error.message}`);
+  }
+};
+const handleDecrypt = () => {
+  if (!encryptForm.ciphertext) return ElMessage.warning('请输入密文');
+  try {
+    encryptForm.decryptedText = caesarDecrypt(encryptForm.ciphertext, encryptForm.shift);
+    ElMessage.success('解密计算完成(凯撒密码不提供真实性校验)');
+  } catch (error) {
+    ElMessage.error(`解密失败:${error.message}`);
+  }
+};
+const handleBruteForce = () => {
+  if (!crackForm.ciphertext) return ElMessage.warning('请输入待破解的密文');
+  try {
+    crackResults.value = caesarBruteForce(crackForm.ciphertext);
+    ElMessage.success('已枚举 25 个候选位移,请人工判断正确明文');
+  } catch (error) {
+    ElMessage.error(`破解失败:${error.message}`);
+  }
+};
+const useThisResult = (result) => {
+  encryptForm.inputFormat = 'string';
+  encryptForm.plaintext = result.plaintext;
+  encryptForm.ciphertext = crackForm.ciphertext;
+  encryptForm.decryptedText = result.plaintext;
+  encryptForm.shift = result.shift;
+  operationMode.value = 'encrypt';
+  nextTick(() => { encryptForm.decryptedText = result.plaintext; });
+  ElMessage.success(`已使用位移量 ${result.shift} 的候选明文`);
+};
+const clearEncryptForm = () => {
+  encryptForm.plaintext = '';
+  encryptForm.ciphertext = '';
+  encryptForm.decryptedText = '';
+  encryptForm.shift = 3;
+  encryptForm.inputFormat = 'string';
+};
+const clearCrackForm = () => {
+  crackForm.ciphertext = '';
+  crackResults.value = [];
+};
+// 输入变化后旧的解密结果不再可信。
+watch(() => [encryptForm.ciphertext, encryptForm.shift], () => {
+  encryptForm.decryptedText = '';
+});
+watch(() => crackForm.ciphertext, () => {
+  crackResults.value = [];
+});
+</script>
+
+<style scoped>
+.caesar-container {
+  min-height: 100vh;
+}
+.box-card {
+  border-radius: 8px;
+}
+.principle-content,
+.code-content {
+  line-height: 1.8;
+  color: #606266;
+}
+.principle-content h3,
+.code-content h3 {
+  color: #303133;
+  margin-bottom: 15px;
+}
+.principle-content h4 {
+  color: #303133;
+  margin-top: 18px;
+  margin-bottom: 8px;
+}
+.principle-content ol {
+  padding-left: 20px;
+}
+.flowchart-placeholder {
+  margin-top: 20px;
+  padding: 40px;
+  background-color: #fafafa;
+  border: 1px dashed #dcdfe6;
+  text-align: center;
+  color: #909399;
+  border-radius: 4px;
+}
+pre {
+  background-color: #282c34;
+  color: #abb2bf;
+  padding: 15px;
+  border-radius: 4px;
+  overflow-x: auto;
+  font-size: 14px;
+  line-height: 1.5;
+}
+code {
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+}
+.calc-form {
+  max-width: 850px;
+  margin: 20px auto 0;
+}
+.form-tips {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+  line-height: 1.5;
+}
+.action-buttons {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  width: 100%;
+}
+.action-buttons :deep(.el-button + .el-button) {
+  margin-left: 0;
+}
+.mermaid { overflow-x: auto; }
+.crack-results {
+  margin-top: 30px;
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+}
+</style>

+ 523 - 0
src/view/sysApplication/experiment/classicalCiphers/RailView.vue

@@ -0,0 +1,523 @@
+<template>
+  <div class="rail-fence-container">
+    <el-card class="box-card" shadow="never">
+      <!-- Vue 3 推荐使用 template #header 替代 div slot="header" -->
+      <template #header>
+        <div class="clearfix">
+          <span style="font-weight: bold; font-size: 16px;">栅栏密码破解模拟</span>
+        </div>
+      </template>
+
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- ==================== 1. 原理说明 ==================== -->
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>栅栏密码工作原理</h3>
+            <p>
+              栅栏密码(Rail Fence Cipher)是一种经典的<strong>置换密码</strong>。
+              它不改变明文中的字符本身,而是通过改变字符的位置来隐藏信息。
+              其核心思想是将明文按锯齿形(Zig-Zag)模式写入多栏(多行),然后再按行依次读出形成密文。
+            </p>
+
+            <h4>加密原理(以栏数=3为例):</h4>
+            <ol>
+              <li><strong>锯齿形写入:</strong>将明文字符从上到下、再从下到上呈“W”型(锯齿形)交替写入各个栏中。</li>
+              <li><strong>按行读出:</strong>写满所有字符后,按从上到下的顺序,将每一栏的字符依次拼接,形成最终密文。</li>
+            </ol>
+            <div style="background: #f5f7fa; padding: 15px; border-radius: 4px; font-family: monospace; color: #303133; margin: 10px 0;">
+              明文: H E L L O W O R L D (10个字符)<br>
+              栏数: 3<br><br>
+              H . . . O . . . L . &nbsp; (第1栏: H O L)<br>
+              . E . L . W . R . D &nbsp; (第2栏: E L W R D)<br>
+              . . L . . . O . . . &nbsp; (第3栏: L O)<br><br>
+              <strong>密文: H O L E L W R D L O</strong>
+            </div>
+
+            <h4>破解原理:</h4>
+            <ol>
+              <li><strong>暴力遍历:</strong>由于栅栏密码的密钥仅仅是“栏数”,且栏数通常小于明文长度。页面允许栏数 2–50,破解枚举此范围内小于密文字符数的所有候选。
+                因此可以尝试从 2 到 (密文长度-1) 的所有可能栏数进行解密。</li>
+              <li><strong>还原矩阵:</strong>根据假设的栏数,计算出每一栏应包含的字符数量,将密文重新分配回锯齿形矩阵中,最后按列(原始读取顺序)读出明文。</li>
+            </ol>
+
+            <el-alert
+                title="安全性提示"
+                type="warning"
+                :closable="false"
+                show-icon
+                style="margin-top: 15px;"
+            >
+              <template #title>
+                栅栏密码仅改变了字符位置,未改变字符频率分布,极易被暴力破解,仅适用于密码学教学演示。
+              </template>
+            </el-alert>
+
+            <div class="flowchart-placeholder">
+
+              <div ref="mermaidContainer" class="mermaid">{{ diagramCode }}</div>
+            </div>
+          </div>
+        </el-tab-pane>
+
+        <!-- ==================== 2. 算法实现 ==================== -->
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现</h3>
+            <p>以下为栅栏密码加解密与暴力破解的核心算法代码(基于 Zig-Zag 轨迹):</p>
+            <pre><code>
+/**
+ * 栅栏密码加密 (Zig-Zag 轨迹)
+ * @param {string} text 明文
+ * @param {number} rails 栏数
+ */
+function railFenceEncrypt(text, rails) {
+  const letters = Array.from(text);
+  if (rails >= letters.length || rails <= 1) return text;
+  let fence = Array.from({ length: rails }, () => []);
+  let rail = 0, dir = 1; // dir: 1表示向下,-1表示向上
+
+  for (let char of letters) {
+    fence[rail].push(char);
+    if (rail === 0) dir = 1;
+    else if (rail === rails - 1) dir = -1;
+    rail += dir;
+  }
+  // 将二维数组展平并拼接
+  return fence.flat().join('');
+}
+
+/**
+ * 栅栏密码解密
+ * @param {string} cipher 密文
+ * @param {number} rails 栏数
+ */
+function railFenceDecrypt(cipher, rails) {
+  const letters = Array.from(cipher);
+  if (rails >= letters.length || rails <= 1) return cipher;
+  let n = letters.length;
+
+  // 1. 计算每栏应分配的字符数量
+  let railLengths = Array(rails).fill(0);
+  let rail = 0, dir = 1;
+  for (let i = 0; i < n; i++) {
+    railLengths[rail]++;
+    if (rail === 0) dir = 1;
+    else if (rail === rails - 1) dir = -1;
+    rail += dir;
+  }
+
+  // 2. 将密文按长度切分,填入各栏
+  let fence = [];
+  let idx = 0;
+  for (let i = 0; i < rails; i++) {
+    fence.push(letters.slice(idx, idx + railLengths[i]));
+    idx += railLengths[i];
+  }
+
+  // 3. 按照 Zig-Zag 轨迹重新读取字符
+  let result = [];
+  let railIdx = Array(rails).fill(0);
+  rail = 0; dir = 1;
+  for (let i = 0; i < n; i++) {
+    result.push(fence[rail][railIdx[rail]++]);
+    if (rail === 0) dir = 1;
+    else if (rail === rails - 1) dir = -1;
+    rail += dir;
+  }
+  return result.join('');
+}
+            </code></pre>
+          </div>
+        </el-tab-pane>
+
+        <!-- ==================== 3. 加解密与破解 ==================== -->
+        <el-tab-pane label="加解密与破解" name="calculation">
+
+          <el-radio-group v-model="mode" style="margin-bottom: 20px;">
+            <el-radio value="encrypt">加解密模式</el-radio>
+            <el-radio value="crack">暴力破解模式</el-radio>
+          </el-radio-group>
+
+          <!-- 加解密模式 -->
+          <div v-if="mode === 'encrypt'">
+            <el-form :model="encryptForm" label-width="100px" class="calc-form">
+              <el-form-item label="明文" required>
+                <el-input
+                    type="textarea"
+                    v-model="encryptForm.plainText"
+                    :rows="3"
+                    placeholder="请输入明文(支持任意字符,将按Zig-Zag轨迹置换)">
+                </el-input>
+              </el-form-item>
+
+              <el-form-item label="栏数" required>
+                <el-input-number
+                    v-model="encryptForm.rails"
+                    :min="2"
+                    :max="50"
+                    :step="1"
+                    step-strictly
+                    style="width: 100%;">
+                </el-input-number>
+                <div class="form-tips">栏数必须为 2–50 的整数,且小于字符数;空格及标点也参与置换</div>
+              </el-form-item>
+
+              <el-form-item label="密文">
+                <el-input
+                    type="textarea"
+                    v-model="encryptForm.cipherText"
+                    :rows="3"
+                    placeholder="加密结果 / 待解密的密文">
+                </el-input>
+              </el-form-item>
+
+              <el-form-item label="解密结果">
+                <el-input type="textarea" :model-value="decryptResult" :rows="3" readonly placeholder="点击解密校验后显示结果" />
+                <div v-if="comparisonStatus" class="form-tips">{{ comparisonStatus }}</div>
+              </el-form-item>
+
+              <el-form-item label-width="0" class="action-form-item">
+                <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-item>
+            </el-form>
+          </div>
+
+          <!-- 破解模式 -->
+          <div v-if="mode === 'crack'">
+            <el-form :model="crackForm" label-width="100px" class="calc-form">
+              <el-form-item label="密文" required>
+                <el-input
+                    type="textarea"
+                    v-model="crackForm.cipherText"
+                    :rows="4"
+                    placeholder="请输入待破解的密文">
+                </el-input>
+              </el-form-item>
+
+              <el-form-item label-width="0" class="action-form-item">
+                <el-button
+                    type="warning"
+                    :icon="Search"
+                    @click="handleBruteForce"
+                >
+                  自动破解 (暴力遍历)
+                </el-button>
+                <el-button  :icon="Delete" @click="clearCrack">清空</el-button>
+              </el-form-item>
+
+              <!-- 破解结果展示 -->
+              <div v-if="crackResults.length > 0" class="crack-results">
+                <h4 style="margin-bottom: 15px; color: #303133;">
+                  破解结果推导(共尝试 {{ crackResults.length }} 种栏数)
+                </h4>
+                <el-table
+                    :data="crackResults"
+                    border
+                    stripe
+                    style="width: 100%;"
+                    :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
+                    max-height="400"
+                >
+                  <el-table-column
+                      prop="rails"
+                      label="栏数"
+                      width="100"
+                      align="center">
+                  </el-table-column>
+                  <el-table-column
+                      prop="plainText"
+                      label="解密明文"
+                      show-overflow-tooltip>
+                  </el-table-column>
+                  <el-table-column label="操作" width="110" align="center">
+                    <template #default="scope">
+                      <el-button link type="primary" @click="useCandidate(scope.row)">使用此结果</el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </div>
+            </el-form>
+          </div>
+
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue'
+import { Lock, Unlock, Delete, Search } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import mermaid from "mermaid";
+
+// ==================== 响应式状态 ====================
+const activeTab = ref('calculation')
+const mode = ref('encrypt')
+
+const mermaidContainer = ref(null);
+
+const diagramCode = ref(`
+flowchart TD
+    A[开始] --> B[输入明文 plaintext 和栏数 k]
+    B --> C[初始化 k 个空行 rows]
+    C --> D[设置当前行 row = 1 方向 dir = 1]
+
+    D --> E{是否还有明文字符}
+    E -->|是| F[取下一个字符 c]
+    F --> G[将 c 追加到 rows 当前行]
+    G --> H{row 是否等于 1}
+
+    H -->|是| I[dir = 1]
+    H -->|否| J{row 是否等于 k}
+
+    J -->|是| K[dir = -1]
+    J -->|否| L[保持 dir 不变]
+
+    I --> M[row = row + dir]
+    K --> M
+    L --> M
+
+    M --> E
+
+    E -->|否| N[按行拼接所有 rows]
+    N --> O[输出密文 ciphertext]
+    O --> P[结束]
+`);
+
+let mermaidRenderToken = 0
+watch(activeTab, async (tab) => {
+  if (tab === 'principle') await renderMermaid()
+}, { immediate: true })
+onBeforeUnmount(() => { mermaidRenderToken++ })
+
+const renderMermaid = async () => {
+  mermaid.initialize({
+    startOnLoad: false,
+    theme: 'default',
+    flowchart: { curve: 'basis' }
+  });
+
+  const token = ++mermaidRenderToken
+  await nextTick();
+  if (mermaidContainer.value && activeTab.value === 'principle') {
+    try {
+      // 使用 render API,兼容 v9 和 v10+
+      const { svg } = await mermaid.render(`mermaid-rail-${token}`, diagramCode.value);
+      if (token === mermaidRenderToken && mermaidContainer.value && activeTab.value === 'principle') mermaidContainer.value.innerHTML = svg;
+    } catch (error) {
+      console.error('Mermaid 渲染失败:', error);
+      mermaidContainer.value.textContent = '流程图渲染失败,请检查语法';
+    }
+  }
+};
+
+const encryptForm = reactive({
+  plainText: '',
+  cipherText: '',
+  rails: 3
+})
+
+const crackForm = reactive({
+  cipherText: ''
+})
+
+const decryptResult = ref('')
+const comparisonStatus = ref('')
+const crackResults = ref([])
+
+// ==================== 核心算法 ====================
+
+/**
+ * 栅栏密码加密 (Zig-Zag)
+ */
+const chars = (s) => Array.from(s)
+const validRails = (rails, length) => Number.isInteger(rails) && rails >= 2 && rails <= 50 && rails < length
+
+const railFenceEncrypt = (text, rails) => {
+  const letters = chars(text)
+  if (!validRails(rails, letters.length)) return text
+  const fence = Array.from({ length: rails }, () => [])
+  let rail = 0, dir = 1
+  for (const letter of letters) {
+    fence[rail].push(letter)
+    if (rail === 0) dir = 1
+    else if (rail === rails - 1) dir = -1
+    rail += dir
+  }
+  return fence.flat().join('')
+}
+
+const railFenceDecrypt = (cipher, rails) => {
+  const letters = chars(cipher)
+  if (!validRails(rails, letters.length)) return cipher
+  const lengths = Array(rails).fill(0)
+  let rail = 0, dir = 1
+  for (let i = 0; i < letters.length; i++) {
+    lengths[rail]++
+    if (rail === 0) dir = 1
+    else if (rail === rails - 1) dir = -1
+    rail += dir
+  }
+  const fence = []
+  let offset = 0
+  for (const len of lengths) {
+    fence.push(letters.slice(offset, offset + len))
+    offset += len
+  }
+  const offsets = Array(rails).fill(0)
+  rail = 0; dir = 1
+  const result = []
+  for (let i = 0; i < letters.length; i++) {
+    result.push(fence[rail][offsets[rail]++])
+    if (rail === 0) dir = 1
+    else if (rail === rails - 1) dir = -1
+    rail += dir
+  }
+  return result.join('')
+}
+
+watch(() => [encryptForm.plainText, encryptForm.cipherText, encryptForm.rails], () => {
+  decryptResult.value = ''
+  comparisonStatus.value = ''
+})
+watch(() => crackForm.cipherText, () => { crackResults.value = [] })
+
+// ==================== 交互逻辑 ====================
+
+const ensureRails = (value, text) => {
+  if (!validRails(value, chars(text).length)) {
+    ElMessage.warning('栏数必须为 2–50 的整数,并且小于输入文本字符数')
+    return false
+  }
+  return true
+}
+
+const handleEncrypt = () => {
+  if (!encryptForm.plainText) return ElMessage.warning('请输入明文')
+  if (!ensureRails(encryptForm.rails, encryptForm.plainText)) return
+  encryptForm.cipherText = railFenceEncrypt(encryptForm.plainText, encryptForm.rails)
+  ElMessage.success('加密成功')
+}
+
+const handleDecrypt = () => {
+  if (!encryptForm.cipherText) return ElMessage.warning('请输入密文')
+  if (!ensureRails(encryptForm.rails, encryptForm.cipherText)) return
+  const result = railFenceDecrypt(encryptForm.cipherText, encryptForm.rails)
+  decryptResult.value = result
+  comparisonStatus.value = encryptForm.plainText
+      ? (result === encryptForm.plainText ? '与输入明文一致(往返验证通过)' : '与输入明文不一致;请确认密文、栏数及明文是否对应')
+      : '已解密;无原始明文可供比对'
+  ElMessage.success('解密计算完成')
+}
+
+const handleBruteForce = () => {
+  const cipher = crackForm.cipherText
+  const length = chars(cipher).length
+  if (length < 3) return ElMessage.warning('密文至少需要 3 个字符')
+  const max = Math.min(50, length - 1)
+  crackResults.value = Array.from({ length: max - 1 }, (_, i) => {
+    const rails = i + 2
+    return { rails, plainText: railFenceDecrypt(cipher, rails) }
+  })
+  ElMessage.success(`列出 ${crackResults.value.length} 种栏数的候选明文(不是自动识别正确答案)`)
+}
+
+const useCandidate = async (row) => {
+  encryptForm.rails = row.rails
+  encryptForm.cipherText = crackForm.cipherText
+  encryptForm.plainText = ''
+  mode.value = 'encrypt'
+  await nextTick()
+  decryptResult.value = row.plainText
+  comparisonStatus.value = '已选中候选结果,请人工判断内容是否有意义'
+}
+
+const clearEncrypt = () => {
+  encryptForm.plainText = ''
+  encryptForm.cipherText = ''
+  encryptForm.rails = 3
+  decryptResult.value = ''
+  comparisonStatus.value = ''
+  ElMessage.info('已清空')
+}
+
+const clearCrack = () => {
+  crackForm.cipherText = ''
+  crackResults.value = []
+  ElMessage.info('已清空')
+}
+</script>
+
+<style scoped>
+.rail-fence-container {
+  min-height: 100vh;
+}
+.box-card {
+  border-radius: 8px;
+}
+.principle-content,
+.code-content {
+  line-height: 1.8;
+  color: #606266;
+}
+.principle-content h3,
+.code-content h3 {
+  color: #303133;
+  margin-bottom: 15px;
+}
+.principle-content h4 {
+  color: #303133;
+  margin-top: 18px;
+  margin-bottom: 8px;
+}
+.principle-content ol {
+  padding-left: 20px;
+}
+.flowchart-placeholder {
+  margin-top: 20px;
+  padding: 40px;
+  background-color: #fafafa;
+  border: 1px dashed #dcdfe6;
+  text-align: center;
+  color: #909399;
+  border-radius: 4px;
+}
+pre {
+  background-color: #282c34;
+  color: #abb2bf;
+  padding: 15px;
+  border-radius: 4px;
+  overflow-x: auto;
+  font-size: 14px;
+  line-height: 1.5;
+}
+code {
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+}
+.calc-form {
+  max-width: 850px;
+  margin: 20px auto 0;
+}
+.form-tips {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+  line-height: 1.5;
+}
+.crack-results {
+  margin-top: 30px;
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+}
+</style>
+<style scoped>
+.action-form-item :deep(.el-form-item__content) { display: flex; flex-wrap: wrap; gap: 10px; margin-left: 0 !important; }
+.action-form-item :deep(.el-button + .el-button) { margin-left: 0; }
+.mermaid { overflow-x: auto; }
+@media (max-width: 600px) { .rail-fence-container { padding: 8px; } .crack-results { padding: 8px; } }
+</style>

+ 385 - 0
src/view/sysApplication/experiment/classicalCiphers/VigenereView.vue

@@ -0,0 +1,385 @@
+<template>
+  <div class="vigenere-container">
+    <el-card
+      class="box-card"
+      shadow="never"
+    >
+      <template #header><strong>维吉尼亚密码加解密与破解模拟</strong></template>
+      <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>本页面仅对 A–Z 字母进行运算,并统一转换为大写;空格、标点、数字、中文和其他字符均被过滤。密钥只允许英文字母。结果不能还原被过滤的字符。</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>
+          </div>
+        </el-tab-pane>
+        <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>
+          </div>
+        </el-tab-pane>
+        <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>
+            </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>
+          <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="候选密钥长度"
+            >
+              <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)"
+                >
+                  {{ 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"
+            >
+              <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>
+            </div>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { computed, nextTick, reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Lock, Unlock, Delete, Search, MagicStick } from '@element-plus/icons-vue'
+import mermaid from 'mermaid'
+
+const activeTab = ref('calculation')
+const mode = ref('encrypt')
+const encryptForm = reactive({ plainText: '', cipherText: '', keyword: '' })
+const crackForm = reactive({ cipherText: '', targetLength: 3 })
+const decryptedResult = ref(null)
+const lengthOptions = ref([])
+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)
+  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('')
+}
+const decrypt = (text, key) => {
+  const p = extractLetters(text); const 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('')
+}
+const countLetters = text => {
+  const counts = Array(26).fill(0)
+  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))
+}
+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))
+  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)
+}
+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 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 }
+    }
+    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 }
+}
+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)
+  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)
+  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
+  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) }
+  })
+  ElMessage.info('生成的是统计候选结果,正确性需人工判断')
+}
+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
+  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
+  try {
+    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 = '流程图暂时无法显示,请参阅上方文字步骤。' }
+}
+async function onTabChange(tab) { if (tab === 'principle') { await nextTick(); await renderMermaid() } }
+</script>
+
+<style scoped>
+.vigenere-container{min-height:100vh}
+.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)}
+.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}
+.mode-group{margin-bottom:12px}
+.action-item :deep(.el-form-item__content){margin-left:0!important}
+.actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;width:100%}
+.actions :deep(.el-button){margin-left:0}
+.length-options{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
+.length-options :deep(.el-button){margin-left:0}
+.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>

+ 263 - 0
src/view/sysApplication/experiment/hashAlgorithm/HmacView.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="hmac-container">
+    <el-card class="box-card" shadow="never">
+      <template #header><strong>HMAC 算法原理实现与验证</strong></template>
+      <el-tabs v-model="activeTab" type="border-card" @tab-change="handleTabChange">
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>HMAC 算法工作原理</h3>
+            <p>HMAC 是使用密钥和哈希函数计算消息认证码的算法,用于验证消息的完整性以及验证方是否持有相同密钥。HMAC 不提供加密,也不能单独防止重放攻击。</p>
+            <p>HMAC 使用两个固定填充值 <code>ipad = 0x36</code>、<code>opad = 0x5c</code>,计算公式为:</p>
+            <pre>HMAC(K, M) = H((K' XOR opad) || H((K' XOR ipad) || M))</pre>
+            <ol>
+              <li>如果密钥超过哈希分组长度,先对密钥做一次哈希,再补零到分组长度。</li>
+              <li>将处理后的密钥与 ipad 异或,拼接消息并计算内层摘要。</li>
+              <li>将处理后的密钥与 opad 异或,拼接内层摘要并计算最终认证码。</li>
+            </ol>
+            <el-alert type="warning" title="教学提醒" :closable="false" show-icon>
+              标准 HMAC 不需要额外盐值;“自定义拼接 MAC”仅用于演示不安全的构造,不应用于生产环境。SHA-1 与 MD5 仅保留作历史算法对比,新系统优先选 HMAC-SHA256。
+            </el-alert>
+            <div class="flowchart-placeholder">
+              <div ref="mermaidContainer" class="mermaid-graph">{{ diagramCode }}</div>
+            </div>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>核心代码示例(CryptoJS)</h3>
+            <p>请注意消息和密钥需要根据实际编码解析,不能把十六进制字符串直接当成原始字节。</p>
+            <pre><code>{{ exampleCode }}</code></pre>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="计算与校验" name="calculation">
+          <el-form :model="form" label-width="125px" class="calc-form">
+            <el-form-item label="消息编码">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">Hex</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="3" placeholder="输入消息;Hex/Base64 模式请使用相应编码" />
+            </el-form-item>
+            <el-form-item label="密钥编码">
+              <el-radio-group v-model="form.keyFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">Hex</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="密钥值">
+              <el-input v-model="form.key" placeholder="输入密钥,或者生成 32 字节随机密钥">
+                <template #append><el-button :icon="Refresh" @click="generateKey">生成密钥</el-button></template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="哈希算法">
+              <el-select v-model="form.hashType" style="width:100%">
+                <el-option label="HMAC-SHA256(推荐)" value="SHA256" />
+                <el-option label="HMAC-SHA1(教学)" value="SHA1" />
+                <el-option label="HMAC-MD5(教学)" value="MD5" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="认证模式">
+              <el-radio-group v-model="form.authMode">
+                <el-radio value="standard">标准 HMAC</el-radio>
+                <el-radio value="custom">自定义拼接 MAC(教学)</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <template v-if="form.authMode === 'custom'">
+              <el-alert class="custom-warning" type="warning" :closable="false" show-icon title="自定义拼接不是标准 HMAC,不能用于真实的消息认证。" />
+              <el-form-item label="附加盐值">
+                <el-input v-model="form.salt" placeholder="可选;按 UTF-8 文本拼接" />
+              </el-form-item>
+              <el-form-item label="拼接方式">
+                <el-select v-model="form.concatMethod" style="width:100%">
+                  <el-option label="密钥 + 盐值 + 原文(前缀)" value="prefix" />
+                  <el-option label="原文 + 密钥 + 盐值(后缀)" value="suffix" />
+                </el-select>
+              </el-form-item>
+            </template>
+            <el-form-item label="MAC 输出格式">
+              <el-radio-group v-model="form.outputFormat">
+                <el-radio value="hex">Hex</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="计算结果">
+              <el-input :model-value="form.result" readonly placeholder="点击计算生成 MAC">
+                <template #append><el-button :icon="CopyDocument" aria-label="复制计算结果" @click="copyResult">复制</el-button></template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="待验证 MAC">
+              <el-input v-model="form.expectedMac" placeholder="输入待验证值;计算后可自动填入,也可粘贴外部 MAC" />
+            </el-form-item>
+            <el-form-item label-width="0" class="actions">
+              <div class="button-group">
+                <el-button type="primary" :icon="Operation" @click="handleCalculate">计算 MAC</el-button>
+                <el-button type="success" :icon="CircleCheck" @click="handleVerify">验证 MAC</el-button>
+                <el-button :icon="Delete" @click="clearResults">清除结果</el-button>
+              </div>
+            </el-form-item>
+            <el-alert v-if="verificationStatus" :type="verificationStatus === 'ok' ? 'success' : 'error'" :closable="false" show-icon :title="verificationStatus === 'ok' ? '验证通过:输入 MAC 与重新计算值一致' : '验证失败:输入 MAC 不匹配'" />
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Refresh, CopyDocument, Operation, CircleCheck, Delete } from '@element-plus/icons-vue';
+import CryptoJS from 'crypto-js';
+import mermaid from 'mermaid';
+
+const activeTab = ref('calculation');
+const mermaidContainer = ref(null);
+const verificationStatus = ref('');
+const form = reactive({
+  inputFormat: 'string', message: '', keyFormat: 'string', key: '', hashType: 'SHA256',
+  authMode: 'standard', salt: '', concatMethod: 'prefix', outputFormat: 'hex',
+  result: '', expectedMac: ''
+});
+
+const diagramCode = `flowchart TD
+  A[消息 M 与密钥 K] --> B[按分组长度处理密钥 K']
+  B --> C[K' XOR ipad]
+  B --> D[K' XOR opad]
+  C --> E[拼接消息 M]
+  E --> F[内层哈希]
+  F --> G[拼接内层摘要]
+  D --> G
+  G --> H[外层哈希]
+  H --> I[输出 HMAC]`;
+
+const exampleCode = `import CryptoJS from 'crypto-js';
+const message = CryptoJS.enc.Utf8.parse('Hi There');
+const key = CryptoJS.enc.Hex.parse('0b'.repeat(20));
+const mac = CryptoJS.HmacSHA256(message, key);
+console.log(mac.toString(CryptoJS.enc.Hex));`;
+
+let renderSeq = 0;
+let destroyed = false;
+const renderMermaid = async () => {
+  const seq = ++renderSeq;
+  await nextTick();
+  if (destroyed || !mermaidContainer.value || activeTab.value !== 'principle') return;
+  try {
+    mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'strict' });
+    const rendered = await mermaid.render(`hmac-diagram-${seq}`, diagramCode);
+    if (!destroyed && seq === renderSeq && mermaidContainer.value && activeTab.value === 'principle') {
+      mermaidContainer.value.innerHTML = rendered.svg;
+      rendered.bindFunctions?.(mermaidContainer.value);
+    }
+  } catch (error) {
+    console.error('Mermaid 渲染失败', error);
+    if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图渲染失败';
+  }
+};
+const handleTabChange = (name) => { if (name === 'principle') void renderMermaid(); };
+onBeforeUnmount(() => { destroyed = true; ++renderSeq; });
+
+const parseInput = (value, format, title) => {
+  if (format === 'hex') {
+    if (value.length % 2 !== 0 || !/^[a-fA-F0-9]*$/.test(value)) throw new Error(`${title}不是合法的 Hex(必须为偶数位)`);
+    return CryptoJS.enc.Hex.parse(value);
+  }
+  if (format === 'base64') {
+    const pattern = /^(?:[a-zA-Z0-9+/]{4})*(?:[a-zA-Z0-9+/]{2}==|[a-zA-Z0-9+/]{3}=)?$/;
+    if (!pattern.test(value)) throw new Error(`${title}不是合法的标准 Base64`);
+    const decoded = CryptoJS.enc.Base64.parse(value);
+    if (decoded.toString(CryptoJS.enc.Base64) !== value) throw new Error(`${title}的 Base64 编码不规范`);
+    return decoded;
+  }
+  return CryptoJS.enc.Utf8.parse(value);
+};
+
+const digest = (data) => {
+  if (form.hashType === 'SHA1') return CryptoJS.SHA1(data);
+  if (form.hashType === 'MD5') return CryptoJS.MD5(data);
+  return CryptoJS.SHA256(data);
+};
+const computeMAC = (message, key) => {
+  if (form.authMode === 'standard') {
+    if (form.hashType === 'SHA1') return CryptoJS.HmacSHA1(message, key);
+    if (form.hashType === 'MD5') return CryptoJS.HmacMD5(message, key);
+    return CryptoJS.HmacSHA256(message, key);
+  }
+  const salt = CryptoJS.enc.Utf8.parse(form.salt);
+  if (form.concatMethod === 'prefix') return digest(key.clone().concat(salt).concat(message));
+  return digest(message.clone().concat(key).concat(salt));
+};
+const getCalculatedMac = () => {
+  if (!form.key) throw new Error('请输入或生成密钥');
+  const message = parseInput(form.message, form.inputFormat, '消息');
+  const key = parseInput(form.key, form.keyFormat, '密钥');
+  if (key.sigBytes === 0) throw new Error('密钥不能为空');
+  return computeMAC(message, key);
+};
+const formatMac = (mac) => mac.toString(form.outputFormat === 'base64' ? CryptoJS.enc.Base64 : CryptoJS.enc.Hex);
+const showError = (error) => ElMessage.error(error instanceof Error ? error.message : '操作失败');
+
+const generateKey = () => {
+  try {
+    form.key = CryptoJS.lib.WordArray.random(32).toString(CryptoJS.enc.Hex);
+    form.keyFormat = 'hex';
+    ElMessage.success('已生成 256 位随机密钥(Hex 编码)');
+  } catch (error) { showError(error); }
+};
+const handleCalculate = () => {
+  try {
+    form.result = formatMac(getCalculatedMac());
+    form.expectedMac = form.result;
+    verificationStatus.value = '';
+    ElMessage.success('MAC 计算完成');
+  } catch (error) { form.result = ''; verificationStatus.value = ''; showError(error); }
+};
+const handleVerify = () => {
+  try {
+    const expected = parseInput(form.expectedMac, form.outputFormat, '待验证 MAC');
+    const actual = getCalculatedMac();
+    // 避免提前退出的字节比较;浏览器端仍不能保证真正恒定时间。
+    const actualHex = actual.toString(CryptoJS.enc.Hex);
+    const expectedHex = expected.toString(CryptoJS.enc.Hex);
+    const max = Math.max(actualHex.length, expectedHex.length);
+    let diff = actualHex.length ^ expectedHex.length;
+    for (let i = 0; i < max; i++) diff |= (actualHex.charCodeAt(i) || 0) ^ (expectedHex.charCodeAt(i) || 0);
+    verificationStatus.value = diff === 0 ? 'ok' : 'error';
+    if (diff === 0) ElMessage.success('MAC 验证通过');
+    else ElMessage.error('MAC 验证失败');
+  } catch (error) { verificationStatus.value = ''; showError(error); }
+};
+const clearResults = () => { form.result = ''; form.expectedMac = ''; verificationStatus.value = ''; };
+const copyResult = async () => {
+  if (!form.result) return ElMessage.warning('暂无可复制结果');
+  try {
+    await navigator.clipboard.writeText(form.result);
+    ElMessage.success('已复制到剪贴板');
+  } catch { ElMessage.warning('自动复制不可用,请手动选中结果复制'); }
+};
+watch(() => [form.message, form.inputFormat, form.key, form.keyFormat, form.hashType, form.authMode, form.salt, form.concatMethod, form.outputFormat], () => {
+  form.result = '';
+  verificationStatus.value = '';
+});
+watch(() => form.expectedMac, () => { verificationStatus.value = ''; });
+</script>
+
+<style scoped>
+.hmac-container {  min-height: 100vh; box-sizing: border-box; }
+.box-card {  border-radius: 8px; }
+.principle-content, .code-content { line-height: 1.8; color: #606266; }
+.principle-content h3, .code-content h3 { color: #303133; }
+.flowchart-placeholder { margin-top: 20px; padding: 20px; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 6px; overflow-x: auto; }
+.mermaid-graph { min-width: 0; }
+pre { background: #282c34; color: #d8dee9; padding: 15px; border-radius: 6px; overflow-x: auto; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
+code { font-family: Consolas, Monaco, monospace; }
+.calc-form { max-width: 850px; margin: 20px auto 0; }
+.custom-warning { margin-bottom: 18px; }
+.button-group { display: flex; flex-wrap: wrap; gap: 12px; }
+.button-group .el-button { margin-left: 0; }
+@media (max-width: 600px) { .hmac-container { padding: 8px; } .calc-form :deep(.el-form-item) { display: block; } .calc-form :deep(.el-form-item__label) { display: block; width: auto !important; text-align: left; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
+</style>

+ 245 - 0
src/view/sysApplication/experiment/hashAlgorithm/Sha1View.vue

@@ -0,0 +1,245 @@
+<template>
+  <div class="sha1-container">
+    <el-card class="box-card" shadow="never">
+      <template #header><strong>SHA-1 算法原理实现与验证</strong></template>
+      <el-alert title="安全提示:SHA-1 已不适合数字签名、证书签名或需要抗碰撞性的安全场景。这里仅用于教学和兼容性测试;密码存储请使用 Argon2id、scrypt 等专用方案。" type="warning" :closable="false" show-icon class="security-note" />
+      <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>SHA-1 算法工作原理</h3>
+            <p>SHA-1 是输出固定 160 位(20 字节)摘要的哈希函数;它不需要密钥,也不是加密算法。已发现实用碰撞攻击,因此不应再用于要求碰撞安全性的场景。</p>
+            <ol>
+              <li>按字节处理消息,在比特末尾填充 1 和若干个 0,使其长度模 512 等于 448。</li>
+              <li>追加原消息的 64 位大端比特长度,得到 512 位整数倍的消息块。</li>
+              <li>初始化五个 32 位状态字 H0~H4。</li>
+              <li>每个消息块扩展为 80 个字并进行 80 轮压缩。</li>
+              <li>拼接五个状态字,输出 160 位摘要。</li>
+            </ol>
+            <p><strong>关于盐值:</strong>本页面的“盐值”仅做简单的文本字节前后拼接,不是 HMAC,也不是安全密码哈希算法。</p>
+            <div ref="mermaidContainer" class="flowchart-placeholder" aria-label="SHA-1 哈希流程图"></div>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现(CryptoJS)</h3>
+            <p>将输入按 UTF-8、Hex 或 Base64 解码为字节序列;盐值始终使用 UTF-8,然后计算 SHA-1:</p>
+            <pre><code>import CryptoJS from 'crypto-js'
+
+// messageBytes 是严格解析输入编码后的 WordArray
+const saltBytes = CryptoJS.enc.Utf8.parse(salt)
+const input = salt
+  ? (prefix
+      ? saltBytes.clone().concat(messageBytes.clone())
+      : messageBytes.clone().concat(saltBytes))
+  : messageBytes
+const digest = CryptoJS.SHA1(input)
+const hex = digest.toString(CryptoJS.enc.Hex)</code></pre>
+            <p>验证时重新计算摘要,并与用户输入的期望摘要比较;这只检查两者是否一致,不能证明 SHA-1 具有抗碰撞性。</p>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="计算与校验" name="calculation">
+          <el-form :model="form" label-width="110px" class="calc-form">
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="4" :placeholder="form.inputFormat === 'string' ? '输入文本;空字符串也是合法测试输入' : '输入严格编码的数据(可留空代表空字节串)'" />
+            </el-form-item>
+            <el-form-item label="盐值(可选)">
+              <el-input v-model="form.salt" placeholder="按 UTF-8 编码拼接,不属于安全密码存储" clearable />
+            </el-form-item>
+            <el-form-item label="拼接方式">
+              <el-select v-model="form.concatMethod" style="width: 100%" :disabled="!form.salt">
+                <el-option label="原文 + 盐值" value="suffix" />
+                <el-option label="盐值 + 原文" value="prefix" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="输出格式">
+              <el-radio-group v-model="form.outputFormat">
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="计算结果">
+              <el-input v-model="form.result" readonly placeholder="点击“计算摘要”获取结果">
+                <template #append><el-button :icon="CopyDocument" aria-label="复制摘要" @click="copyResult" /></template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="待验证摘要">
+              <el-input v-model="form.expected" placeholder="输入或粘贴外部 SHA-1 摘要进行验证" clearable />
+            </el-form-item>
+            <el-form-item label-width="0" class="action-item">
+              <div class="button-group">
+                <el-button type="primary" :icon="Operation" @click="handleCalculate">计算摘要</el-button>
+                <el-button type="success" :icon="CircleCheck" @click="handleVerify">验证摘要</el-button>
+                <el-button :icon="RefreshRight" @click="handleClear">清空</el-button>
+              </div>
+            </el-form-item>
+            <el-alert v-if="verifyState" :title="verifyState.message" :type="verifyState.ok ? 'success' : 'error'" :closable="false" show-icon />
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { CopyDocument, Operation, CircleCheck, RefreshRight } from '@element-plus/icons-vue'
+import CryptoJS from 'crypto-js'
+import mermaid from 'mermaid'
+
+const activeTab = ref('calculation')
+const form = reactive({
+  inputFormat: 'string', message: '', salt: '', concatMethod: 'suffix',
+  outputFormat: 'hex', result: '', expected: ''
+})
+const verifyState = ref(null)
+const mermaidContainer = ref(null)
+let diagramRendered = false
+
+// 空消息有效,但非空 Hex 必须是偶数位合法字符。
+function parseInput(data, format) {
+  if (format === 'string') return CryptoJS.enc.Utf8.parse(data)
+  if (format === 'hex') {
+    if (data && !/^(?:[0-9a-fA-F]{2})+$/.test(data)) {
+      throw new Error('Hex 格式错误:只能使用 0-9、a-f,且必须为偶数位')
+    }
+    return CryptoJS.enc.Hex.parse(data)
+  }
+  if (format === 'base64') {
+    const re = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
+    if (data && !re.test(data)) throw new Error('Base64 格式错误,请检查字符和填充符 =')
+    const parsed = CryptoJS.enc.Base64.parse(data)
+    // 严格检查规范编码,拒绝忽略了无效末尾比特的非规范 Base64。
+    if (parsed.toString(CryptoJS.enc.Base64) !== data) {
+      throw new Error('Base64 编码不规范')
+    }
+    return parsed
+  }
+  throw new Error('不支持的输入格式')
+}
+
+function calculateDigest() {
+  const messageBytes = parseInput(form.message, form.inputFormat)
+  const saltBytes = CryptoJS.enc.Utf8.parse(form.salt)
+  const input = !form.salt ? messageBytes : form.concatMethod === 'prefix'
+      ? saltBytes.clone().concat(messageBytes.clone())
+      : messageBytes.clone().concat(saltBytes)
+  const digest = CryptoJS.SHA1(input)
+  return digest.toString(form.outputFormat === 'base64' ? CryptoJS.enc.Base64 : CryptoJS.enc.Hex)
+}
+
+function normalizeExpected(value) {
+  if (form.outputFormat === 'hex') {
+    if (!/^[0-9a-fA-F]{40}$/.test(value)) throw new Error('SHA-1 Hex 摘要必须恰好为 40 位十六进制字符')
+    return value.toLowerCase()
+  }
+  if (!/^[A-Za-z0-9+/]{27}=$/.test(value)) throw new Error('SHA-1 Base64 摘要必须为 28 位规范编码')
+  const bytes = CryptoJS.enc.Base64.parse(value)
+  if (bytes.sigBytes !== 20 || bytes.toString(CryptoJS.enc.Base64) !== value) {
+    throw new Error('SHA-1 Base64 摘要内容或编码不合法')
+  }
+  return value
+}
+
+function handleCalculate() {
+  try {
+    const digest = calculateDigest()
+    form.result = digest
+    form.expected = digest
+    verifyState.value = null
+    ElMessage.success('摘要计算成功,已同步到待验证摘要输入框')
+  } catch (error) {
+    form.result = ''
+    verifyState.value = null
+    ElMessage.error(error.message || '计算失败')
+  }
+}
+function handleVerify() {
+  verifyState.value = null
+  try {
+    const expected = normalizeExpected(form.expected)
+    const actual = calculateDigest()
+    const ok = actual === expected
+    verifyState.value = { ok, message: ok ? '验证通过:当前输入的摘要与计算结果一致' : '验证失败:待验证摘要与计算结果不一致' }
+    if (ok) ElMessage.success('摘要匹配')
+    else ElMessage.error('摘要不匹配')
+  } catch (error) {
+    ElMessage.error(error.message || '验证失败')
+  }
+}
+function handleClear() {
+  form.message = ''
+  form.salt = ''
+  form.result = ''
+  form.expected = ''
+  verifyState.value = null
+}
+watch(() => [form.message, form.inputFormat, form.salt, form.concatMethod, form.outputFormat], () => {
+  form.result = ''
+  form.expected = ''
+  verifyState.value = null
+})
+watch(() => form.expected, () => { verifyState.value = null })
+
+async function copyResult() {
+  if (!form.result) return ElMessage.warning('请先计算摘要')
+  try {
+    await navigator.clipboard.writeText(form.result)
+    ElMessage.success('已复制到剪贴板')
+  } catch (error) {
+    ElMessage.warning('复制失败,请手动选择结果复制(可尝试 HTTPS 或 localhost)')
+  }
+}
+
+async function renderMermaid() {
+  if (diagramRendered) return
+  await nextTick()
+  if (!mermaidContainer.value) return
+  const chart = `flowchart TD
+    A[输入消息字节] --> B[填充到512位块]
+    B --> C[附加64位消息长度]
+    C --> D[初始化 H0-H4]
+    D --> E[按块扩展消息字到 W0-W79]
+    E --> F[执行80轮压缩]
+    F --> G{还有消息块?}
+    G -->|是| E
+    G -->|否| H[输出160位 SHA-1 摘要]`
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    const { svg } = await mermaid.render('sha1-flow-' + Math.random().toString(36).slice(2), chart)
+    if (mermaidContainer.value) {
+      mermaidContainer.value.innerHTML = svg
+      diagramRendered = true
+    }
+  } catch (error) {
+    console.error('SHA-1 流程图渲染失败', error)
+    if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图暂时无法显示,请参考上方文字说明。'
+  }
+}
+function onTabChange(name) { if (name === 'principle') renderMermaid() }
+onMounted(() => { if (activeTab.value === 'principle') renderMermaid() })
+</script>
+
+<style scoped>
+.sha1-container {  min-height: 100vh; }
+.box-card { border-radius: 8px; }
+.security-note { margin-bottom: 16px; }
+.principle-content, .code-content { line-height: 1.8; color: #606266; }
+.principle-content h3, .code-content h3 { color: #303133; }
+.flowchart-placeholder { margin-top: 20px; padding: 20px; min-height: 100px; overflow-x: auto; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 4px; }
+.flowchart-placeholder :deep(svg) { max-width: 100%; height: auto; }
+pre { background: #282c34; color: #e4e7ed; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 13px; line-height: 1.6; }
+code { font-family: Consolas, Monaco, monospace; }
+.calc-form { max-width: 800px; margin: 20px auto 0; }
+.button-group { display: flex; flex-wrap: wrap; gap: 12px; width: 100%; }
+.button-group .el-button { margin-left: 0; }
+.action-item { margin-top: 20px; }
+@media (max-width: 600px) { .sha1-container { padding: 8px; } .calc-form :deep(.el-form-item) { display: block; } .calc-form :deep(.el-form-item__label) { display: block; width: auto !important; text-align: left; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
+</style>

+ 245 - 0
src/view/sysApplication/experiment/hashAlgorithm/Sha256View.vue

@@ -0,0 +1,245 @@
+<template>
+  <div class="sha256-container">
+    <el-card class="box-card" shadow="never">
+      <template #header><strong>SHA-256 算法原理实现与验证</strong></template>
+      <el-alert title="安全提示:SHA-256 是广泛使用的安全哈希算法,但不是加密或消息认证;若需消息认证请使用 HMAC-SHA256,密码存储请使用 Argon2id、scrypt 等专用方案。" type="info" :closable="false" show-icon class="security-note" />
+      <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>SHA-256 算法工作原理</h3>
+            <p>SHA-256 是输出固定 256 位(32 字节)摘要的哈希函数;它不需要密钥,也不是加密算法。目前未发现针对完整 SHA-256 的实用碰撞攻击;但哈希摘要不提供消息来源认证,也不适合直接存储用户密码。</p>
+            <ol>
+              <li>按字节处理消息,在比特末尾填充 1 和若干个 0,使其长度模 512 等于 448。</li>
+              <li>追加原消息的 64 位大端比特长度,得到 512 位整数倍的消息块。</li>
+              <li>初始化八个 32 位状态字 H0~H7。</li>
+              <li>每个消息块扩展为 64 个 32 位字并进行 64 轮压缩。</li>
+              <li>拼接八个状态字,输出 256 位摘要。</li>
+            </ol>
+            <p><strong>关于盐值:</strong>本页面的“盐值”仅做简单的文本字节前后拼接,不是 HMAC,也不是安全密码哈希算法。</p>
+            <div ref="mermaidContainer" class="flowchart-placeholder" aria-label="SHA-256 哈希流程图"></div>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现(CryptoJS)</h3>
+            <p>将输入按 UTF-8、Hex 或 Base64 解码为字节序列;盐值始终使用 UTF-8,然后计算 SHA-256:</p>
+            <pre><code>import CryptoJS from 'crypto-js'
+
+// messageBytes 是严格解析输入编码后的 WordArray
+const saltBytes = CryptoJS.enc.Utf8.parse(salt)
+const input = salt
+  ? (prefix
+      ? saltBytes.clone().concat(messageBytes.clone())
+      : messageBytes.clone().concat(saltBytes))
+  : messageBytes
+const digest = CryptoJS.SHA256(input)
+const hex = digest.toString(CryptoJS.enc.Hex)</code></pre>
+            <p>验证时重新计算摘要,并与用户输入的期望摘要比较;这只检查摘要是否一致,并不验证消息来源身份。</p>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="计算与校验" name="calculation">
+          <el-form :model="form" label-width="110px" class="calc-form">
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="4" :placeholder="form.inputFormat === 'string' ? '输入文本;空字符串也是合法测试输入' : '输入严格编码的数据(可留空代表空字节串)'" />
+            </el-form-item>
+            <el-form-item label="盐值(可选)">
+              <el-input v-model="form.salt" placeholder="按 UTF-8 编码拼接,不属于安全密码存储" clearable />
+            </el-form-item>
+            <el-form-item label="拼接方式">
+              <el-select v-model="form.concatMethod" style="width: 100%" :disabled="!form.salt">
+                <el-option label="原文 + 盐值" value="suffix" />
+                <el-option label="盐值 + 原文" value="prefix" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="输出格式">
+              <el-radio-group v-model="form.outputFormat">
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="计算结果">
+              <el-input v-model="form.result" readonly placeholder="点击“计算摘要”获取结果">
+                <template #append><el-button :icon="CopyDocument" aria-label="复制摘要" @click="copyResult" /></template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="待验证摘要">
+              <el-input v-model="form.expected" placeholder="输入或粘贴外部 SHA-256 摘要进行验证" clearable />
+            </el-form-item>
+            <el-form-item label-width="0" class="action-item">
+              <div class="button-group">
+                <el-button type="primary" :icon="Operation" @click="handleCalculate">计算摘要</el-button>
+                <el-button type="success" :icon="CircleCheck" @click="handleVerify">验证摘要</el-button>
+                <el-button :icon="RefreshRight" @click="handleClear">清空</el-button>
+              </div>
+            </el-form-item>
+            <el-alert v-if="verifyState" :title="verifyState.message" :type="verifyState.ok ? 'success' : 'error'" :closable="false" show-icon />
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { CopyDocument, Operation, CircleCheck, RefreshRight } from '@element-plus/icons-vue'
+import CryptoJS from 'crypto-js'
+import mermaid from 'mermaid'
+
+const activeTab = ref('calculation')
+const form = reactive({
+  inputFormat: 'string', message: '', salt: '', concatMethod: 'suffix',
+  outputFormat: 'hex', result: '', expected: ''
+})
+const verifyState = ref(null)
+const mermaidContainer = ref(null)
+let diagramRendered = false
+
+// 空消息有效,但非空 Hex 必须是偶数位合法字符。
+function parseInput(data, format) {
+  if (format === 'string') return CryptoJS.enc.Utf8.parse(data)
+  if (format === 'hex') {
+    if (data && !/^(?:[0-9a-fA-F]{2})+$/.test(data)) {
+      throw new Error('Hex 格式错误:只能使用 0-9、a-f,且必须为偶数位')
+    }
+    return CryptoJS.enc.Hex.parse(data)
+  }
+  if (format === 'base64') {
+    const re = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
+    if (data && !re.test(data)) throw new Error('Base64 格式错误,请检查字符和填充符 =')
+    const parsed = CryptoJS.enc.Base64.parse(data)
+    // 严格检查规范编码,拒绝忽略了无效末尾比特的非规范 Base64。
+    if (parsed.toString(CryptoJS.enc.Base64) !== data) {
+      throw new Error('Base64 编码不规范')
+    }
+    return parsed
+  }
+  throw new Error('不支持的输入格式')
+}
+
+function calculateDigest() {
+  const messageBytes = parseInput(form.message, form.inputFormat)
+  const saltBytes = CryptoJS.enc.Utf8.parse(form.salt)
+  const input = !form.salt ? messageBytes : form.concatMethod === 'prefix'
+      ? saltBytes.clone().concat(messageBytes.clone())
+      : messageBytes.clone().concat(saltBytes)
+  const digest = CryptoJS.SHA256(input)
+  return digest.toString(form.outputFormat === 'base64' ? CryptoJS.enc.Base64 : CryptoJS.enc.Hex)
+}
+
+function normalizeExpected(value) {
+  if (form.outputFormat === 'hex') {
+    if (!/^[0-9a-fA-F]{64}$/.test(value)) throw new Error('SHA-256 Hex 摘要必须恰好为 64 位十六进制字符')
+    return value.toLowerCase()
+  }
+  if (!/^[A-Za-z0-9+/]{43}=$/.test(value)) throw new Error('SHA-256 Base64 摘要必须为 44 位规范编码')
+  const bytes = CryptoJS.enc.Base64.parse(value)
+  if (bytes.sigBytes !== 32 || bytes.toString(CryptoJS.enc.Base64) !== value) {
+    throw new Error('SHA-256 Base64 摘要内容或编码不合法')
+  }
+  return value
+}
+
+function handleCalculate() {
+  try {
+    const digest = calculateDigest()
+    form.result = digest
+    form.expected = digest
+    verifyState.value = null
+    ElMessage.success('摘要计算成功,已同步到待验证摘要输入框')
+  } catch (error) {
+    form.result = ''
+    verifyState.value = null
+    ElMessage.error(error.message || '计算失败')
+  }
+}
+function handleVerify() {
+  verifyState.value = null
+  try {
+    const expected = normalizeExpected(form.expected)
+    const actual = calculateDigest()
+    const ok = actual === expected
+    verifyState.value = { ok, message: ok ? '验证通过:当前输入的摘要与计算结果一致' : '验证失败:待验证摘要与计算结果不一致' }
+    if (ok) ElMessage.success('摘要匹配')
+    else ElMessage.error('摘要不匹配')
+  } catch (error) {
+    ElMessage.error(error.message || '验证失败')
+  }
+}
+function handleClear() {
+  form.message = ''
+  form.salt = ''
+  form.result = ''
+  form.expected = ''
+  verifyState.value = null
+}
+watch(() => [form.message, form.inputFormat, form.salt, form.concatMethod, form.outputFormat], () => {
+  form.result = ''
+  form.expected = ''
+  verifyState.value = null
+})
+watch(() => form.expected, () => { verifyState.value = null })
+
+async function copyResult() {
+  if (!form.result) return ElMessage.warning('请先计算摘要')
+  try {
+    await navigator.clipboard.writeText(form.result)
+    ElMessage.success('已复制到剪贴板')
+  } catch (error) {
+    ElMessage.warning('复制失败,请手动选择结果复制(可尝试 HTTPS 或 localhost)')
+  }
+}
+
+async function renderMermaid() {
+  if (diagramRendered) return
+  await nextTick()
+  if (!mermaidContainer.value) return
+  const chart = `flowchart TD
+    A[输入消息字节] --> B[填充到512位块]
+    B --> C[附加64位消息长度]
+    C --> D[初始化 H0-H7]
+    D --> E[按块扩展消息字到 W0-W63]
+    E --> F[执行64轮压缩]
+    F --> G{还有消息块?}
+    G -->|是| E
+    G -->|否| H[输出256位 SHA-256 摘要]`
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    const { svg } = await mermaid.render('sha256-flow-' + Math.random().toString(36).slice(2), chart)
+    if (mermaidContainer.value) {
+      mermaidContainer.value.innerHTML = svg
+      diagramRendered = true
+    }
+  } catch (error) {
+    console.error('SHA-256 流程图渲染失败', error)
+    if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图暂时无法显示,请参考上方文字说明。'
+  }
+}
+function onTabChange(name) { if (name === 'principle') renderMermaid() }
+onMounted(() => { if (activeTab.value === 'principle') renderMermaid() })
+</script>
+
+<style scoped>
+.sha256-container { min-height: 100vh; }
+.box-card {  border-radius: 8px; }
+.security-note { margin-bottom: 16px; }
+.principle-content, .code-content { line-height: 1.8; color: #606266; }
+.principle-content h3, .code-content h3 { color: #303133; }
+.flowchart-placeholder { margin-top: 20px; padding: 20px; min-height: 100px; overflow-x: auto; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 4px; }
+.flowchart-placeholder :deep(svg) { max-width: 100%; height: auto; }
+pre { background: #282c34; color: #e4e7ed; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 13px; line-height: 1.6; }
+code { font-family: Consolas, Monaco, monospace; }
+.calc-form { max-width: 800px; margin: 20px auto 0; }
+.button-group { display: flex; flex-wrap: wrap; gap: 12px; width: 100%; }
+.button-group .el-button { margin-left: 0; }
+.action-item { margin-top: 20px; }
+@media (max-width: 600px) { .sha256-container { padding: 8px; } .calc-form :deep(.el-form-item) { display: block; } .calc-form :deep(.el-form-item__label) { display: block; width: auto !important; text-align: left; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
+</style>

+ 202 - 0
src/view/sysApplication/experiment/index.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="experiment-container">
+    <!-- 左侧边栏 -->
+    <div class="experiment-sidebar">
+      <el-menu
+        default-active="aes"
+        :default-openeds="['classical-crypto', 'modern-crypto']"
+        class="experiment-menu"
+        background-color="#ffffff"
+        text-color="#303133"
+        active-text-color="#409eff"
+        @select="handleSelect"
+      >
+        <el-sub-menu index="classical-crypto">
+          <template #title>
+            <el-icon><Document /></el-icon>
+            <span>古典密码</span>
+          </template>
+          <el-menu-item
+            v-for="algorithm in classicalAlgorithms"
+            :key="algorithm.name"
+            :index="algorithm.name"
+          >
+            {{ algorithm.label }}
+          </el-menu-item>
+        </el-sub-menu>
+        <el-sub-menu index="modern-crypto">
+          <template #title>
+            <el-icon><Lock /></el-icon>
+            <span>现代密码</span>
+          </template>
+          <el-menu-item
+            v-for="algorithm in modernAlgorithms"
+            :key="algorithm.name"
+            :index="algorithm.name"
+          >
+            {{ algorithm.label }}
+          </el-menu-item>
+        </el-sub-menu>
+      </el-menu>
+    </div>
+
+    <!-- 右侧内容区域 -->
+    <div class="experiment-content">
+
+      <div class="content-body">
+        <div v-if="selectedAlgorithm" class="experiment-component">
+          <component :is="selectedAlgorithm.component" />
+        </div>
+
+        <!-- 其他实验模块占位符 -->
+        <div v-else class="experiment-placeholder">
+          <el-empty description="请从左侧选择实验模块">
+          </el-empty>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { Lock, Document } from '@element-plus/icons-vue'
+import AesView from './views/AesView.vue'
+import CaesarView from './views/CaesarView.vue'
+import DesView from './views/DesView.vue'
+import HmacView from './views/HmacView.vue'
+import RailView from './views/RailView.vue'
+import RsaEnc from './views/RsaEnc.vue'
+import RsaSign from './views/RsaSign.vue'
+import Sha1View from './views/Sha1View.vue'
+import Sha256View from './views/Sha256View.vue'
+import Sm2EncView from './views/Sm2EncView.vue'
+import Sm2SignView from './views/Sm2SignView.vue'
+import Sm4View from './views/Sm4View.vue'
+import VigenereView from './views/VigenereView.vue'
+
+const currentActive = ref('aes')
+
+const classicalAlgorithms = [
+  { name: 'caesar', label: '凯撒密码', component: CaesarView },
+  { name: 'vigenere', label: '维吉尼亚密码', component: VigenereView },
+  { name: 'rail', label: '栅栏密码', component: RailView }
+]
+
+const modernAlgorithms = [
+  { name: 'aes', label: 'AES', component: AesView },
+  { name: 'des', label: 'DES', component: DesView },
+  { name: 'sm4', label: 'SM4', component: Sm4View },
+  { name: 'rsa-encryption', label: 'RSA 加解密', component: RsaEnc },
+  { name: 'rsa-signature', label: 'RSA 签名验签', component: RsaSign },
+  { name: 'sm2-encryption', label: 'SM2 加解密', component: Sm2EncView },
+  { name: 'sm2-signature', label: 'SM2 签名验签', component: Sm2SignView },
+  { name: 'sha1', label: 'SHA-1', component: Sha1View },
+  { name: 'sha256', label: 'SHA-256', component: Sha256View },
+  { name: 'hmac', label: 'HMAC', component: HmacView }
+]
+
+const selectedAlgorithm = computed(() =>
+  [...classicalAlgorithms, ...modernAlgorithms].find(
+    (algorithm) => algorithm.name === currentActive.value
+  )
+)
+
+const handleSelect = (key) => {
+  currentActive.value = key
+}
+</script>
+
+<style scoped>
+.experiment-container {
+  display: flex;
+  height: 100%;
+  background-color: #f5f7fa;
+}
+
+.experiment-sidebar {
+  width: 200px;
+  background-color: #ffffff;
+  border: 1px solid #dcdfe6;
+  box-sizing: border-box;
+  border-radius: 8px;
+
+}
+
+.experiment-menu {
+  border-radius: 8px;
+  height: 80%;
+}
+
+.experiment-menu :deep(.el-menu-item),
+.experiment-menu :deep(.el-sub-menu__title) {
+  transition: background-color 0.2s, color 0.2s;
+}
+
+.experiment-menu :deep(.el-menu-item:hover),
+.experiment-menu :deep(.el-sub-menu__title:hover) {
+  color: #409eff;
+  background-color: #ecf5ff;
+}
+
+.experiment-content {
+  flex: 1;
+  min-width: 0;
+  overflow-y: auto;
+}
+
+.content-header h2 {
+  margin: 0 0 10px 0;
+  color: #303133;
+  font-size: 24px;
+}
+
+.content-header p {
+  margin: 0;
+  color: #606266;
+  font-size: 14px;
+}
+
+.content-body {
+  background: white;
+  margin: 0px 5px 0px 5px;
+  min-height: 500px;
+  min-width: 0;
+}
+
+.experiment-placeholder {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 300px;
+}
+
+.experiment-component {
+  width: 100%;
+  height: 100%;
+}
+
+.experiment-component :deep(.box-card) {
+  width: 100%;
+  max-width: none;
+  margin: 0;
+  box-sizing: border-box;
+}
+
+.experiment-component :deep(.aes-container),
+.experiment-component :deep(.caesar-container),
+.experiment-component :deep(.des3-container),
+.experiment-component :deep(.hmac-container),
+.experiment-component :deep(.rail-fence-container),
+.experiment-component :deep(.rsa-container),
+.experiment-component :deep(.rsa-sign-container),
+.experiment-component :deep(.sha1-container),
+.experiment-component :deep(.sha256-container),
+.experiment-component :deep(.sm2-container),
+.experiment-component :deep(.sm2-sign-container),
+.experiment-component :deep(.sm4-container),
+.experiment-component :deep(.vigenere-container) {
+  padding: 0 !important;
+}
+
+</style>

+ 432 - 0
src/view/sysApplication/experiment/symmetricKeyAlgorithm/AesView.vue

@@ -0,0 +1,432 @@
+<template>
+  <div class="aes-container">
+    <el-card class="box-card" shadow="never">
+      <template #header>
+        <div class="clearfix">
+          <span style="font-weight: bold; font-size: 16px;">AES 算法原理实现与验证</span>
+        </div>
+      </template>
+
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- 1. 原理说明 -->
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>AES 分组密码算法工作原理</h3>
+            <p>AES(Advanced Encryption Standard)是一种对称分组密码标准,用于替代 DES。它支持 128、192、256 位密钥长度,分组长度固定为 128 位(16 字节)。其核心结构基于替换-置换网络(SPN)。</p>
+
+            <h4>核心参数与运算步骤拆解(以 128 位密钥为例,10 轮迭代):</h4>
+            <ol>
+              <li><strong>初始轮密钥加(Add Round Key):</strong>将 128 位明文状态矩阵与第一轮子密钥进行按位异或。</li>
+              <li><strong>字节替换(SubBytes):</strong>利用非线性的 S 盒(Substitution Box),将状态矩阵中的每个字节替换为另一个字节,提供混淆特性。</li>
+              <li><strong>行移位(ShiftRows):</strong>将状态矩阵的每一行进行循环左移(第 0 行不移,第 1 行移 1 字节,依此类推),打破列的独立性。</li>
+              <li><strong>列混淆(MixColumns):</strong>在有限域 GF(2^8) 上对状态矩阵的每一列进行矩阵乘法运算,实现字节间的扩散(最后一轮不执行此步骤)。</li>
+              <li><strong>轮密钥加(Add Round Key):</strong>将当前状态矩阵与本轮子密钥异或。</li>
+              <li><strong>重复迭代:</strong>重复步骤 2-5,128 位密钥共进行 10 轮,最终输出 128 位密文。</li>
+            </ol>
+
+            <div class="flowchart-placeholder">
+
+              <div ref="mermaidContainer" class="mermaid">{{ diagramCode }}</div>
+            </div>
+          </div>
+        </el-tab-pane>
+
+        <!-- 2. 算法实现 -->
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现 (基于 CryptoJS)</h3>
+            <p>以下为前端核心计算逻辑代码,引入 <code>crypto-js</code> 库实现 AES 运算:</p>
+            <pre><code>
+// 引入 CryptoJS 库
+import CryptoJS from 'crypto-js';
+
+const msg = '待加密的明文消息';
+// AES 密钥支持 16/24/32 字节 (128/192/256位),IV 固定 16 字节
+const key = CryptoJS.enc.Hex.parse('00112233445566778899aabbccddeeff');
+const iv = CryptoJS.enc.Hex.parse('ffeeddccbbaa99887766554433221100');
+
+// 1. AES 加密 (支持 ECB/CBC 模式,支持 PKCS#7/NoPadding 填充)
+const encrypted = CryptoJS.AES.encrypt(msg, key, {
+  iv: iv,
+  mode: CryptoJS.mode.CBC,       // 'CBC' 或 'ECB'
+  padding: CryptoJS.pad.Pkcs7    // 'Pkcs7' 或 'NoPadding'
+});
+const ciphertextHex = encrypted.ciphertext.toString(CryptoJS.enc.Hex);
+
+// 2. AES 解密
+const decrypted = CryptoJS.AES.decrypt(
+  { ciphertext: CryptoJS.enc.Hex.parse(ciphertextHex) },
+  key,
+  { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
+);
+const plaintext = decrypted.toString(CryptoJS.enc.Utf8);
+            </code></pre>
+          </div>
+        </el-tab-pane>
+
+        <!-- 3. 计算与校验 -->
+        <el-tab-pane label="计算与校验" name="calculation">
+          <el-form :model="form" label-width="110px" class="calc-form">
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio label="string">字符串</el-radio>
+                <el-radio label="hex">十六进制</el-radio>
+                <el-radio label="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="消息原文" required>
+              <el-input
+                  type="textarea"
+                  v-model="form.message"
+                  :rows="3"
+                  placeholder="请输入消息原文">
+              </el-input>
+            </el-form-item>
+
+            <el-form-item label="密钥长度">
+              <el-select v-model="form.keyLength" placeholder="请选择密钥长度" style="width: 100%;">
+                <el-option label="128 位 (16字节)" value="128"></el-option>
+                <el-option label="192 位 (24字节)" value="192"></el-option>
+                <el-option label="256 位 (32字节)" value="256"></el-option>
+              </el-select>
+            </el-form-item>
+
+            <el-form-item label="密钥值 (Hex)" required>
+              <el-input v-model="form.key" :placeholder="`请输入 ${form.keyLength / 4} 位 Hex 字符`" :maxlength="form.keyLength / 4">
+                <template #append>
+                  <el-button :icon="Refresh" @click="generateKey">生成密钥</el-button>
+                </template>
+              </el-input>
+              <div class="form-tips">AES 密钥长度需与选择的密钥长度严格匹配,且只能输入十六进制字符</div>
+            </el-form-item>
+
+            <el-form-item label="加密模式">
+              <el-radio-group v-model="form.mode">
+                <el-radio label="ecb">ECB</el-radio>
+                <el-radio label="cbc">CBC</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="IV (初始向量)" v-if="form.mode === 'cbc'">
+              <el-input v-model="form.iv" placeholder="请输入 32 位 Hex 字符 (16字节)" maxlength="32">
+                <template #append><el-button :icon="Refresh" @click="generateIv">生成 IV</el-button></template>
+              </el-input>
+              <el-checkbox v-model="form.autoIv">加密时自动生成新 IV(推荐,标准向量测试时关闭)</el-checkbox>
+              <div class="form-tips">CBC 模式下 IV 长度固定为 16 字节(32 个 Hex 字符)</div>
+            </el-form-item>
+
+            <el-form-item label="填充方式">
+              <el-radio-group v-model="form.padding">
+                <el-radio label="pkcs7">PKCS#7</el-radio>
+                <el-radio label="nopadding">NO PADDING</el-radio>
+              </el-radio-group>
+              <div class="form-tips" v-if="form.padding === 'nopadding'" style="color: #E6A23C;">
+                注:NO PADDING 模式下,输入数据长度必须为 16 字节的整数倍。
+              </div>
+            </el-form-item>
+
+            <el-form-item label="输出格式">
+              <el-radio-group v-model="form.outputFormat">
+                <el-radio label="hex">十六进制</el-radio>
+                <el-radio label="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="密文值">
+              <el-input
+                  type="textarea"
+                  v-model="form.ciphertext"
+                  :rows="3"
+                  placeholder="加密结果 / 待解密输入的密文">
+              </el-input>
+            </el-form-item>
+
+            <el-form-item label="解密结果">
+              <el-input v-model="decryptedResult" type="textarea" :rows="3" readonly placeholder="解密结果在这里显示,不会覆盖原始消息" />
+              <div class="form-tips">ECB/CBC 不提供完整性认证;解密得到文本不代表密文未被篡改。</div>
+            </el-form-item>
+
+            <el-form-item label-width="0">
+              <div class="action-buttons">
+                <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="handleClear">清空数据</el-button>
+              </div>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Refresh, Lock, Unlock, Delete } from '@element-plus/icons-vue';
+import CryptoJS from 'crypto-js';
+import mermaid from 'mermaid';
+
+const activeTab = ref('calculation');
+
+const form = reactive({
+  inputFormat: 'string',
+  message: '',
+  keyLength: '128',
+  key: '',
+  mode: 'cbc',
+  padding: 'pkcs7',
+  iv: '',
+  autoIv: true,
+  ciphertext: '',
+  outputFormat: 'hex'
+});
+
+const mermaidContainer = ref(null);
+const decryptedResult = ref('');
+
+const diagramCode = ref(`
+flowchart TD
+    A[开始] --> B[输入128位明文 + 128位密钥]
+    B --> C[将明文排列为4x4状态矩阵 State]
+    C --> D[密钥扩展: 生成11个轮密钥 RK0~RK10]
+    D --> E[初始轮: AddRoundKey State XOR RK0]
+    E --> F[循环 i = 1 到 9]
+    F --> G[SubBytes: 每个字节用S盒替换]
+    G --> H[ShiftRows: 行移位]
+    H --> I[MixColumns: 列混合]
+    I --> J[AddRoundKey: State XOR RKi]
+    J --> K{是否 i == 9?}
+    K -- 否 --> F
+    K -- 是 --> L[最终轮]
+    L --> M[SubBytes: S盒替换]
+    M --> N[ShiftRows: 行移位]
+    N --> O[AddRoundKey: State XOR RK10]
+    O --> P[输出128位密文]
+    P --> Q[结束]
+`);
+
+// 流程图只在“原理说明”选项卡显示后渲染,避免隐藏容器尺寸为 0。
+let diagramRevision = 0;
+watch(activeTab, async (tab) => {
+  if (tab !== 'principle') return;
+  await nextTick();
+  const target = mermaidContainer.value;
+  if (!target) return;
+  const revision = ++diagramRevision;
+  try {
+    mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'strict' });
+    const { svg, bindFunctions } = await mermaid.render(`aes-flow-${revision}`, diagramCode.value);
+    if (activeTab.value !== 'principle' || target !== mermaidContainer.value) return;
+    target.innerHTML = svg;
+    bindFunctions?.(target);
+  } catch (error) {
+    console.error('Mermaid 渲染失败:', error);
+    target.textContent = '流程图渲染失败,请检查 Mermaid 版本与控制台错误';
+  }
+});
+
+const hexPattern = /^(?:[0-9a-fA-F]{2})+$/;
+const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
+
+const parseInput = (data, format) => {
+  if (format === 'hex') {
+    if (!hexPattern.test(data)) throw new Error('Hex 必须由偶数个合法十六进制字符组成');
+    return CryptoJS.enc.Hex.parse(data);
+  }
+  if (format === 'base64') {
+    if (!data || !base64Pattern.test(data)) throw new Error('Base64 格式不合法');
+    return CryptoJS.enc.Base64.parse(data);
+  }
+  return CryptoJS.enc.Utf8.parse(data);
+};
+
+const validateKeyAndIv = () => {
+  const keySize = Number(form.keyLength) / 4;
+  if (!Number.isInteger(keySize) || ![32, 48, 64].includes(keySize)) {
+    throw new Error('请选择有效的 AES 密钥长度');
+  }
+  if (!new RegExp(`^[0-9a-fA-F]{${keySize}}$`).test(form.key)) {
+    throw new Error(`密钥必须是 ${keySize} 位合法 Hex 字符`);
+  }
+  if (form.mode === 'cbc' && !/^[0-9a-fA-F]{32}$/.test(form.iv)) {
+    throw new Error('CBC 模式 IV 必须是 32 位合法 Hex 字符');
+  }
+};
+
+const generateIv = () => {
+  form.iv = CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Hex);
+  decryptedResult.value = '';
+};
+
+const generateKey = () => {
+  try {
+    form.key = CryptoJS.lib.WordArray.random(Number(form.keyLength) / 8).toString(CryptoJS.enc.Hex);
+    if (form.mode === 'cbc') generateIv();
+    decryptedResult.value = '';
+    ElMessage.success(form.mode === 'cbc' ? '密钥和 IV 生成成功' : '密钥生成成功');
+  } catch (error) {
+    ElMessage.error(error.message || '密钥生成失败');
+  }
+};
+
+const cipherOptions = (padding = CryptoJS.pad.NoPadding) => ({
+  mode: form.mode === 'cbc' ? CryptoJS.mode.CBC : CryptoJS.mode.ECB,
+  padding,
+  ...(form.mode === 'cbc' ? { iv: CryptoJS.enc.Hex.parse(form.iv) } : {})
+});
+
+// CryptoJS 的 Pkcs7.unpad 并不检查填充值是否合法,因此使用 NoPadding
+// 先获得完整解密分组,然后对末尾 PKCS#7 字节进行严格校验。
+const getByte = (words, offset) =>
+    (words.words[offset >>> 2] >>> (24 - (offset % 4) * 8)) & 0xff;
+
+const stripPkcs7 = (words) => {
+  const length = words.sigBytes;
+  if (length < 16 || length % 16 !== 0) throw new Error('PKCS#7 密文分组长度错误');
+  const padCount = getByte(words, length - 1);
+  if (padCount < 1 || padCount > 16) throw new Error('PKCS#7 填充不合法(密钥、IV 或密文可能有误)');
+  let invalid = 0;
+  for (let i = 0; i < 16; i++) {
+    const value = getByte(words, length - 1 - i);
+    if (i < padCount) invalid |= value ^ padCount;
+  }
+  if (invalid !== 0) throw new Error('PKCS#7 填充不合法(密钥、IV 或密文可能有误)');
+  words.sigBytes = length - padCount;
+  words.clamp();
+  return words;
+};
+
+const showError = (prefix, error) => {
+  ElMessage.error(`${prefix}:${error?.message || '未知错误'}`);
+  console.error(prefix, error);
+};
+
+const handleEncrypt = () => {
+  try {
+    if (!form.message) throw new Error('请输入消息原文');
+    // 固定 IV 可用于标准向量验证;日常加密使用随机 IV。
+    if (form.mode === 'cbc' && form.autoIv) generateIv();
+    validateKeyAndIv();
+    const message = parseInput(form.message, form.inputFormat);
+    if (form.padding === 'nopadding' && message.sigBytes % 16 !== 0) {
+      throw new Error('NoPadding 模式要求明文字节长度是 16 的整数倍');
+    }
+    const encrypted = CryptoJS.AES.encrypt(
+        message, CryptoJS.enc.Hex.parse(form.key),
+        cipherOptions(form.padding === 'pkcs7' ? CryptoJS.pad.Pkcs7 : CryptoJS.pad.NoPadding)
+    );
+    form.ciphertext = encrypted.ciphertext.toString(
+        form.outputFormat === 'base64' ? CryptoJS.enc.Base64 : CryptoJS.enc.Hex
+    );
+    decryptedResult.value = '';
+    ElMessage.success('加密计算成功');
+  } catch (error) {
+    showError('加密失败', error);
+  }
+};
+
+const handleDecrypt = () => {
+  try {
+    if (!form.ciphertext) throw new Error('请输入密文值');
+    validateKeyAndIv();
+    const ciphertext = parseInput(form.ciphertext, form.outputFormat);
+    if (!ciphertext.sigBytes || ciphertext.sigBytes % 16 !== 0) {
+      throw new Error('AES 密文长度必须是 16 字节的正整数倍');
+    }
+    const decrypted = CryptoJS.AES.decrypt(
+        { ciphertext }, CryptoJS.enc.Hex.parse(form.key), cipherOptions()
+    );
+    if (decrypted.sigBytes !== ciphertext.sigBytes) {
+      throw new Error('解密结果长度异常');
+    }
+    const data = form.padding === 'pkcs7' ? stripPkcs7(decrypted) : decrypted;
+    const codec = form.inputFormat === 'hex' ? CryptoJS.enc.Hex
+        : form.inputFormat === 'base64' ? CryptoJS.enc.Base64 : CryptoJS.enc.Utf8;
+    decryptedResult.value = data.toString(codec);
+    if (decryptedResult.value === form.message && form.message !== '') {
+      ElMessage.success('解密结果与原消息一致(不代表通过完整性认证)');
+    } else {
+      ElMessage.warning('解密已完成;结果与原消息不同,或没有原消息可比对');
+    }
+  } catch (error) {
+    decryptedResult.value = '';
+    showError('解密失败', error);
+  }
+};
+
+const handleClear = () => {
+  form.message = '';
+  form.key = '';
+  form.iv = '';
+  form.ciphertext = '';
+  decryptedResult.value = '';
+  ElMessage.info('数据已清空');
+};
+</script>
+
+<style scoped>
+.aes-container {
+  min-height: 100vh;
+}
+.box-card {
+  border-radius: 8px;
+}
+.principle-content, .code-content {
+  line-height: 1.8;
+  color: #606266;
+}
+.principle-content h3, .code-content h3 {
+  color: #303133;
+  margin-bottom: 15px;
+}
+.flowchart-placeholder {
+  margin-top: 20px;
+  padding: 20px;
+  background-color: #fafafa;
+  border: 1px dashed #dcdfe6;
+  text-align: center;
+  color: #909399;
+  border-radius: 4px;
+}
+pre {
+  background-color: #282c34;
+  color: #abb2bf;
+  padding: 15px;
+  border-radius: 4px;
+  overflow-x: auto;
+  font-size: 14px;
+  line-height: 1.5;
+}
+code {
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+}
+.calc-form {
+  max-width: 850px;
+  margin: 20px auto 0;
+}
+/* 取消操作按钮区域的表单标签预留,并使用统一按钮间距 */
+.action-buttons {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  align-items: center;
+}
+.action-buttons .el-button + .el-button {
+  margin-left: 0;
+}
+.form-tips {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+  line-height: 1.5;
+}
+.flowchart-placeholder :deep(svg) { max-width: 100%; height: auto; }
+.action-buttons :deep(.el-button + .el-button) { margin-left: 0; }
+@media (max-width: 600px) {
+  .aes-container { padding: 8px; }
+  .calc-form { margin-top: 12px; }
+  .action-buttons { gap: 8px; }
+}
+</style>

+ 292 - 0
src/view/sysApplication/experiment/symmetricKeyAlgorithm/DesView.vue

@@ -0,0 +1,292 @@
+<template>
+  <div class="des3-container">
+    <el-card class="box-card" shadow="never">
+      <template #header><strong>DES / 3DES 加解密教学实验</strong></template>
+      <el-tabs v-model="activeTab" type="border-card">
+        <el-tab-pane label="原理说明" name="principle">
+          <section class="article">
+            <h3>DES 与 3DES 工作原理</h3>
+            <p>DES 是分组长度为 64 位、有效密钥长度为 56 位的对称分组密码;密钥通常按包含校验位的 8 字节形式输入。3DES(TDEA)以 EDE(加密-解密-加密)结构重复使用 DES,本实验使用 24 字节的三密钥形式。</p>
+            <p>ECB 对每个分组独立处理,会泄露重复明文的模式。CBC 先将明文分组与前一个密文分组(首块使用 IV)异或再加密,IV 为 8 字节;每次实际 CBC 加密应使用新的不可预测 IV。</p>
+            <p>PKCS#7 会补足 8 字节的整数倍,恰好整块时也会额外补一块;NoPadding 则要求明文字节数为 8 的整数倍。文本采用 UTF-8 编码,Hex 表示的是原始字节。</p>
+            <el-alert type="warning" show-icon :closable="false" title="仅用于教学" description="DES 已不安全,3DES 也是不再推荐用于新系统的旧算法。CBC/ECB + PKCS#7 不提供消息完整性认证;实际业务建议使用 AES-GCM 等认证加密方案。" />
+            <div class="diagram-wrap"><div ref="mermaidContainer" class="mermaid">{{ diagramCode }}</div></div>
+          </section>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <section class="article">
+            <h3>基于 CryptoJS 的核心调用</h3>
+            <p>下方是关键 API 示意。实际页面另外实施了严格的 Hex、Base64、分组长度和 PKCS#7 填充校验。</p>
+            <pre><code>import CryptoJS from 'crypto-js';
+
+const algorithm = CryptoJS.TripleDES; // DES 则使用 CryptoJS.DES
+const key = CryptoJS.enc.Hex.parse(keyHex); // 3DES: 24 字节
+const iv = CryptoJS.enc.Hex.parse(ivHex);   // CBC: 8 字节
+const plaintext = CryptoJS.enc.Utf8.parse('Hello');
+
+const encrypted = algorithm.encrypt(plaintext, key, {
+  iv,
+  mode: CryptoJS.mode.CBC,
+  padding: CryptoJS.pad.Pkcs7
+});
+const ciphertextHex = encrypted.ciphertext.toString(CryptoJS.enc.Hex);</code></pre>
+            <p>注意:CryptoJS 的解密接口本身不能证明密文来源可信;错误的密钥或篡改后的密文即使通过了填充检查,也可能产生无意义数据。</p>
+          </section>
+        </el-tab-pane>
+        <el-tab-pane label="加解密实验" name="calculation">
+          <el-form :model="form" label-width="110px" class="calc-form">
+            <el-form-item label="算法">
+              <el-radio-group v-model="form.algorithm" @change="onAlgorithmChange">
+                <el-radio value="des">DES(8 字节密钥)</el-radio>
+                <el-radio value="3des">3DES(24 字节密钥)</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="工作模式">
+              <el-radio-group v-model="form.mode" @change="clearResults">
+                <el-radio value="cbc">CBC</el-radio>
+                <el-radio value="ecb">ECB</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="填充方式">
+              <el-radio-group v-model="form.padding" @change="clearResults">
+                <el-radio value="pkcs7">PKCS#7</el-radio>
+                <el-radio value="none">NoPadding</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="密钥(Hex)" required>
+              <div class="field-row">
+                <el-input v-model="form.key" :placeholder="`输入 ${keyBytes * 2} 位十六进制字符`" clearable @input="clearResults" />
+                <el-button :icon="Refresh" @click="generateKey">生成密钥</el-button>
+              </div>
+              <div class="hint">{{ form.algorithm === 'des' ? 'DES 使用 8 字节(含校验位,安全强度仅 56 位)' : '本实验使用三密钥 3DES(共 24 字节)' }};请输入 {{ keyBytes * 2 }} 位 Hex。</div>
+            </el-form-item>
+            <el-form-item v-if="form.mode === 'cbc'" label="IV(Hex)" required>
+              <div class="field-row">
+                <el-input v-model="form.iv" placeholder="16 位十六进制字符(8 字节)" clearable @input="clearResults" />
+                <el-button :icon="Refresh" @click="generateIv">生成 IV</el-button>
+              </div>
+              <el-checkbox v-model="form.autoIv" @change="clearResults">加密时自动生成新 IV(推荐)</el-checkbox>
+              <div class="hint">解密时会使用当前显示的 IV,请与密文一起保存;固定 IV 仅用于复现教学测试向量。</div>
+            </el-form-item>
+            <el-form-item label="明文输入格式">
+              <el-radio-group v-model="form.inputFormat" @change="clearResults">
+                <el-radio value="utf8">UTF-8 文本</el-radio>
+                <el-radio value="hex">Hex 字节</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="明文" required>
+              <el-input v-model="form.plaintext" type="textarea" :rows="3" :placeholder="form.inputFormat === 'utf8' ? '输入普通文本(支持中文)' : '输入偶数位 Hex,例如 0123456789ABCDEF'" @input="clearResults" />
+            </el-form-item>
+            <el-form-item label="密文格式">
+              <el-radio-group v-model="form.cipherFormat" @change="onCipherFormatChange">
+                <el-radio value="hex">Hex</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="密文">
+              <el-input v-model="form.ciphertext" type="textarea" :rows="3" placeholder="加密结果,或粘贴待解密的密文" @input="onCiphertextInput" />
+            </el-form-item>
+            <el-form-item label="解密结果">
+              <el-input v-model="decryptedText" type="textarea" :rows="3" readonly placeholder="点击解密后显示结果,不会修改原明文" />
+              <div class="hint">{{ form.inputFormat === 'utf8' ? '按照 UTF-8 解码结果;无法解码时将提示改用 Hex。' : '按照 Hex 输出原始解密字节。' }}</div>
+            </el-form-item>
+            <el-form-item label-width="0">
+              <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="handleClear">清空数据</el-button>
+              </div>
+            </el-form-item>
+            <el-alert v-if="statusMessage" class="result-alert" :type="statusType" :closable="false" show-icon :title="statusMessage" />
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Refresh, Lock, Unlock, Delete } from '@element-plus/icons-vue';
+import CryptoJS from 'crypto-js';
+import mermaid from 'mermaid';
+
+const activeTab = ref('calculation');
+const mermaidContainer = ref(null);
+const diagramCode = `flowchart TD
+  A[输入字节、密钥、模式] --> B{CBC 模式?}
+  B -- 是 --> C[使用 8 字节 IV]
+  B -- 否 --> D[不使用 IV]
+  C --> E[PKCS7 填充或不填充]
+  D --> E
+  E --> F[DES 或 3DES 分组加密]
+  F --> G[输出 Hex 或 Base64 密文]
+  G --> H[按相同参数解密]
+`;
+let diagramRendered = false;
+let disposed = false;
+async function renderDiagram() {
+  if (diagramRendered || activeTab.value !== 'principle') return;
+  await nextTick();
+  const node = mermaidContainer.value;
+  if (!node || disposed) return;
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' });
+    const uniqueId = `mermaid-des3-${Math.random().toString(36).slice(2)}`;
+    const { svg, bindFunctions } = await mermaid.render(uniqueId, diagramCode);
+    if (disposed || mermaidContainer.value !== node) return;
+    node.innerHTML = svg;
+    bindFunctions?.(node);
+    diagramRendered = true;
+  } catch (error) {
+    console.error('流程图绘制失败:', error);
+    if (!disposed && mermaidContainer.value === node) node.textContent = '流程图渲染失败,请检查 Mermaid 版本。';
+  }
+}
+watch(activeTab, (tab) => { if (tab === 'principle') void renderDiagram(); }, { flush: 'post' });
+onBeforeUnmount(() => { disposed = true; });
+
+const form = reactive({
+  algorithm: '3des', mode: 'cbc', padding: 'pkcs7', key: '', iv: '', autoIv: true,
+  inputFormat: 'utf8', plaintext: 'Hello DES / 3DES!', cipherFormat: 'hex', ciphertext: ''
+});
+const keyBytes = computed(() => form.algorithm === 'des' ? 8 : 24);
+const decryptedText = ref('');
+const statusMessage = ref('');
+const statusType = ref('info');
+let lastCipherBytes = null;
+function clearResults() { decryptedText.value = ''; statusMessage.value = ''; lastCipherBytes = null; }
+function messageError(error) {
+  const text = error instanceof Error ? error.message : String(error);
+  statusMessage.value = text;
+  statusType.value = 'error';
+  ElMessage.error(text);
+}
+function requireHex(value, byteLength, name) {
+  if (typeof value !== 'string' || !/^(?:[0-9a-fA-F]{2})+$/.test(value) || (byteLength !== null && value.length !== byteLength * 2)) {
+    throw new Error(`${name}必须是${byteLength === null ? '非空、偶数位的' : `${byteLength * 2} 位`}合法 Hex 字符`);
+  }
+  return CryptoJS.enc.Hex.parse(value);
+}
+function wordArrayToBytes(wordArray) {
+  const bytes = new Uint8Array(wordArray.sigBytes);
+  for (let i = 0; i < bytes.length; i++) bytes[i] = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+  return bytes;
+}
+function bytesToWordArray(bytes) {
+  const words = [];
+  for (let i = 0; i < bytes.length; i++) words[i >>> 2] = (words[i >>> 2] || 0) | (bytes[i] << (24 - (i % 4) * 8));
+  return CryptoJS.lib.WordArray.create(words, bytes.length);
+}
+function parseBase64(value) {
+  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value) || !value) {
+    throw new Error('密文不是合法的标准 Base64');
+  }
+  const parsed = CryptoJS.enc.Base64.parse(value);
+  if (parsed.toString(CryptoJS.enc.Base64) !== value) throw new Error('Base64 编码不规范');
+  return parsed;
+}
+function readCiphertext() {
+  const str = form.ciphertext.trim();
+  const wa = form.cipherFormat === 'hex' ? requireHex(str, null, '密文') : parseBase64(str);
+  if (wa.sigBytes === 0 || wa.sigBytes % 8 !== 0) throw new Error('DES/3DES 密文字节数必须为非零的 8 的整数倍');
+  return wa;
+}
+function parameters() {
+  const key = requireHex(form.key.trim(), keyBytes.value, '密钥');
+  const iv = form.mode === 'cbc' ? requireHex(form.iv.trim(), 8, 'IV') : undefined;
+  return { key, iv, algorithm: form.algorithm === 'des' ? CryptoJS.DES : CryptoJS.TripleDES,
+    options: { mode: form.mode === 'cbc' ? CryptoJS.mode.CBC : CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding, ...(iv ? { iv } : {}) } };
+}
+function plaintextBytes() {
+  if (!form.plaintext) throw new Error('请输入明文');
+  return form.inputFormat === 'hex' ? requireHex(form.plaintext.trim(), null, '明文') : CryptoJS.enc.Utf8.parse(form.plaintext);
+}
+function addPadding(bytes) {
+  const padLen = 8 - (bytes.length % 8);
+  const padded = new Uint8Array(bytes.length + padLen);
+  padded.set(bytes);
+  padded.fill(padLen, bytes.length);
+  return padded;
+}
+function stripPadding(bytes) {
+  if (!bytes.length || bytes.length % 8 !== 0) throw new Error('解密结果长度不正确');
+  const padLen = bytes[bytes.length - 1];
+  if (padLen < 1 || padLen > 8) throw new Error('PKCS#7 填充无效:密钥、IV、模式或密文可能不匹配');
+  for (let i = bytes.length - padLen; i < bytes.length; i++) {
+    if (bytes[i] !== padLen) throw new Error('PKCS#7 填充无效:密钥、IV、模式或密文可能不匹配');
+  }
+  return bytes.slice(0, bytes.length - padLen);
+}
+function setCiphertext(wa) {
+  lastCipherBytes = wa.clone();
+  form.ciphertext = wa.toString(form.cipherFormat === 'hex' ? CryptoJS.enc.Hex : CryptoJS.enc.Base64);
+}
+function handleEncrypt() {
+  try {
+    const bytes = wordArrayToBytes(plaintextBytes());
+    if (form.padding === 'none' && bytes.length % 8 !== 0) throw new Error('NoPadding 模式要求明文字节数是 8 的整数倍');
+    if (form.mode === 'cbc' && form.autoIv) form.iv = CryptoJS.lib.WordArray.random(8).toString(CryptoJS.enc.Hex);
+    const { key, algorithm, options } = parameters();
+    const input = bytesToWordArray(form.padding === 'pkcs7' ? addPadding(bytes) : bytes);
+    const encrypted = algorithm.encrypt(input, key, options);
+    setCiphertext(encrypted.ciphertext);
+    decryptedText.value = '';
+    statusMessage.value = `加密完成,共 ${encrypted.ciphertext.sigBytes} 字节密文;请保存密钥${form.mode === 'cbc' ? '及本次 IV' : ''}。`;
+    statusType.value = 'success';
+    ElMessage.success('加密完成');
+  } catch (error) { clearResults(); messageError(error); }
+}
+function handleDecrypt() {
+  try {
+    const ciphertext = readCiphertext();
+    const { key, algorithm, options } = parameters();
+    const raw = algorithm.decrypt(CryptoJS.lib.CipherParams.create({ ciphertext }), key, options);
+    const resultBytes = form.padding === 'pkcs7' ? stripPadding(wordArrayToBytes(raw)) : wordArrayToBytes(raw);
+    if (form.inputFormat === 'hex') {
+      decryptedText.value = bytesToWordArray(resultBytes).toString(CryptoJS.enc.Hex);
+    } else {
+      try { decryptedText.value = new TextDecoder('utf-8', { fatal: true }).decode(resultBytes); }
+      catch { throw new Error('解密结果不是合法 UTF-8,请切换“明文输入格式”为 Hex 查看原始字节,或核对密钥与参数'); }
+    }
+    statusMessage.value = '解密计算完成;填充校验通过不等于密文经过真实性认证。';
+    statusType.value = 'success';
+    ElMessage.success('解密完成');
+  } catch (error) { decryptedText.value = ''; messageError(error); }
+}
+function generateKey() { form.key = CryptoJS.lib.WordArray.random(keyBytes.value).toString(CryptoJS.enc.Hex); clearResults(); }
+function generateIv() { form.iv = CryptoJS.lib.WordArray.random(8).toString(CryptoJS.enc.Hex); clearResults(); }
+function onAlgorithmChange() { form.key = ''; clearResults(); }
+function onCiphertextInput() { decryptedText.value = ''; statusMessage.value = ''; lastCipherBytes = null; }
+function onCipherFormatChange() {
+  if (lastCipherBytes) form.ciphertext = lastCipherBytes.toString(form.cipherFormat === 'hex' ? CryptoJS.enc.Hex : CryptoJS.enc.Base64);
+  else { form.ciphertext = ''; }
+  decryptedText.value = '';
+  statusMessage.value = '';
+}
+function handleClear() {
+  Object.assign(form, { algorithm: '3des', mode: 'cbc', padding: 'pkcs7', key: '', iv: '', autoIv: true,
+    inputFormat: 'utf8', plaintext: '', cipherFormat: 'hex', ciphertext: '' });
+  clearResults();
+}
+</script>
+
+<style scoped>
+.des3-container { min-height: 100vh; }
+.box-card {  border-radius: 8px; }
+.article { color: #606266; line-height: 1.8; }
+.article h3 { color: #303133; }
+.article pre { padding: 18px; background: #282c34; color: #e0e4eb; border-radius: 6px; overflow-x: auto; line-height: 1.6; }
+.diagram-wrap { padding: 24px; margin-top: 20px; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 6px; overflow-x: auto; }
+.calc-form { max-width: 850px; margin: 22px auto 0; }
+.field-row { width: 100%; display: flex; flex-wrap: wrap; gap: 10px; }
+.field-row :deep(.el-input) { flex: 1 1 280px; min-width: 0; }
+.field-row :deep(.el-button) { margin-left: 0; }
+.hint { width: 100%; margin-top: 5px; color: #909399; font-size: 12px; line-height: 1.6; }
+.actions { display: flex; width: 100%; gap: 12px; flex-wrap: wrap; }
+.actions :deep(.el-button + .el-button) { margin-left: 0; }
+.result-alert { margin-top: 18px; }
+.mermaid { overflow-x: auto; }
+@media (max-width: 600px) { .des3-container { padding: 8px; } .calc-form :deep(.el-form-item__label) { width: 100% !important; justify-content: flex-start; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
+</style>

+ 266 - 0
src/view/sysApplication/experiment/symmetricKeyAlgorithm/Sm4View.vue

@@ -0,0 +1,266 @@
+<template>
+  <div class="sm4-container">
+    <el-card class="box-card" shadow="never">
+      <h3 class="page-title">SM4 算法原理实现与验证</h3>
+      <el-tabs v-model="activeTab" type="border-card">
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>SM4 分组密码算法工作原理</h3>
+            <p>SM4 是 128 位分组、128 位密钥的对称分组密码(GB/T 32907-2016),采用 32 轮迭代。ECB/CBC 是分组密码工作模式,PKCS#7 是填充方式。</p>
+            <ol>
+              <li>把 16 字节分组拆成四个 32 位字,扩展 128 位密钥得到 32 个轮密钥。</li>
+              <li>每轮计算 X(i+4) = X(i) XOR T(X(i+1) XOR X(i+2) XOR X(i+3) XOR rk(i))。</li>
+              <li>T 由 S 盒非线性变换和线性变换组成,完成 32 轮后逆序输出 X35、X34、X33、X32。</li>
+              <li>解密使用逆序轮密钥;CBC 每条消息需要新的、不可预测的 IV。</li>
+            </ol>
+            <el-alert type="warning" :closable="false" show-icon title="安全提示:ECB 会泄露重复分组模式;CBC 不提供完整性认证。该页面用于教学测试,实际业务优先采用经过审核的认证加密方案。" />
+            <div class="flowchart-placeholder"><div ref="mermaidContainer" class="mermaid-view">{{ diagramCode }}</div></div>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>核心示例(sm-crypto)</h3>
+            <p>下面的密钥、IV 和明文只用于演示;实际 CBC 加密应为每条消息随机生成 IV。</p>
+            <pre><code>{{ sampleCode }}</code></pre>
+            <p>本页面支持 UTF-8、Hex、Base64 明文输入,使用字节数组调用加密库,并在解密之后检查 PKCS#7 填充。</p>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="计算与校验" name="calculation">
+          <el-form :model="form" label-width="116px" class="calc-form">
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">Hex</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="3" placeholder="允许空明文(使用 PKCS#7 时)" />
+            </el-form-item>
+            <el-form-item label="密钥值 (Hex)">
+              <el-input v-model="form.key" placeholder="32 位十六进制字符(16 字节)">
+                <template #append><el-button :icon="Refresh" @click="generateKey">生成密钥</el-button></template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="加密模式">
+              <el-radio-group v-model="form.mode"><el-radio value="cbc">CBC(推荐演示)</el-radio><el-radio value="ecb">ECB(仅教学)</el-radio></el-radio-group>
+            </el-form-item>
+            <template v-if="form.mode === 'cbc'">
+              <el-form-item label="IV 生成策略">
+                <el-radio-group v-model="form.autoIv"><el-radio :value="true">每次加密随机生成</el-radio><el-radio :value="false">手动指定(测试向量)</el-radio></el-radio-group>
+              </el-form-item>
+              <el-form-item label="IV (Hex)">
+                <el-input v-model="form.iv" :readonly="form.autoIv" placeholder="32 位十六进制字符(16 字节)">
+                  <template #append><el-button :icon="Refresh" @click="generateIv">生成 IV</el-button></template>
+                </el-input>
+              </el-form-item>
+              <el-alert class="inline-alert" type="info" :closable="false" show-icon title="解密外部 CBC 密文时,请填入加密时使用的 IV;自动生成只在点击“加密计算”时生效。" />
+            </template>
+            <el-form-item label="填充方式">
+              <div class="field-stack">
+                <el-radio-group v-model="form.padding"><el-radio value="pkcs#7">PKCS#7</el-radio><el-radio value="none">NoPadding</el-radio></el-radio-group>
+                <span class="hint">NoPadding 要求明文字节数为 16 的整数倍;PKCS#7 允许空消息。</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="密文格式">
+              <el-radio-group v-model="form.outputFormat"><el-radio value="hex">Hex</el-radio><el-radio value="base64">Base64</el-radio></el-radio-group>
+            </el-form-item>
+            <el-form-item label="密文值">
+              <el-input v-model="form.ciphertext" type="textarea" :rows="3" placeholder="加密结果;也可以粘贴外部密文进行解密" />
+            </el-form-item>
+            <el-form-item label="解密结果">
+              <div class="field-stack result-wrap">
+                <el-input v-model="decryptedResult" type="textarea" :rows="3" readonly placeholder="解密后单独显示,不修改原消息" />
+                <span class="hint">按当前“输入格式”展示解密字节。UTF-8 格式遇到非文本字节会提示切换 Hex。</span>
+                <el-tag v-if="matchState === 'match'" type="success">与原始消息字节一致</el-tag>
+                <el-tag v-else-if="matchState === 'different'" type="warning">与原始消息不一致(仅供比较,并非认证)</el-tag>
+              </div>
+            </el-form-item>
+            <el-form-item label-width="0">
+              <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="handleClear">清空数据</el-button>
+              </div>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Refresh, Lock, Unlock, Delete } from '@element-plus/icons-vue'
+import smCrypto from 'sm-crypto'
+import mermaid from 'mermaid'
+
+const { sm4 } = smCrypto
+const activeTab = ref('calculation')
+const form = reactive({ inputFormat: 'string', message: '', key: '', mode: 'cbc', padding: 'pkcs#7', iv: '', autoIv: true, ciphertext: '', outputFormat: 'hex' })
+const decryptedResult = ref('')
+const matchState = ref('')
+const mermaidContainer = ref(null)
+const diagramCode = `flowchart TD
+  A[输入16字节数据块] --> B[分成4个32位字]
+  B --> C[密钥扩展得到32个轮密钥]
+  C --> D[迭代32轮非线性及线性变换]
+  D --> E[反序输出X35 X34 X33 X32]
+  E --> F[输出16字节数据块]`
+const sampleCode = `import smCrypto from 'sm-crypto'
+const { sm4 } = smCrypto
+const key = '0123456789abcdeffedcba9876543210'
+const iv  = 'fedcba98765432100123456789abcdef' // 演示用固定 IV
+const plaintext = Array.from(new TextEncoder().encode('你好 SM4'))
+const cipherHex = sm4.encrypt(plaintext, key, { mode: 'cbc', iv, padding: 'pkcs#7' })
+const restored = sm4.decrypt(cipherHex, key, { mode: 'cbc', iv, padding: 'pkcs#7', output: 'array' })
+console.log(new TextDecoder().decode(new Uint8Array(restored)))`
+
+const hexRegex = /^(?:[0-9a-fA-F]{2})*$/
+const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
+const hexToBytes = text => {
+  if (!hexRegex.test(text)) throw new Error('Hex 格式错误:请使用偶数位合法十六进制字符')
+  const bytes = []
+  for (let i = 0; i < text.length; i += 2) bytes.push(parseInt(text.slice(i, i + 2), 16))
+  return bytes
+}
+const bytesToHex = bytes => Array.from(bytes, x => x.toString(16).padStart(2, '0')).join('')
+const bytesToBase64 = bytes => {
+  let binary = ''
+  for (const byte of bytes) binary += String.fromCharCode(byte)
+  return btoa(binary)
+}
+const base64ToBytes = text => {
+  if (!base64Regex.test(text)) throw new Error('Base64 格式不合法')
+  const binary = atob(text)
+  const result = Array.from(binary, char => char.charCodeAt(0))
+  if (bytesToBase64(result) !== text) throw new Error('Base64 必须使用规范编码')
+  return result
+}
+const parseMessage = (value, format) => {
+  if (format === 'hex') return hexToBytes(value)
+  if (format === 'base64') return base64ToBytes(value)
+  return Array.from(new TextEncoder().encode(value))
+}
+const displayMessage = (bytes, format) => {
+  if (format === 'hex') return bytesToHex(bytes)
+  if (format === 'base64') return bytesToBase64(bytes)
+  return new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array(bytes))
+}
+const randomHex = size => {
+  if (!globalThis.crypto?.getRandomValues) throw new Error('当前浏览器不支持安全随机数,请使用 HTTPS 或 localhost')
+  const array = new Uint8Array(size)
+  globalThis.crypto.getRandomValues(array)
+  return bytesToHex(array)
+}
+const validateKey = () => {
+  if (!/^[0-9a-fA-F]{32}$/.test(form.key)) throw new Error('SM4 密钥必须是 32 位合法 Hex 字符')
+}
+const validateIv = () => {
+  if (form.mode === 'cbc' && !/^[0-9a-fA-F]{32}$/.test(form.iv)) throw new Error('CBC 模式 IV 必须是 32 位合法 Hex 字符')
+}
+const strictUnpad = bytes => {
+  if (!bytes.length || bytes.length % 16 !== 0) throw new Error('PKCS#7 解密数据长度不正确')
+  const n = bytes[bytes.length - 1]
+  if (n < 1 || n > 16) throw new Error('PKCS#7 填充无效(密钥、IV 或密文可能不匹配)')
+  for (let i = bytes.length - n; i < bytes.length; i++) {
+    if (bytes[i] !== n) throw new Error('PKCS#7 填充无效(密钥、IV 或密文可能不匹配)')
+  }
+  return bytes.slice(0, -n)
+}
+const notifyError = error => { console.error(error); ElMessage.error(error?.message || '操作失败') }
+const clearResult = () => { decryptedResult.value = ''; matchState.value = '' }
+watch(() => [form.message, form.inputFormat, form.key, form.mode, form.iv, form.padding, form.outputFormat, form.ciphertext], clearResult)
+const generateKey = () => {
+  try {
+    form.key = randomHex(16)
+    if (form.mode === 'cbc') form.iv = randomHex(16)
+    ElMessage.success('已生成 128 位随机密钥' + (form.mode === 'cbc' ? '及 IV' : ''))
+  } catch (error) { notifyError(error) }
+}
+const generateIv = () => {
+  try { form.iv = randomHex(16); ElMessage.success('已生成 128 位随机 IV') } catch (error) { notifyError(error) }
+}
+const handleEncrypt = () => {
+  try {
+    validateKey()
+    const bytes = parseMessage(form.message, form.inputFormat)
+    if (form.padding === 'none' && bytes.length % 16 !== 0) throw new Error('NoPadding 模式下明文字节数必须是 16 的整数倍')
+    if (form.mode === 'cbc' && form.autoIv) form.iv = randomHex(16)
+    validateIv()
+    const opts = { mode: form.mode, padding: form.padding }
+    if (form.mode === 'cbc') opts.iv = form.iv
+    // 用字节数组传入,避免 Hex / Base64 二进制输入被误当作 UTF-8。
+    const cipherHex = sm4.encrypt(bytes, form.key, opts)
+    form.ciphertext = form.outputFormat === 'base64' ? bytesToBase64(hexToBytes(cipherHex)) : cipherHex
+    clearResult()
+    ElMessage.success('加密成功(请同时保存密钥、模式、IV 与填充方式)')
+  } catch (error) { notifyError(error) }
+}
+const handleDecrypt = () => {
+  try {
+    validateKey()
+    validateIv()
+    const cipher = form.outputFormat === 'base64' ? base64ToBytes(form.ciphertext.trim()) : hexToBytes(form.ciphertext.trim())
+    if (!cipher.length || cipher.length % 16 !== 0) throw new Error('密文长度必须是非零的 16 字节整数倍')
+    const opts = { mode: form.mode, padding: 'none', output: 'array' }
+    if (form.mode === 'cbc') opts.iv = form.iv
+    const raw = sm4.decrypt(bytesToHex(cipher), form.key, opts)
+    if (!Array.isArray(raw) && !(raw instanceof Uint8Array)) throw new Error('解密接口没有返回字节数组')
+    const bytes = form.padding === 'pkcs#7' ? strictUnpad(Array.from(raw)) : Array.from(raw)
+    decryptedResult.value = displayMessage(bytes, form.inputFormat)
+    // 原消息可能故意为空;仍可比较确切的字节序列。
+    try {
+      const original = parseMessage(form.message, form.inputFormat)
+      matchState.value = bytesToHex(bytes) === bytesToHex(original) ? 'match' : 'different'
+    } catch (_) { matchState.value = '' }
+    ElMessage.success('解密计算完成(CBC/ECB 不提供真实性认证)')
+  } catch (error) { clearResult(); notifyError(error) }
+}
+const handleClear = () => {
+  form.message = ''; form.key = ''; form.iv = ''; form.ciphertext = ''
+  clearResult(); ElMessage.info('已清空数据')
+}
+
+let renderSequence = 0
+watch(activeTab, async tab => {
+  if (tab !== 'principle') return
+  const current = ++renderSequence
+  await nextTick()
+  if (!mermaidContainer.value) return
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    const { svg, bindFunctions } = await mermaid.render(`sm4-diagram-${current}`, diagramCode)
+    if (current === renderSequence && mermaidContainer.value) {
+      mermaidContainer.value.innerHTML = svg
+      bindFunctions?.(mermaidContainer.value)
+    }
+  } catch (error) {
+    console.error('Mermaid 渲染失败', error)
+    if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图渲染失败,请检查 Mermaid 版本或控制台提示'
+  }
+})
+onBeforeUnmount(() => { renderSequence++ })
+</script>
+
+<style scoped>
+.sm4-container { min-height: 100vh; }
+.box-card {  border-radius: 8px; }
+.page-title { font-size: 17px; margin: 2px 0 18px; }
+.principle-content, .code-content { line-height: 1.85; color: #606266; }
+.flowchart-placeholder { margin-top: 20px; padding: 20px; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 8px; overflow-x: auto; }
+.mermaid-view { min-width: 280px; }
+pre { background: #282c34; color: #e4e7ed; padding: 16px; border-radius: 6px; overflow-x: auto; line-height: 1.6; }
+code { font-family: Consolas, Monaco, monospace; }
+.calc-form { max-width: 860px; margin: 20px auto 0; }
+.field-stack { width: 100%; display: flex; flex-direction: column; gap: 8px; }
+.result-wrap { align-items: flex-start; }
+.result-wrap :deep(.el-textarea) { width: 100%; }
+.hint { font-size: 12px; color: #909399; line-height: 1.5; }
+.inline-alert { margin-bottom: 16px; }
+.actions { display: flex; flex-wrap: wrap; gap: 10px; width: 100%; }
+.actions :deep(.el-button) { margin-left: 0; }
+@media (max-width: 640px) { .sm4-container { padding: 8px; } .calc-form :deep(.el-form-item__label) { font-size: 12px; } }
+</style>