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

tv6aics1  于 2023-03-28  发布在  Java
关注(0)|答案(2)|浏览(125)

我们正在尝试在Javascript(浏览器)中构建大文件的流解密。由于加密本身不支持流解密。微妙的是,我们正在逐块进行。这应该可以通过提供前一个块作为当前块的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);
vmpqdwk3

vmpqdwk31#

如果你可以切换模式,另一个答案中建议的CTR模式是更方便的方法,因为WebCrypto API(像大多数库一样)会自动禁用流密码模式(如CTR)的填充,而它会自动启用块密码模式(如CBC)。
但是,如果您无法更改模式,并且仍然希望使用WebCrypto API,则必须在解密之前将带有加密填充的密文块添加到每个密文块中,如发布的代码中所示,因为WebCrypto API不允许对CBC等块密码模式禁用PKCS#7填充。
发布的代码可以进行优化:当前代码使用16字节作为块大小。此外,为了确定要附加的密文块,对每个密文块加密完整的填充块,由于填充而导致2块密文,其中第二个块必须被丢弃。
对于优化:

  • 首先,块大小应该选择得尽可能大,尽管为了简单起见,它仍然应该是块大小的整数倍。
  • 其次,为了确定要附加的密文块,应该加密一个空块,由于填充,这只提供要附加的密文块。

以下实现考虑了这两种情况:

(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));
}

})();
mqkwyuun

mqkwyuun2#

简单解决方案:使用CTR模式。这不会填充,你可以简单地计算下一个块的计数器。你可以在AesCtrParams中指示整个(无符号大端)计数器,所以你应该是黄金的。计数器需要为每16字节的数据块增加1。如果你聪明的话,你当然可以让你的块成为16字节的倍数。
请注意,没有密码提供与使用传输模式安全性/ TLS相同的安全性。
请记住,CTR模式是未经身份验证的,尽管填充oracle攻击在CTR上是不可能的(显然),但由此产生的密文容易受到位翻转攻击,因此,明文oracle也容易受到攻击。因此,您可能需要在nonce和密文上包含一个HMAC身份验证标签,并在使用解密数据之前验证该标签。
因此,如果您想在服务器端存储数据(“静态数据”),则在客户端使用CBC或CTR进行加密非常有用。

相关问题