开篇:加密算法的选择困境
在做涉及敏感数据的前端项目时,你肯定遇到过这个问题:用 AES 还是国密?国际算法生态成熟,国密算法合规性强,两者各有优劣。今天我们深入聊聊这两种 加密 算法的前端实现,看看它们的本质区别在哪里。
先看个实际场景:你在做一个政务系统的数据加密模块,甲方明确要求"必须使用国密算法"。这时候你需要的不仅是调用 API,更得搞清楚 SM4 和 AES 到底差在哪儿。
AES 加密:从原理到代码
AES( Advanced Encryption Standard)是目前最广泛使用的对称加密算法。它的核心是 SPN 结构——代换-置换网络,通过多轮的字节替换、行移位、列混淆、轮密钥加来完成加密。
AES 的关键参数
| 密钥长度 | 轮数 | 分组长度 |
|---|---|---|
| 128 bit | 10 | 128 bit |
| 192 bit | 12 | 128 bit |
| 256 bit | 14 | 128 bit |
前端实现时,我们通常用 CryptoJS 这个库:
import CryptoJS from 'crypto-js'
// AES 加密
const plaintext = '敏感数据'
const key = 'my-secret-key-123'
const encrypted = CryptoJS.AES.encrypt(plaintext, key).toString()
console.log(encrypted) // U2FsdGVkX1+3Z7Q8...
// AES 解密
const decrypted = CryptoJS.AES.decrypt(encrypted, key)
const originalText = decrypted.toString(CryptoJS.enc.Utf8)
console.log(originalText) // 敏感数据
这段代码看着简单,但背后有几个容易踩的坑:
坑一:密钥处理方式
CryptoJS 收到字符串密钥时,会自动用 OpenSSL 的 EvpKDF 派生出实际的加密密钥和 IV。这意味着:
// 这两行代码的结果每次都不同!
CryptoJS.AES.encrypt('hello', 'key').toString() // 包含随机 salt
CryptoJS.AES.encrypt('hello', 'key').toString() // 又一个不同的密文
如果你需要确定性加密(相同明文 + 相同密钥 = 相同密文),必须手动指定 IV:
const keyBytes = CryptoJS.enc.Utf8.parse('1234567890123456') // 16字节密钥
const ivBytes = CryptoJS.enc.Utf8.parse('1234567890123456') // 16字节IV
const encrypted = CryptoJS.AES.encrypt('敏感数据', keyBytes, {
iv: ivBytes,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString()
// 现在,同样的输入永远得到同样的输出
坑二:填充模式
AES 要求明文长度必须是 16 字节的倍数。不足的部分需要填充。CryptoJS 默认用 PKCS7 填充,但后端可能用 ZeroPadding 或 NoPadding,对接时务必确认。
SM4 加密:国密算法的实现
SM4 是中国国家密码管理局发布的对称加密算法,分组长度和密钥长度都是 128 位,共 32 轮迭代。它的核心是 Feistel 结构的变体——轮函数采用非 线性 变换 τ 和线性变换 L。
SM4 vs AES 核心差异
| 特性 | AES | SM4 |
|---|---|---|
| 结构 | SPN | Feistel 变体 |
| 轮数 | 10/12/14 | 固定 32 轮 |
| S-box | 有限域逆运算 | 查表法 |
| 设计理念 | 数学优雅 | 安全裕度大 |
前端实现 SM4,推荐用 sm-crypto 库:
import { sm4 } from 'sm-crypto'
const key = '0123456789abcdeffedcba9876543210' // 32位十六进制密钥
const plaintext = '国密测试数据'
// SM4 加密
const ciphertext = sm4.encrypt(plaintext, key)
console.log(ciphertext) // 128位十六进制字符串
// SM4 解密
const decrypted = sm4.decrypt(ciphertext, key)
console.log(decrypted) // 国密测试数据
SM4 的密钥注意事项
SM4 的密钥必须是 32 位十六进制字符串(16 字节)。如果你传入普通字符串,需要先转换:
// 字符串转十六进制
function stringToHex(str) {
const encoder = new TextEncoder()
const bytes = encoder.encode(str)
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
// 使用自定义密钥
const myKey = '我的密钥123456'
const hexKey = stringToHex(myKey).padEnd(32, '0').slice(0, 32)
const encrypted = sm4.encrypt('敏感数据', hexKey)
性能对比:实测数据
我在 Chrome 120 上做了个简单测试,加密 1MB 随机数据:
AES-128-CBC (CryptoJS): ~180ms
SM4 (sm-crypto): ~250ms
AES-128-GCM (Web Crypto): ~15ms 🚀
结论很明确:Web Crypto API 性能碾压 JavaScript 实现。如果你能用浏览器原生 API,尽量用它:
// Web Crypto API 实现 AES-GCM
async function aesGcmEncrypt(plaintext, password) {
const encoder = new TextEncoder()
const data = encoder.encode(plaintext)
// 从密码派生密钥
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
)
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: crypto.getRandomValues(new Uint8Array(16)), iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 128 },
false,
['encrypt']
)
const iv = crypto.getRandomValues(new Uint8Array(12))
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
)
return { iv, ciphertext }
}
国密算法的特殊应用场景
在中国,政务、金融、医疗等领域有明确的国密合规要求。《密码法》规定关键 信息 基础设施必须使用国家认可的商用密码算法。这意味着:
- 政府采购项目:必须支持 SM2/SM3/SM4
- 银行系统:银联要求使用国密算法
- 医疗数据:电子病历系统要求国密保护
国密三件套:
| 算法 | 用途 | 对标国际标准 |
|---|---|---|
| SM2 | 非对称加密 | ECC P-256 |
| SM3 | 哈希算法 | SHA-256 |
| SM4 | 对称加密 | AES-128 |
// SM2 非对称加密示例
import { sm2 } from 'sm-crypto'
const keyPair = sm2.generateKeyPairHex()
// 公钥用于加密,私钥用于解密
const ciphertext = sm2.doEncrypt('敏感数据', keyPair.publicKey, 1)
实战建议:如何选择?
- 面向全球用户 → AES + Web Crypto API
- 国内政务/金融项目 → SM 系列国密算法
- 高安全场景 → AES-256-GCM 或 SM4 + SM2 混合加密
- 性能敏感场景 → 优先用 Web Crypto API
安全最佳实践
无论选 AES 还是 SM4,这些原则都不能忘:
// ❌ 错误:硬编码密钥
const KEY = 'my-secret-key-123'
// ✅ 正确:从环境变量或后端获取密钥
const KEY = process.env.ENCRYPTION_KEY
// ✅ 正确:使用强随机数生成密钥
const key = crypto.getRandomValues(new Uint8Array(16))
永远不要在前端代码中硬编码密钥,也不要用时间戳或 Math.random() 生成密钥——这些都不是密码学安全的随机源。
相关工具推荐
- 高级加密工具 - 支持 AES/DES/SM2/SM4 等多种算法
- 哈希生成器 - MD5/SHA-256/SM3 哈希计算
- Base64 编解码 - 常用于密文编码传输