skeggse
skeggse

Reputation: 6333

Node.js Crypto AES Cipher

For some odd reason, Node's built-in Cipher and Decipher classes aren't working as expected. The documentation states that cipher.update

"Returns the enciphered contents, and can be called many times with new data as it is streamed."

The docs also state that cipher.final

"Returns any remaining enciphered contents."

However, in my tests you must call cipher.final to get all of the data, thus rendering the Cipher object worthless, and to process the next block you have to create a new Cipher object.

var secret = crypto.randomBytes(16)
  , source = crypto.randomBytes(8)
  , cipher = crypto.createCipher("aes128", secret)
  , decipher = crypto.createDecipher("aes128", secret);

var step = cipher.update(source);
var end = decipher.update(step);

assert.strictEqual(source.toString('binary'), end); // should not fail, but does

Note that this happens when using crypto.createCipher or crypto.createCipheriv, with the secret as the initialization vector. The fix is to replace lines 6 and 7 with the following:

var step = cipher.update(source) + cipher.final();
var end = decipher.update(step) + decipher.final();

But this, as previously noted, renders both cipher and decipher worthless.

This is how I expect Node's built-in cryptography to work, but it clearly doesn't. Is this a problem with how I'm using it or a bug in Node? Or am I expecting the wrong thing? I could go and implement AES directly, but that would be time-consuming and annoying. Should I just create a new Cipher or Decipher object every time I need to encrypt or decrypt? That seems expensive if I'm doing so as part of a stream.

Upvotes: 1

Views: 6938

Answers (3)

Maarten Bodewes
Maarten Bodewes

Reputation: 94098

AES uses block sizes of 16 bytes (not two times 8 as you were suggesting). Furthermore, if padding is enabled it should always pad. The reason for this is that otherwise the unpadding algorithm cannot distinguish between padding and the last bytes of the plaintext.

Most of the time you should not expect the ciphertext to be the same size as the plain text. Make sure that doFinal() is always called. You should only use update this way for encryption / decryption if you are implementing your own encryption scheme.

Upvotes: 2

skeggse
skeggse

Reputation: 6333

I was having two problems: the first is that I assumed, incorrectly, that the size of a block would be 64 bits, or 8 bytes, which is what I use to create the "plaintext." In reality the internals of AES split the 128 bit plaintext into two 64 bit chunks, and go from there.

The second problem was that despite using the correct chunk size after applying the above changes, the crypto module was applying auto padding, and disabling auto padding solved the second problem. Thus, the working example is as follows:

var secret = crypto.randomBytes(16)
  , source = crypto.randomBytes(16)
  , cipher = crypto.createCipheriv("aes128", secret, secret); // or createCipher
  , decipher = crypto.createDecipheriv("aes128", secret, secret);

cipher.setAutoPadding(false);
decipher.setAutoPadding(false);

var step = cipher.update(source);
var end = decipher.update(step);

assert.strictEqual(source.toString('binary'), end); // does not fail

Upvotes: 4

Patrick
Patrick

Reputation: 8083

There's a node.js issue with calling update multiple times in a row. I suppose it's been solved and reflected in the next release.

Upvotes: 0

Related Questions