subtlecrypto 生成 aes 密钥并加密需分三步:先用 generatekey() 创建 256 位 aes-gcm 密钥,再用 getrandomvalues() 生成 12 字节随机 iv,最后调用 encrypt() 加密 uint8array 数据;密文、iv 和 authtag 必须分别转 base64 后组合传输,不可拼接字符串或复用 iv。

如何用 SubtleCrypto 生成 AES 密钥并加密数据
浏览器端 AES 加密必须走 SubtleCrypto,不能手动拼接密钥或 IV;直接传字符串进 importKey() 会失败,必须先转成 Uint8Array。
常见错误是把密码当密钥用——AES 需要固定长度密钥(128/192/256 位),而用户口令是任意长度字符串。得用 deriveKey() + PBKDF2 派生:
-
importKey()只能用于已知格式的密钥材料(如从后端下发的 base64 密钥),不能直接塞密码字符串 - IV 必须每次加密都随机生成,且长度固定为 12 字节(GCM 模式)或 16 字节(CBC),不能复用
- CBC 模式需手动 PKCS#7 填充,GCM 更安全且自带认证,推荐优先用
{ name: 'AES-GCM', iv }
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const data = encoder.encode('hello world');
<p>const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key.privateKey || key,
data
);</p>
为什么 digest() 返回的是 ArrayBuffer 而不是 hex 字符串
SubtleCrypto.digest() 的输出永远是 ArrayBuffer,不是字符串。想转成常见 hex 或 base64,必须手动转换,否则直接 console.log() 看到的是空对象或乱码。
SHA-256 输出长度固定为 32 字节,但不同场景对格式要求不同:后端可能要 hex 小写,签名验签常要 base64url 编码。别跳过这步:
- hex 转换:用
Array.from(new Uint8Array(hash), b => b.toString(16).padStart(2, '0')).join('') - base64:用
btoa(String.fromCharCode(...new Uint8Array(hash)))(注意非 utf-8 安全,小数据可用) - 更健壮的 base64url:先转普通 base64,再替换
+→-、/→_、去掉=
const hash = await crypto.subtle.digest('SHA-256', encoder.encode('secret'));
const hex = Array.from(new Uint8Array(hash), b => b.toString(16).padStart(2,'0')).join('');
// → '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'
如何安全地把 AES 密文和 IV 一起序列化传输
AES-GCM 加密后,iv 和密文必须一起传给解密方,但不能拼成字符串乱连——二进制数据混入文本会出错。常见错误是 iv + '|' + encrypted,结果解密时 iv 已被破坏。
正确做法是统一转成 base64 或 base64url,再组合。GCM 还有额外字段 authTag(16 字节),它也是 ArrayBuffer,必须一并提取:
- 加密返回值是完整 ArrayBuffer,需用
getAuthTag()单独取 tag(GCM 模式下) - 推荐结构:
{ iv: base64, authTag: base64, ciphertext: base64 },JSON 序列化后传输 - 解密前必须还原为
Uint8Array,不能直接传 base64 字符串给decrypt()
const encryptedBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
const authTag = encryptedBuf.slice(encryptedBuf.byteLength - 16, encryptedBuf.byteLength);
const ciphertext = encryptedBuf.slice(0, encryptedBuf.byteLength - 16);
<p>// 发送前:
const payload = {
iv: btoa(String.fromCharCode(...iv)),
authTag: btoa(String.fromCharCode(...new Uint8Array(authTag))),
ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertext)))
};</p>
为什么 importKey() 用 raw 格式失败但 jwk 成功
用 importKey('raw', keyMaterial, ...) 报错 DataError,大概率是 keyMaterial 类型不对:它必须是 ArrayBuffer 或 TypedArray,不能是 string、number 或普通 object。
尤其注意:从 localStorage 读出的密钥如果是 base64 字符串,必须先 atob() 再转 Uint8Array;如果密钥来自后端 JSON,且是 hex 字符串,得先 parse 成字节数组。
- hex 字符串转
Uint8Array:每两位 hex 转一个 byte,parseInt(hex.slice(i, i+2), 16) - base64 字符串转
Uint8Array:先atob()得到原始字符串,再逐字符.charCodeAt()构造 - JWK 格式虽然啰嗦,但自带元信息(
kty,alg,k),importKey('jwk', ...)自动处理编码,适合跨平台交换
真正难的不是调用 API,而是密钥生命周期管理——谁生成、谁存储、谁销毁、是否导出。浏览器里没有“安全 enclave”,extractable: false 只防 JS 读取,不防调试器内存 dump。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











