You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 16:05:54