用 Web Crypto API 做安全的客户端加密工具
做一个纯前端的加密工具集,不依赖任何后端。 Web Crypto API 是好东西,但坑也不少。
为什么要纯前端加密?
📚 本文是 UtlKit 技术系列第 7 篇 — 系列索引 →
- 上一篇:Sentry Source Map →
- 下一篇:[PageSpeed CI →]
- 隐私 — 用户的敏感数据不应该发到任何服务器
- 信任 — 前端代码开源可审计,比黑盒后端可信
- 成本 — 零服务器成本
Web Crypto API 支持什么?
✅ SHA-1 / SHA-256 / SHA-384 / SHA-512 (digest)
✅ RSA-OAEP / RSA-PSS (非对称)
✅ AES-GCM / AES-CBC / AES-KW (对称)
✅ HMAC (签名)
✅ ECDSA / Ed25519 (椭圆曲线)
❌ MD5 (不在规范里)
❌ Blowfish / TWOFISH (不在规范里)
SHA 哈希系列
最简单的用法:
async function sha256(message) {
const encoder = new TextEncoder()
const data = encoder.encode(message)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// 使用
sha256('hello')
// → "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
一次调用拿到 5 种哈希:
async function hashAll(text) {
const encoder = new TextEncoder()
const data = encoder.encode(text)
const algorithms = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']
const results = {}
for (const algo of algorithms) {
const hashBuffer = await crypto.subtle.digest(algo, data)
results[algo] = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
return results
}
MD5:Web Crypto 不支持,自己实现
MD5 是弱哈希,已被 SHA-2 取代,但实际使用中仍然大量存在。
Web Crypto API 没有 MD5,需要自己实现:
function md5(string) {
function md5cycle(x, k) {
var a = x[0], b = x[1], c = x[2], d = x[3]
a = ff(a, b, c, d, k[0], 7, -680876936)
d = ff(d, a, b, c, k[1], 12, -389564586)
// ... (完整的 MD5 四轮运算)
x[0] = add32(a, x[0])
x[1] = add32(b, x[1])
x[2] = add32(c, x[2])
x[3] = add32(d, x[3])
}
// ... (padding、处理、输出 hex)
// 完整实现约 100 行
}
完整的 MD5 实现在项目里约 100 行代码,纯 bitwise operation,无外部依赖。
性能: 对于短文本(< 10KB),纯 JS MD5 约 5-10ms,完全可接受。
AES-GCM 加密
// 生成随机 key
async function generateAesKey() {
return crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true, // 可导出
['encrypt', 'decrypt']
)
}
// 加密
async function aesEncrypt(plaintext, key) {
const encoder = new TextEncoder()
const iv = crypto.getRandomValues(new Uint8Array(12)) // 96-bit IV
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
encoder.encode(plaintext)
)
// 返回 IV + ciphertext(IV 需要一起传输)
const result = new Uint8Array(iv.length + ciphertext.byteLength)
result.set(iv)
result.set(new Uint8Array(ciphertext), iv.length)
return btoa(String.fromCharCode(...result)) // Base64 输出
}
// 解密
async function aesDecrypt(encodedText, key) {
const data = Uint8Array.from(atob(encodedText), c => c.charCodeAt(0))
const iv = data.slice(0, 12)
const ciphertext = data.slice(12)
const plaintextBuffer = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
ciphertext
)
return new TextDecoder().decode(plaintextBuffer)
}
安全要点
- IV 每次随机 — 同一个 key 加密不同消息必须用不同 IV
- IV 不需要保密 — 和密文一起传输即可
- GCM 模式自带认证 — 解密时如果密文被篡改,会直接报错
- Key 不持久化 — 页面刷新后 key 消失
HMAC 签名
async function hmacSha256(message, keyString) {
const encoder = new TextEncoder()
// 从字符串导入 key
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(keyString),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
)
// 签名
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message)
)
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
// 验证
async function verifyHmac(message, keyString, expectedSignature) {
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(keyString),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
)
const signatureBytes = Uint8Array.from(
expectedSignature.match(/.{1,2}/g).map(b => parseInt(b, 16))
)
return crypto.subtle.verify(
'HMAC',
key,
signatureBytes,
encoder.encode(message)
)
}
性能对比
文本大小 算法 耗时
─────────────────────────────────
1 KB SHA-256 < 1ms
1 KB MD5 (JS) ~2ms
1 KB AES-GCM 加密 ~1ms
100 KB SHA-256 ~2ms
100 KB AES-GCM ~3ms
1 MB SHA-256 ~15ms
1 MB AES-GCM ~20ms
对于工具类网站,处理的数据量通常在 KB 级别,Web Crypto 性能完全足够。
不支持 MD5 的替代方案
如果一定要 MD5,有两个选择:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 纯 JS 实现 (~100 行) | 无依赖,可控 | 自己维护 |
blueimp-md5 库 | 成熟稳定 | 额外依赖 |
项目中选择了纯 JS 实现。
最佳实践总结
- 优先用 Web Crypto API — 原生、安全、性能好
- MD5 用纯 JS 实现 — 代码量小,不影响架构
- Key 不存储 — 页面刷新即消失
- IV 随机生成 — 每次加密都要新的 IV
- 用 GCM 不用 CBC — GCM 自带认证,更安全
- HMAC 做完整性校验 — 比单独算 Hash 更可靠
📚 本文是 UtlKit 技术系列第 7 篇 — 系列索引 →
- 上一篇:Sentry Source Map →
- 下一篇:[PageSpeed CI →]
utlkit.com/zh-CN/tools… — Hash 生成器
utlkit.com/zh-CN/tools… — AES 加密解密
utlkit.com/zh-CN/tools… — HMAC 生成器