使用Web Crypto API在TypeScript中实现加解密遇解密失败求助
问题:Web Crypto API AES-GCM加解密失败及跨端加密方案推荐
需求与问题背景
需要实现客户端加密数据后发送到Node.js服务端解密,避免明文传输且凭证不在浏览器网络面板可见。因数据库已存储加盐哈希的旧密码,无法改用哈希方案,尝试用Web Crypto API实现AES-GCM加解密,但加密成功后解密报错。
测试脚本如下:
function base64ToArrayBuffer(base64): ArrayBuffer { return Uint8Array.from(atob(base64), c => c.charCodeAt(0)) } function ArrayBufferToBase64(buffer): string { return btoa(String.fromCharCode(...new Uint8Array(buffer))); } async function encryptString(privateKey: string, publicKey: string, data: string) { let params: AesGcmParams = { name: 'AES-GCM', iv: base64ToArrayBuffer(publicKey) } let keyImported: CryptoKey = await window.crypto.subtle.importKey("raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"]); let dataInstance: ArrayBuffer = new TextEncoder().encode(data); return Promise.resolve(await window.crypto.subtle.encrypt(params, keyImported, dataInstance)); } async function decryptString(privateKey: string, publicKey: string, data: string) { let params: AesGcmParams = { name: 'AES-GCM', iv: base64ToArrayBuffer(publicKey) } let keyImported: CryptoKey = await window.crypto.subtle.importKey("raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"]); let dataInstance: ArrayBuffer = new TextEncoder().encode(data); return Promise.resolve(await window.crypto.subtle.decrypt(params, keyImported, dataInstance)); } // Key for encryption/decryption process let PRIVATE_KEY: string = 'Fdz2z54vAwIjq0tginHyzgyL62q+f4sBlMi5IOOXJ7c='; let PUBLIC_KEY: string = btoa(String.fromCharCode(...window.crypto.getRandomValues(new Uint8Array(12)))); // Password to be encrypted before being sent to server let password: string = "Test@123"; (async () => { let encrypted_data: string = ArrayBufferToBase64(await encryptString(PRIVATE_KEY, PUBLIC_KEY, password)); console.log(encrypted_data); let decrypted_data: string = ArrayBufferToBase64(await decryptString(PRIVATE_KEY, PUBLIC_KEY, encrypted_data)); console.log(decrypted_data); })();
报错信息:
Promise {<pending>} VM98:33 FSWTBSgdQNVgYOelgRpU5JZm/4G0TRynIyr2jg== VM98:36 Uncaught (in promise) Error await (async) (anonymous) @ VM98:36
错误根本原因
- 解密时加密数据处理错误:加密后的base64字符串是二进制密文的编码,解密时需要将其转回
ArrayBuffer(用base64ToArrayBuffer),而不是用TextEncoder.encode把字符串转成UTF-8数组——这会把密文的base64字符串当成明文编码,导致解密输入的数据完全错误。 - 解密结果处理错误:解密成功后得到的是明文的
ArrayBuffer,需要用TextDecoder转成字符串,而不是用ArrayBufferToBase64转成base64,否则得到的不是原明文。
修复后的脚本
function base64ToArrayBuffer(base64): ArrayBuffer { return Uint8Array.from(atob(base64), c => c.charCodeAt(0)) } function ArrayBufferToBase64(buffer): string { return btoa(String.fromCharCode(...new Uint8Array(buffer))); } async function encryptString(privateKey: string, publicKey: string, data: string) { const params: AesGcmParams = { name: 'AES-GCM', iv: base64ToArrayBuffer(publicKey) } const keyImported: CryptoKey = await window.crypto.subtle.importKey( "raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"] ); const dataInstance: ArrayBuffer = new TextEncoder().encode(data); return window.crypto.subtle.encrypt(params, keyImported, dataInstance); } async function decryptString(privateKey: string, publicKey: string, encryptedBase64: string) { const params: AesGcmParams = { name: 'AES-GCM', iv: base64ToArrayBuffer(publicKey) } const keyImported: CryptoKey = await window.crypto.subtle.importKey( "raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"] ); // 修正:将加密后的base64转回ArrayBuffer,而不是用TextEncoder编码 const encryptedData: ArrayBuffer = base64ToArrayBuffer(encryptedBase64); const decryptedBuffer: ArrayBuffer = await window.crypto.subtle.decrypt(params, keyImported, encryptedData); // 修正:用TextDecoder将ArrayBuffer转成明文字符串 return new TextDecoder().decode(decryptedBuffer); } // Key for encryption/decryption process const PRIVATE_KEY: string = 'Fdz2z54vAwIjq0tginHyzgyL62q+f4sBlMi5IOOXJ7c='; const PUBLIC_KEY: string = btoa(String.fromCharCode(...window.crypto.getRandomValues(new Uint8Array(12)))); // Password to be encrypted before being sent to server const password: string = "Test@123"; (async () => { const encrypted_data: string = ArrayBufferToBase64(await encryptString(PRIVATE_KEY, PUBLIC_KEY, password)); console.log('加密结果:', encrypted_data); const decrypted_data: string = await decryptString(PRIVATE_KEY, PUBLIC_KEY, encrypted_data); console.log('解密结果:', decrypted_data); // 输出:Test@123 })();
跨客户端/服务端的加密库推荐
- crypto-js:支持AES、RSA等多种加密算法,API简洁直观,无需适配即可在浏览器和Node.js环境使用,适合快速开发。
- libsodium-wrappers:基于安全的libsodium库封装,提供现代加密算法(如X25519、ChaCha20-Poly1305),安全性高,跨环境兼容性好,适合对安全要求较高的场景。
- webcrypto-liner:让Node.js的
crypto模块兼容Web Crypto API,可复用同一套加解密代码,减少环境适配成本。
内容的提问来源于stack exchange,提问作者Aditya
相关产品推荐
相关产品推荐

