坐望云起头像
Web Crypto API 实战:浏览器端 MD5/AES/RSA/国密SM2/SM3/SM4 加密解密封面图

Web Crypto API 实战:浏览器端 MD5/AES/RSA/国密SM2/SM3/SM4 加密解密

本文介绍如何使用 Web Crypto API 和 sm-crypto 库在浏览器端实现常用加密算法,包括 MD5/SHA 哈希、AES 对称加密、RSA 非对称加密、国密 SM2/SM3/SM4,以及 JWT 解码。含完整代码示例。

前端加密的典型场景

  1. 密码哈希:用户密码不能明文传输,前端先 MD5/SHA 哈希再发到后端
  2. 数据加密传输:敏感数据用 AES 加密后再传输,即使 HTTPS 被中间人攻击也无法解密
  3. RSA 密钥对生成:在浏览器端生成公私钥对,私钥不离开设备
  4. 国密合规:金融、政务场景要求使用国密算法(SM2/SM3/SM4)
  5. JWT 调试:前端需要解码 JWT Token 查看载荷内容

Web Crypto API 简介

Web Crypto API 是浏览器原生提供的加密接口,基于 window.crypto.subtle。

优势:

  • 浏览器原生支持,无需第三方库
  • 底层用 C/C++ 实现,性能远高于 JS 库
  • 密钥材料不可导出(除非显式标记 extractable: true)

限制:

  • 不支持 MD5(已被认为不安全)
  • 不支持国密算法
  • API 是异步的,返回 Promise

所以我们需要补充:

  • MD5 用第三方库(如 crypto-js)
  • 国密用 sm-crypto 库

实战代码

1. SHA 哈希(Web Crypto API 原生)

// SHA-256 哈希
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('');
}

// 支持 SHA-1, SHA-256, SHA-384, SHA-512
async function sha(message, algorithm = 'SHA-256') {
  const encoder = new TextEncoder();
  const data = encoder.encode(message);
  const hashBuffer = await crypto.subtle.digest(algorithm, data);
  return bufferToHex(hashBuffer);
}

function bufferToHex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

2. HMAC 签名

async function hmacSign(message, key, algorithm = 'SHA-256') {
  const encoder = new TextEncoder();
  
  // 导入密钥
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    encoder.encode(key),
    { name: 'HMAC', hash: algorithm },
    false,
    ['sign']
  );
  
  // 签名
  const signature = await crypto.subtle.sign(
    'HMAC',
    cryptoKey,
    encoder.encode(message)
  );
  
  return bufferToHex(signature);
}

3. AES 加密解密

// AES-CBC 加密
async function aesEncrypt(plaintext, password) {
  const encoder = new TextEncoder();
  
  // 从密码派生密钥(PBKDF2)
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const iv = crypto.getRandomValues(new Uint8Array(16));
  
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    encoder.encode(password),
    'PBKDF2',
    false,
    ['deriveKey']
  );
  
  const key = await crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: 100000,
      hash: 'SHA-256'
    },
    keyMaterial,
    { name: 'AES-CBC', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
  
  // 加密
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-CBC', iv: iv },
    key,
    encoder.encode(plaintext)
  );
  
  // 返回 salt + iv + ciphertext(base64)
  const combined = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
  combined.set(salt, 0);
  combined.set(iv, salt.length);
  combined.set(new Uint8Array(ciphertext), salt.length + iv.length);
  
  return btoa(String.fromCharCode(...combined));
}

// AES-CBC 解密
async function aesDecrypt(ciphertextBase64, password) {
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();
  
  // 解析 salt + iv + ciphertext
  const combined = Uint8Array.from(atob(ciphertextBase64), c => c.charCodeAt(0));
  const salt = combined.slice(0, 16);
  const iv = combined.slice(16, 32);
  const ciphertext = combined.slice(32);
  
  // 派生密钥
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    encoder.encode(password),
    'PBKDF2',
    false,
    ['deriveKey']
  );
  
  const key = await crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: 100000,
      hash: 'SHA-256'
    },
    keyMaterial,
    { name: 'AES-CBC', length: 256 },
    false,
    ['decrypt']
  );
  
  // 解密
  const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-CBC', iv: iv },
    key,
    ciphertext
  );
  
  return decoder.decode(plaintext);
}

4. RSA 密钥对生成与加解密

// 生成 RSA 密钥对
async function generateRSAKeyPair(bits = 2048) {
  const keyPair = await crypto.subtle.generateKey(
    {
      name: 'RSA-OAEP',
      modulusLength: bits,
      publicExponent: new Uint8Array([1, 0, 1]), // 65537
      hash: 'SHA-256'
    },
    true, // extractable
    ['encrypt', 'decrypt']
  );
  
  // 导出为 PEM 格式
  const publicKey = await crypto.subtle.exportKey('spki', keyPair.publicKey);
  const privateKey = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
  
  return {
    publicKey: arrayBufferToPEM(publicKey, 'PUBLIC KEY'),
    privateKey: arrayBufferToPEM(privateKey, 'PRIVATE KEY')
  };
}

function arrayBufferToPEM(buffer, type) {
  const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
  const lines = base64.match(/.{1,64}/g).join('\n');
  return `-----BEGIN ${type}-----\n${lines}\n-----END ${type}-----`;
}

// RSA 加密(用公钥)
async function rsaEncrypt(plaintext, publicKeyPEM) {
  const publicKey = await importPublicKey(publicKeyPEM);
  const encoder = new TextEncoder();
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'RSA-OAEP' },
    publicKey,
    encoder.encode(plaintext)
  );
  return btoa(String.fromCharCode(...new Uint8Array(ciphertext)));
}

5. 国密 SM2/SM3/SM4(sm-crypto 库)

Web Crypto API 不支持国密算法,需要用 sm-crypto 库:

npm install sm-crypto
import { sm2, sm3, sm4 } from 'sm-crypto';

// SM3 哈希(类似 SHA-256 的国密版本)
const hash = sm3('Hello World');

// SM4 对称加密(类似 AES 的国密版本)
const key = '0123456789abcdef0123456789abcdef'; // 128bit hex
const ciphertext = sm4.encrypt('Hello World', key);

// SM2 非对称加密(类似 RSA 的国密版本)
const keypair = sm2.generateKeyPairHex();
// keypair.publicKey  公钥
// keypair.privateKey 私钥

const encrypted = sm2.doEncrypt('Hello World', keypair.publicKey);
const decrypted = sm2.doDecrypt(encrypted, keypair.privateKey);

6. JWT 解码

JWT 由三部分组成:header.payload.signature,用 . 分隔。前两部分是 Base64URL 编码的 JSON:

function decodeJWT(token) {
  const parts = token.split('.');
  if (parts.length !== 3) {
    throw new Error('Invalid JWT format');
  }
  
  // Base64URL 解码
  function base64UrlDecode(str) {
    str = str.replace(/-/g, '+').replace(/_/g, '/');
    while (str.length % 4) str += '=';
    return atob(str);
  }
  
  const header = JSON.parse(base64UrlDecode(parts[0]));
  const payload = JSON.parse(base64UrlDecode(parts[1]));
  
  return {
    header,
    payload,
    signature: parts[2],
    // 检查是否过期
    isExpired: payload.exp ? (Date.now() / 1000 > payload.exp) : false
  };
}

安全注意事项

  1. MD5 不安全:MD5 已被证明存在碰撞攻击,不要用于安全场景。密码哈希用 SHA-256 或更强的算法。

  2. AES 密钥派生:不要直接用用户密码作为 AES 密钥,用 PBKDF2 派生,迭代次数至少 100000。

  3. RSA 密钥长度:至少 2048 位,推荐 4096 位。1024 位已被认为不安全。

  4. 前端加密不能替代 HTTPS:前端加密是额外安全层,不是 HTTPS 的替代品。中间人可以替换页面中的 JS 代码,绕过前端加密。

  5. 国密算法使用场景:SM2 替代 RSA,SM3 替代 SHA-256,SM4 替代 AES。金融、政务等合规场景必须使用国密。

在线体验

以下加密工具全部纯浏览器端实现,不上传服务器:

支持 RSA 密钥对生成并下载 PEM 文件,方便配合后端使用。

总结

算法类型实现方式适用场景
SHA-1/256/384/512哈希Web Crypto API 原生数据完整性校验、密码哈希
HMAC签名Web Crypto API 原生API 请求签名
AES-CBC/GCM对称加密Web Crypto API 原生数据加密传输
RSA-OAEP非对称加密Web Crypto API 原生密钥交换、数字签名
SM2非对称加密sm-crypto 库国密合规场景
SM3哈希sm-crypto 库国密合规场景
SM4对称加密sm-crypto 库国密合规场景
JWTToken 解码原生 JS身份认证调试

Web Crypto API 覆盖了大部分加密需求,国密场景补充 sm-crypto 库即可。两者结合,前端加密能力已经很完善了。


本文中的加密工具由 AIWriter 提供,更多免费在线工具请访问 工具合集。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/bashendixie5/article/details/162612506

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--