Let's say we have a file named foobar.enc and a script called decrypt-from-stdin-to-stdout.js. the file foobar.enc contains an AES-encrypted text. This text is unencrypted as follows: "foobar".
The script is being called in this way:
cat foobar.enc | node decrypt-from-stdin-to-stdout.js
Expected output on console should be: "foobar" But it is "fooba" instead.
Im am working with Windows 10 Professional and Cygwin and Node v20.9.0. Another test with same input file and script on a native Debian system yields the same strange result.
So what is the problem with the script? This is the script:
const crypto = require('crypto');
const zlib = require('zlib');
function createCipherKeyBuffer(passwordStr) {
return crypto.createHash('sha256').update(passwordStr).digest();
}
const stdoutStream = process.stdout;
const stdinStream = process.stdin;
let decipherStream;
let initVectBufferCreated = false;
stdinStream.on('data', inputBuffer => {
if (inputBuffer.length >= 16 && !initVectBufferCreated) {
const initVectBuffer = inputBuffer.subarray(0, 16);
initVectBufferCreated = true;
const unzipStream = zlib.createUnzip();
const cipherKeyBuffer = createCipherKeyBuffer('power237');
decipherStream = crypto.createDecipheriv('aes256', cipherKeyBuffer, initVectBuffer);
/*stdinStream.on('end', () => {
console.error('stdinStream on end');
});*/
decipherStream.write(inputBuffer.subarray(16, inputBuffer.length));
decipherStream
.pipe(unzipStream)
.pipe(stdoutStream);
} else if (inputBuffer.length < 16) {
console.error('input too small');
process.exit(1);
} else {
decipherStream.write(inputBuffer);
}
});
Some hints:
I know using passwords in source code is a bad habit. But this code is not for production purposes but only for test. I would just like to understand the main problem.
When I use a larger text like "foobarbazboz", everything is working fine. The file foobarbazboz.enc contains the encrypted text.
Calling like:
cat foobarbazboz.enc | node decrypt-from-stdin-to-stdout.js
This outputs (as expected):
"foobarbazboz"
This is the Base64-encoded data for the file foobar.enc:
9BoTOZenA+Ynw0m9/Qzqij9Wzw/VkkuVGXR/jm2+YneeQ08L75ylM6gk+EmhwYJE
And this is the Base64-encoded data for the file foobarbazboz.enc:
zKTsPq7wvdW7dxSj/tNTtC9R7sAvPhrBsgbFDcvSE7CgvGY7DWyBsDtroOh1gOVwUhCv3TbBCb8Ar+UU3DeDAw==
Since the both input files contains binary (encrypted) data and because I want to avoid external resources, I have decided to encode them with Base64 and print them here. I am sorry for that circumstance. So please first decode the strings and write them to the test files mentioned above.
You can decode them like as follows:
echo "9BoTOZenA+Ynw0m9/Qzqij9Wzw/VkkuVGXR/jm2+YneeQ08L75ylM6gk+EmhwYJE" | base64 -d > foobar.enc
echo "zKTsPq7wvdW7dxSj/tNTtC9R7sAvPhrBsgbFDcvSE7CgvGY7DWyBsDtroOh1gOVwUhCv3TbBCb8Ar+UU3DeDAw==" | base64 -d > foobarbazboz.enc
When using streams in Node.js, the data may not always arrive in chunks that match your expectations, especially when small files are involved. This can lead to incomplete decryption, as some data might not get processed before the stream ends.
You need to ensure the decipherStream finishes processing all data and flushing its buffer when the input stream ends.
Here is the quick updated code:
const crypto = require('crypto');
const zlib = require('zlib');
function createCipherKeyBuffer(passwordStr) {
return crypto.createHash('sha256').update(passwordStr).digest();
}
const stdoutStream = process.stdout;
const stdinStream = process.stdin;
let decipherStream;
let initVectBufferCreated = false;
stdinStream.on('data', (inputBuffer) => {
if (inputBuffer.length >= 16 && !initVectBufferCreated) {
const initVectBuffer = inputBuffer.subarray(0, 16);
initVectBufferCreated = true;
const unzipStream = zlib.createUnzip();
const cipherKeyBuffer = createCipherKeyBuffer('power237');
decipherStream = crypto.createDecipheriv('aes256', cipherKeyBuffer, initVectBuffer);
// Pipe decipherStream and unzipStream to stdout
decipherStream
.pipe(unzipStream)
.pipe(stdoutStream);
// Write the remaining data to the decipherStream
decipherStream.write(inputBuffer.subarray(16));
} else if (inputBuffer.length < 16) {
console.error('input too small');
process.exit(1);
} else {
// Write subsequent data chunks to the decipherStream
decipherStream.write(inputBuffer);
}
});
// Ensure streams flush properly on end
stdinStream.on('end', () => {
if (decipherStream) {
decipherStream.end(); // Flush decipherStream
}
});
Make sure you call decipherStream.end()
to flush all data from the stream pipeline