Appearance
实现 AES加解密 工具
分类:developer | 标签:AES、加密、解密 AES 对称加解密
2.1 功能说明
AES 对称加解密
2.1.1 使用指南
功能说明
使用对称加密算法对文本进行加密与解密,支持 AES、DES、TripleDES(3DES)、 RC4、Rabbit。纯浏览器本地计算,不上传任何数据。
算法说明
- AES:分组密码,当前主流标准,支持 128/192/256 位密钥。
- DES:分组密码,56 位有效密钥,已不安全,仅用于兼容老系统。
- TripleDES(3DES):对 DES 做三重加密,安全性优于 DES,性能较低。
- RC4 / Rabbit:流密码,逐字节生成密钥流异或,不使用分组模式、填充和 IV,因此这三个选项在选中它们时会自动禁用。
密钥方式
- 口令派生(OpenSSL 兼容):默认方式。口令经 EVP_BytesToKey 派生出密钥, 每次加密都会生成随机盐,因此同样的明文每次密文都不同,密文自带
Salted__头, 可与openssl enc互通。 - 密钥直接使用:把输入内容按 UTF-8 或 Hex 直接解析为密钥,无盐、结果确定, 便于与后端 Java/Go/Python 等实现对接。此时 AES 密钥必须是 16 / 24 / 32 字节, IV 必须是 16 字节(DES/3DES 为 8 字节)。
使用场景
敏感信息临时加密、与后端加解密逻辑联调对拍、教学演示。
注意事项
ECB 模式不需要 IV 且相同明文块会产生相同密文块,安全性较差,生产环境请优先使用 CBC。 解密时算法、模式、填充、IV、密钥方式与密文编码必须与加密时完全一致。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
parseBy()buildOptions()validate()encCfg()decCfg()encrypt()decrypt()copyC()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref } from 'vue'
import CryptoJS from 'crypto-js'
import ToolPageShell from '@/components/ToolPageShell.vue'
/* ---------------- 算法与选项定义 ---------------- */
const CIPHERS = {
AES: CryptoJS.AES,
DES: CryptoJS.DES,
TripleDES: CryptoJS.TripleDES,
RC4: CryptoJS.RC4,
Rabbit: CryptoJS.Rabbit
}
const ALGO_LIST = [
{ label: 'AES', value: 'AES' },
{ label: 'DES', value: 'DES' },
{ label: '3DES', value: 'TripleDES' },
{ label: 'RC4', value: 'RC4' },
{ label: 'Rabbit', value: 'Rabbit' }
]
const PAD_LIST = [
{ label: 'Pkcs7', value: 'Pkcs7' },
{ label: 'Iso97971', value: 'Iso97971' },
{ label: 'AnsiX923', value: 'AnsiX923' },
{ label: 'Iso10126', value: 'Iso10126' },
{ label: 'ZeroPadding', value: 'ZeroPadding' },
{ label: 'NoPadding', value: 'NoPadding' }
]
// RC4 / Rabbit 为流密码,不支持分组模式、填充与 IV
const STREAM_ALGOS = ['RC4', 'Rabbit']
const isStream = a => STREAM_ALGOS.includes(a)
// 分组长度(字节),即 CBC 模式下 IV 的要求长度
const BLOCK_SIZE = { AES: 16, DES: 8, TripleDES: 8 }
const ivPlaceholder = a => (isStream(a) ? '流密码不需要 IV' : `${BLOCK_SIZE[a]} 字节 IV`)
/* ---------------- 加密 tab 状态 ---------------- */
const mode = ref('enc')
const plain = ref('')
const key = ref('')
const cipherAlgo = ref('AES')
const blockMode = ref('CBC')
const padding = ref('Pkcs7')
const iv = ref('0000000000000000')
const keyMode = ref('passphrase')
const keyEnc = ref('utf8')
const cipherEnc = ref('base64')
const cipher = ref('')
const encError = ref('')
/* ---------------- 解密 tab 状态 ---------------- */
const cipherIn = ref('')
const key2 = ref('')
const cipherAlgo2 = ref('AES')
const blockMode2 = ref('CBC')
const padding2 = ref('Pkcs7')
const iv2 = ref('0000000000000000')
const keyMode2 = ref('passphrase')
const keyEnc2 = ref('utf8')
const cipherEnc2 = ref('base64')
const plainOut = ref('')
const error = ref('')
/* ---------------- 公共辅助 ---------------- */
function parseBy(text, enc) {
return enc === 'hex' ? CryptoJS.enc.Hex.parse(text) : CryptoJS.enc.Utf8.parse(text)
}
function buildOptions(cfg) {
if (isStream(cfg.algo)) return {}
const opt = {
mode: CryptoJS.mode[cfg.blockMode],
padding: CryptoJS.pad[cfg.padding]
}
if (cfg.blockMode !== 'ECB') opt.iv = parseBy(cfg.iv, cfg.keyEnc)
return opt
}
// 校验密钥 / IV 长度,返回中文错误信息(无问题返回空串)
function validate(cfg) {
if (!cfg.key) return '请输入' + (cfg.keyMode === 'passphrase' ? '口令' : '密钥')
if (cfg.keyMode === 'raw') {
let kw
try {
kw = parseBy(cfg.key, cfg.keyEnc)
} catch {
return '密钥不是合法的 Hex 字符串'
}
if (cfg.algo === 'AES' && ![16, 24, 32].includes(kw.sigBytes)) {
return `AES 直接使用密钥时长度必须为 16 / 24 / 32 字节,当前为 ${kw.sigBytes} 字节`
}
if (kw.sigBytes === 0) return '密钥不能为空'
}
if (!isStream(cfg.algo) && cfg.blockMode !== 'ECB') {
let ivw
try {
ivw = parseBy(cfg.iv, cfg.keyEnc)
} catch {
return 'IV 不是合法的 Hex 字符串'
}
const need = BLOCK_SIZE[cfg.algo]
if (ivw.sigBytes !== need) {
return `${cfg.algo} 在 CBC 模式下 IV 必须为 ${need} 字节,当前为 ${ivw.sigBytes} 字节`
}
}
return ''
}
function encCfg() {
return {
algo: cipherAlgo.value,
blockMode: blockMode.value,
padding: padding.value,
iv: iv.value,
key: key.value,
keyMode: keyMode.value,
keyEnc: keyEnc.value,
cipherEnc: cipherEnc.value
}
}
function decCfg() {
return {
algo: cipherAlgo2.value,
blockMode: blockMode2.value,
padding: padding2.value,
iv: iv2.value,
key: key2.value,
keyMode: keyMode2.value,
keyEnc: keyEnc2.value,
cipherEnc: cipherEnc2.value
}
}
/* ---------------- 加解密 ---------------- */
function encrypt() {
encError.value = ''
cipher.value = ''
if (!plain.value) return
const cfg = encCfg()
const msg = validate(cfg)
if (msg) { encError.value = msg; return }
try {
const keyArg = cfg.keyMode === 'raw' ? parseBy(cfg.key, cfg.keyEnc) : cfg.key
const cp = CIPHERS[cfg.algo].encrypt(plain.value, keyArg, buildOptions(cfg))
if (cfg.keyMode === 'passphrase') {
// 口令派生会生成随机盐,盐封装在 OpenSSL 格式中,必须连同盐一起输出
const openssl = cp.toString()
cipher.value = cfg.cipherEnc === 'hex'
? CryptoJS.enc.Base64.parse(openssl).toString(CryptoJS.enc.Hex)
: openssl
} else {
cipher.value = cfg.cipherEnc === 'hex'
? cp.ciphertext.toString(CryptoJS.enc.Hex)
: cp.ciphertext.toString(CryptoJS.enc.Base64)
}
} catch (e) {
encError.value = '加密失败:' + e.message
}
}
function decrypt() {
error.value = ''
plainOut.value = ''
if (!cipherIn.value) return
const cfg = decCfg()
const msg = validate(cfg)
if (msg) { error.value = msg; return }
try {
const keyArg = cfg.keyMode === 'raw' ? parseBy(cfg.key, cfg.keyEnc) : cfg.key
const text = cipherIn.value.trim()
let input
if (cfg.keyMode === 'passphrase') {
input = cfg.cipherEnc === 'hex'
? CryptoJS.enc.Hex.parse(text).toString(CryptoJS.enc.Base64)
: text
} else {
const ct = cfg.cipherEnc === 'hex'
? CryptoJS.enc.Hex.parse(text)
: CryptoJS.enc.Base64.parse(text)
input = CryptoJS.lib.CipherParams.create({ ciphertext: ct })
}
const bytes = CIPHERS[cfg.algo].decrypt(input, keyArg, buildOptions(cfg))
const t = bytes.toString(CryptoJS.enc.Utf8)
if (!t) { error.value = '解密失败:口令、密文或参数不正确'; return }
// ZeroPadding / NoPadding 可能残留尾部 \0
plainOut.value = cfg.padding === 'ZeroPadding' || cfg.padding === 'NoPadding'
? t.replace(/\0+$/, '')
: t
} catch (e) {
error.value = '解密失败:' + e.message
}
}
function copyC() { if (cipher.value) navigator.clipboard?.writeText(cipher.value) }2.3 效果截图
