国密 vs 国际加密:AES 与 SM4 算法的前端实现对比

28 阅读5分钟

在这里插入图片描述

开篇:加密算法的选择困境

在做涉及敏感数据的前端项目时,你肯定遇到过这个问题:用 AES 还是国密?国际算法生态成熟,国密算法合规性强,两者各有优劣。今天我们深入聊聊这两种 加密 算法的前端实现,看看它们的本质区别在哪里。

先看个实际场景:你在做一个政务系统的数据加密模块,甲方明确要求"必须使用国密算法"。这时候你需要的不仅是调用 API,更得搞清楚 SM4 和 AES 到底差在哪儿。

AES 加密:从原理到代码

AES( Advanced Encryption Standard)是目前最广泛使用的对称加密算法。它的核心是 SPN 结构——代换-置换网络,通过多轮的字节替换、行移位、列混淆、轮密钥加来完成加密。

AES 的关键参数

密钥长度轮数分组长度
128 bit10128 bit
192 bit12128 bit
256 bit14128 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 核心差异

特性AESSM4
结构SPNFeistel 变体
轮数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 }
}

国密算法的特殊应用场景

在中国,政务、金融、医疗等领域有明确的国密合规要求。《密码法》规定关键 信息 基础设施必须使用国家认可的商用密码算法。这意味着:

  1. 政府采购项目:必须支持 SM2/SM3/SM4
  2. 银行系统:银联要求使用国密算法
  3. 医疗数据:电子病历系统要求国密保护

国密三件套:

算法用途对标国际标准
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)

工具链接:jsokit.com/tools/crypt…

实战建议:如何选择?

  1. 面向全球用户 → AES + Web Crypto API
  2. 国内政务/金融项目 → SM 系列国密算法
  3. 高安全场景 → AES-256-GCM 或 SM4 + SM2 混合加密
  4. 性能敏感场景 → 优先用 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() 生成密钥——这些都不是密码学安全的随机源。


相关工具推荐