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

如何在Web端用JavaScript分块解密1GB以上的AES-CBC加密大文件?

浏览器端大文件AES-CBC流式解密的原生API实现思路

我们尝试在浏览器端用JavaScript实现大文件流式解密,由于crypto.subtle原生不支持流式解密,因此采用分块解密方案——理论上可将前一块密文作为当前块的初始化向量(IV)。但发现crypto.subtle的AES-CBC解密始终要求密文末尾带有PKCS#7填充,这让实现难度大增。请问有没有基于原生API的JavaScript实现示例或思路?

当前实现代码如下:

const padding = new Uint8Array(16).fill(16);
const chunkSize = 16;
let index = 0;
const chunks = [];
let prevChunk = null;
do {
    const chunk = ciphertext.slice(index, index + chunkSize);
    
    // Encrypt padding with the chunk as iv
    const paddingCypher = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: chunk }, key, padding);
    const encryptedPadding = (new Uint8Array(paddingCypher)).slice(0, 16);

    const decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: prevChunk || subtleIv}, subtleKey, mergeByteArrays([chunk, encryptedPadding]));
    chunks.push(new Uint8Array(decrypted));

    prevChunk = chunk;
    index += chunkSize;
} while (index < length - chunkSize);

// Different for last chunk as that already has the padding
const lastChunk = await decrypt(
    prevChunk || subtleIv,
    subtleKey,
    ciphertext.slice(index, index + chunkSize)
);
chunks.push(new Uint8Array(lastChunk));

var mergedChunks = mergeByteArrays(chunks);

// Decode and output
var fullyDecrypted = String.fromCharCode.apply(null, mergedChunks as any);
console.log(fullyDecrypted);

核心思路与修正实现

由于crypto.subtle的AES-CBC解密必须处理带PKCS#7填充的完整密文,直接分块解密单个无填充块会失败。我们可以利用CBC模式的链式特性,结合原生API实现流式解密,具体步骤如下:

  1. 按16字节整数倍分块(如16KB),最后一块单独处理(自带填充)
  2. 前N-1块通过拼接虚拟填充密文块,构造符合API要求的密文结构,解密后提取真实明文
  3. 最后一块直接解密后去除PKCS#7填充

修正后的实现代码:

/**
 * 合并Uint8Array数组
 * @param {Uint8Array[]} arrays 要合并的数组列表
 * @returns {Uint8Array} 合并后的数组
 */
function mergeByteArrays(arrays) {
    let totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
    let result = new Uint8Array(totalLength);
    let offset = 0;
    for (let arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}

/**
 * 去除PKCS#7填充
 * @param {Uint8Array} data 带填充的数据
 * @returns {Uint8Array} 去除填充后的原始数据
 */
function removePkcs7Padding(data) {
    const paddingLength = data[data.length - 1];
    if (paddingLength < 1 || paddingLength > 16) {
        throw new Error('Invalid PKCS#7 padding');
    }
    return data.slice(0, data.length - paddingLength);
}

/**
 * 流式解密AES-CBC密文
 * @param {CryptoKey} key AES密钥
 * @param {Uint8Array} iv 初始IV
 * @param {Uint8Array} ciphertext 完整密文
 * @param {number} chunkSize 分块大小(必须是16的整数倍)
 * @returns {Promise<Uint8Array>} 解密后的明文
 */
async function streamDecryptAesCbc(key, iv, ciphertext, chunkSize = 16 * 1024) {
    if (chunkSize % 16 !== 0) {
        throw new Error('Chunk size must be a multiple of 16');
    }

    const chunks = [];
    let currentIv = iv;
    const totalLength = ciphertext.length;
    let index = 0;

    // 处理前N-1块(无填充)
    while (index + chunkSize < totalLength) {
        const chunk = ciphertext.slice(index, index + chunkSize);
        
        // 生成虚拟填充对应的密文块
        const dummyPadding = new Uint8Array(16).fill(16);
        const dummyCipherBlock = await crypto.subtle.encrypt(
            { name: 'AES-CBC', iv: chunk },
            key,
            dummyPadding
        );
        const dummyCipherBytes = new Uint8Array(dummyCipherBlock);

        // 拼接当前块与虚拟密文块,构造可解密的完整密文
        const combinedCipher = mergeByteArrays([chunk, dummyCipherBytes]);
        const decryptedCombined = await crypto.subtle.decrypt(
            { name: 'AES-CBC', iv: currentIv },
            key,
            combinedCipher
        );
        const decryptedBytes = new Uint8Array(decryptedCombined);

        // 提取真实明文(去掉最后16字节虚拟填充)
        chunks.push(decryptedBytes.slice(0, chunkSize));

        currentIv = chunk;
        index += chunkSize;
    }

    // 处理最后一块(带PKCS#7填充)
    const lastChunk = ciphertext.slice(index);
    const decryptedLast = await crypto.subtle.decrypt(
        { name: 'AES-CBC', iv: currentIv },
        key,
        lastChunk
    );
    chunks.push(removePkcs7Padding(new Uint8Array(decryptedLast)));

    return mergeByteArrays(chunks);
}

// 使用示例
async function exampleUsage() {
    // 假设已获取subtleKey、subtleIv、ciphertext
    const plaintext = await streamDecryptAesCbc(subtleKey, subtleIv, ciphertext);
    const fullyDecrypted = new TextDecoder().decode(plaintext);
    console.log(fullyDecrypted);
}

关键说明

  • 虚拟填充块:通过加密标准PKCS#7填充块生成对应密文,拼接后满足原生API对密文必须带填充的要求,解密后再剔除虚拟填充部分
  • IV传递:严格遵循CBC规则,每块密文解密完成后,将当前密文块作为下一块的IV
  • 填充校验:最后一块解密后手动校验并去除PKCS#7填充,避免多余数据残留

内容的提问来源于stack exchange,提问作者Stef

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 14:22:52