Web Crypto API 实现浏览器端加密

0 阅读4分钟

用 Web Crypto API 做安全的客户端加密工具

做一个纯前端的加密工具集,不依赖任何后端。 Web Crypto API 是好东西,但坑也不少。

为什么要纯前端加密?

📚 本文是 UtlKit 技术系列第 7 篇系列索引 →

  1. 隐私 — 用户的敏感数据不应该发到任何服务器
  2. 信任 — 前端代码开源可审计,比黑盒后端可信
  3. 成本 — 零服务器成本

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)
}

安全要点

  1. IV 每次随机 — 同一个 key 加密不同消息必须用不同 IV
  2. IV 不需要保密 — 和密文一起传输即可
  3. GCM 模式自带认证 — 解密时如果密文被篡改,会直接报错
  4. 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 实现。

最佳实践总结

  1. 优先用 Web Crypto API — 原生、安全、性能好
  2. MD5 用纯 JS 实现 — 代码量小,不影响架构
  3. Key 不存储 — 页面刷新即消失
  4. IV 随机生成 — 每次加密都要新的 IV
  5. 用 GCM 不用 CBC — GCM 自带认证,更安全
  6. HMAC 做完整性校验 — 比单独算 Hash 更可靠

📚 本文是 UtlKit 技术系列第 7 篇系列索引 →


utlkit.com/zh-CN/tools… — Hash 生成器

utlkit.com/zh-CN/tools… — AES 加密解密

utlkit.com/zh-CN/tools… — HMAC 生成器