Cryptography in NodeJS: when the ciphertext is longer than the cleartext

So, a new year, a new set of assignments… one being that I’ve been given the job to write a comms driver for a series of electricity meters. These meters happen to use encryption using the AES-256-CBC cipher, and I needed to talk to these from within a NodeJS environment.

No problem I thought, there’s a crypto module. I muddled my way through the documentation… wrote pack and unpack functions that process the packets for the meter… then began writing unit tests.

Testing the pack function went fine, but when I came to the unpack, I hit a strange issue. I was getting truncated data. WTF? I added some console.log statements to see what was going on…

write 8b6fbcd1964bc93b6dcd6be409ff0b6b15c11f0764b6c54b02f515b83cd1b164e620d753c349ac45bcf3eca31f93de4c - 48 bytes
- read cd25400dc12613687df9ce2f9f6161d23c2d1e0f000000000000000000000000
read done
read done
read cd25400dc12613687df9ce2f9f6161d23c2d1e0f000000000000000000000000 - 32 bytes

So this is using the stream interface of the Decipher object. Initially this is what I used since that was the first example I came to in the documentation. Did I miss an error? No, nothing there. Did I need to break up the input into smaller pieces instead of writing it in one gulp? No, that didn’t help.

The only thing that was async in my async function was the cryptographic steps… so I thought maybe reverting to update/final might help? No, although it did mean I could drop async/await so that wasn’t so bad.

Nothing made sense… then I had a look at what pack was doing… I just assumed ciphertext output would be the same size as the cleartext. Was it? No, it was giving me extra, that I was then truncating when inserting it into the packet. Where did this extra junk come from?

In the end, the problem was right here:

    if (decrypt) {
        const ciphertext = packet.fields.inner.raw;

        /* Next IV will be the last 128 bits */
        next_iv =
            ciphertext, ciphertext.length - 16, ciphertext.length

        let cleartext = [];
        let decipher = crypto.createDecipheriv(
            cipher_algo || CIPHER_ALGO, key, iv
        decipher.setAutoPadding(false); // ← HERE!!!


        packet.fields.inner.raw = Buffer.concat(cleartext);

NodeJS by default, pads the data you give it. In my code, I am already padding the input, as I actually need to know how many bytes of the input are padding bytes so I can compute/validate CRC checksums and do things the way the meter does them.

Naturally, setAutoPadding needs to be called both sides. I had to update my test cases for pack, but now at least, I get out the same quantity of data I put in both sides with no surprises.