如何在 javascript(Web)中使用 AES-CBC 逐块解密大文件(+ 1 GB)?

问题描述 投票:0回答:2

我们正在尝试在 Javascript(浏览器)中构建大文件的流解密。由于 crypto.Subtle 本身不支持流解密,我们正在逐块进行。这应该可以通过提供前一个块作为当前块的 iv 来实现。

但似乎 Crypto.subtle 总是期望在块的末尾有一个填充。 (在这里确认: what padding does window.crypto.subtle.encrypt use for AES-CBC)。至少可以说,这让实施感觉很奇怪。

有没有人有任何示例或想法如何在 javascript 中执行此操作,同时仍然使用本机 api?

我们想出了这个:

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);
javascript cryptography video-streaming html5-video aes
2个回答
1
投票

简单的解决方案:使用点击率模式。这不会填充,您可以简单地计算下一个块的计数器。您可以在

AesCtrParams
中指示整个(未签名的大端)计数器,所以您应该是金色的。对于每 16 字节的数据块,计数器需要增加 1。如果您很聪明,那么您当然可以将块设为 16 字节的倍数。


注意没有密码提供与使用传输模式安全性/TLS 相同的安全性。

请记住,CTR 模式是未经身份验证的,尽管填充 oracle 攻击不可能对 CTR 进行(显然),但由此产生的密文容易受到位翻转攻击,因此 - 明文 oracles。因此,您可能需要在 nonce 和密文上包含一个 HMAC 身份验证标签,并在使用解密数据之前验证该标签。

因此,如果您想在服务器端存储数据(“静态数据”),则使用 CBC 或 CTR 在客户端加密主要有用。


1
投票

如果你可以切换模式,另一个答案中建议的 CTR 模式是更方便的方法,因为 WebCrypto API(像大多数库一样)自动禁用流密码模式(如 CTR)的填充,而它自动启用块密码模式(如 CBC)。

但是,如果您不能更改模式并且仍然想使用 WebCrypto API,则必须在解密之前将带有加密填充的密文块添加到每个密文块,如发布的代码中所示,因为 WebCrypto API 不允许对于像 CBC 这样的分组密码模式,PKCS#7 填充被禁用。


发布的代码可以优化:当前代码使用16字节作为块大小。此外,为了确定要附加的密文块,为每个密文块加密一个完整的填充块,由于填充导致 2 块密文,其中第二个块必须被丢弃。

对于优化:

  • 首先,块大小应选择尽可能大,但为简单起见,它仍应为块大小的整数倍。这样一来,更大一部分的加密是由库自己实现的操作模式执行的(可能性能更高)。
  • 其次,为了确定要追加的密文块,应该加密一个空块,由于填充,它只提供要追加的密文块。这意味着 less 被加密并且 less 必须被丢弃。

以下实现同时考虑:

(async () => {

const ciphertext = b642ab('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');
const key = await crypto.subtle.importKey("raw", b642ab('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE='), { name: "AES-CBC" }, true, ["decrypt", "encrypt"]);
const iv = b642ab('MDEyMzQ1Njc4OTAxMjM0NQ==');
let plaintext = await testDecrypt(ciphertext, key, iv, 16);
console.log(plaintext);

async function testDecrypt(ciphertext, key, iv, chunkSizeInBlocks) {
    const length = ciphertext.byteLength;
    const blocksize = 16;
    const chunkSize = chunkSizeInBlocks * blocksize; 
    let index = 0;
    const plaintextChunks = [];        
    while (index < length - chunkSize) { // Process all chunks except the last one
        const ciphertextChunk = ciphertext.slice(index, index + chunkSize);
        const lastCiphertextBlock = ciphertextChunk.slice(-blocksize);
        const padCiphertextBlock = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: lastCiphertextBlock }, key, new Uint8Array());
        const fullCiphertextChunk = concat(ciphertextChunk, new Uint8Array(padCiphertextBlock));
        const plaintextChunk = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, key, fullCiphertextChunk); 
        plaintextChunks.push(...new Uint8Array(plaintextChunk)); 
        iv = lastCiphertextBlock;
        index += chunkSize;
    } 
    const ciphertextChunk = ciphertext.slice(index, index + chunkSize); // Process last chunk
    const plaintextChunk = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, key, ciphertextChunk); 
    plaintextChunks.push(...new Uint8Array(plaintextChunk));  
    return String.fromCharCode.apply(null, plaintextChunks);
}
function concat(a, b) { 
    const c = new (a.constructor)(a.length + b.length);
    c.set(a, 0);
    c.set(b, a.length);
    return c;
}
function b642ab(base64String){
    return Uint8Array.from(window.atob(base64String), c => c.charCodeAt(0));
}

})();

© www.soinside.com 2019 - 2024. All rights reserved.