My understanding is that an AES block cipher in CTR mode allows, in theory, to decipher any location of a large file, without needing to read the whole file.
However, I don't see how to do this with nodejs crypto module. I could feed the Decipher.update
method with dummy blocks until I get to the part I'm interested in, at which point I would feed actual data read from the file, but that would be an awful hack, inefficient, and fragile, since I need to be aware of the block size.
Is there a way to do it with the crypto module, and if not, what module can I use?
I've found different approaches to solve this problem:
This answer is based on @ArtjomB. and @gusto2 comments and answer, which really gave me the solution. However, here is a new answer with a working code sample, which also shows implementation details (for example the IV must be incremented as a Big Endian number).
The idea is simple: to decrypt starting at an offset of n
blocks, you just increment the IV by n
. Each block is 16 bytes.
import crypto = require('crypto');
let key = crypto.randomBytes(16);
let iv = crypto.randomBytes(16);
let message = 'Hello world! This is test message, designed to be encrypted and then decrypted';
let messageBytes = Buffer.from(message, 'utf8');
console.log(' clear text: ' + message);
let cipher = crypto.createCipheriv('aes-128-ctr', key, iv);
let cipherText = cipher.update(messageBytes);
cipherText = Buffer.concat([cipherText, cipher.final()]);
// this is the interesting part: we just increment the IV, as if it was a big 128bits unsigned integer. The IV is now valid for decrypting block n°2, which corresponds to byte offset 32
incrementIV(iv, 2); // set counter to 2
let decipher = crypto.createDecipheriv('aes-128-ctr', key, iv);
let decrypted = decipher.update(cipherText.slice(32)); // we slice the cipherText to start at byte 32
decrypted = Buffer.concat([decrypted, decipher.final()]);
let decryptedMessage = decrypted.toString('utf8');
console.log('decrypted message: ' + decryptedMessage);
This program will print:
clear text: Hello world! This is test message, designed to be encrypted and then decrypted decrypted message: e, designed to be encrypted and then decrypted
As expected, the decrypted message is shifted by 32 bytes.
And finally, here is the incrementIV implementation:
function incrementIV(iv: Buffer, increment: number) {
if(iv.length !== 16) throw new Error('Only implemented for 16 bytes IV');
const MAX_UINT32 = 0xFFFFFFFF;
let incrementBig = ~~(increment / MAX_UINT32);
let incrementLittle = (increment % MAX_UINT32) - incrementBig;
// split the 128bits IV in 4 numbers, 32bits each
let overflow = 0;
for(let idx = 0; idx < 4; ++idx) {
let num = iv.readUInt32BE(12 - idx*4);
let inc = overflow;
if(idx == 0) inc += incrementLittle;
if(idx == 1) inc += incrementBig;
num += inc;
let numBig = ~~(num / MAX_UINT32);
let numLittle = (num % MAX_UINT32) - numBig;
overflow = numBig;
iv.writeUInt32BE(numLittle, 12 - idx*4);
}
}
Since CBC uses the previous cipher text block as IV, and that all cipher text blocks are known during the decryption stage, you don't have anything particular to do, you can decrypt at any point of the stream. The only thing is that the first block you decrypt will be garbage, but the next ones will be fine. So you just need to start one block before the part you actually want to decrypt.