Vaclav Honzik
Vaclav Honzik

Reputation: 33

Crypto.decipher.final for 'aes-256-cbc' algorithm with invalid key fails with bad decrypt

I am able to use use node.js Crypto module to encrypt and decrypt a message using Cipher and Decipher classes with 'aes-256-cbc' algorithm like so:

var crypto = require('crypto');

var cipherKey = crypto.randomBytes(32); // aes-256 => key length is 256 bits => 32 bytes
var cipherIV = crypto.randomBytes(16); // aes block size = initialization vector size = 128 bits => 16 bytes
var cipher = crypto.createCipheriv('aes-256-cbc', cipherKey, cipherIV);

var message = 'Hello world';
var encrypted = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
console.log('Encrypted \'' + message + '\' as \'' + encrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Encrypted 'Hello world' as '2b8559ce4227c3c3c200ea126cb50957' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'

var decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, cipherIV);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
console.log('Decrypted \'' + encrypted + '\' as \'' + decrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Decrypted '2b8559ce4227c3c3c200ea126cb50957' as 'Hello world' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'

However when I try to decrypt the message using a wrong key to, perhaps naively, demonstrate an attacker will not be able decrypt the message unless the key is known, I get Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt at Decipheriv.final (internal/crypto/cipher.js:164:28):

var differentCipherKey = crypto.randomBytes(32);
var decipherDifferentKey = crypto.createDecipheriv('aes-256-cbc', differentCipherKey, cipherIV);
decrypted = decipherDifferentKey.update(encrypted, 'hex', 'utf8') + decipherDifferentKey.final('utf8');

What was I was hoping to get is unintelligible text. bad decrypt was featured in other SO questions either regarding openssl version mismatch between encrypting and decrypting or too-short initialization vector in the same case but I believe my case is a different scenario. Does AES somehow known that encrypted text was generated with a different key?

Tested on node v12.13.0 on Windows 10 and also in repl.it running v10.16.0.

EDIT: As suggested in the answers the issue was with default padding, in order to see unintelligible output one needs to disable auto-padding on both cipher and deciphers and pad manually:

var requirePadding = 16 - Buffer.byteLength(message, 'utf8');
var paddedMessage = Buffer.alloc(requirePadding, 0).toString('utf8') + message;
cipher.setAutoPadding(false)

Full example here

Upvotes: 3

Views: 2381

Answers (2)

kelalaka
kelalaka

Reputation: 5636

The CBC mode requires padding, you did not define one, but the library applied one for you as default. The default is PKCS7Padding which supports from 1 to up to 256 bytes of the block size.

Each padding has a specific format so that it can be uniquely removed from the decrypted text without ambiguity. For example, if the plaintext is missing two characters to match the block size, 16-byte in AES, then the PKCS7 padding adds 0202 (in hex) indicating that 2 characters are added and each has a value as the number of added characters. If 5 characters are missing 0505050505, etc. In the below xy is a byte.

xyxyxyxyxyxyxyxyxyxyxyxyxyxyxy01
xyxyxyxyxyxyxyxyxyxyxyxyxyxy0202
xyxyxyxyxyxyxyxyxyxyxyxyxy030303
...
xyxy0E0E0E0E0E0E0E0E0E0E0E0E0E0E
xy0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F

and if the last block is a full block, a new block completely filled with padding

xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxy 10101010101010101010101010101010

After the decryption, firstly the padding is checked. if the padding is not in the correct format as specified in rfc 2315, then one can say that there is a padding error.

In this case, while decrypting the library checks the padding and warns you about this. To prevent the padding oracle attacks you don't get an incorrect padding warning. You get a bad decrypt.

The library knows whether the key results with a valid padding or not, nothing more. There may be more than one key that results with valid padding even with a negligible probability where integrity is helpful.

In modern Cryptography, we don't use CBC mode anymore. We prefer Authenticated Encryption (AE) modes like AES-GCM or ChaCha20-Poly1305. AE modes provide confidentiality, integrity, and authentication in a bundle.

THE Galois Counter Mode (GCM), internally uses CTR mode in which there is no padding therefore they are free from padding oracle attacks.

Upvotes: 4

apsillers
apsillers

Reputation: 115970

Another answer has correctly identified the issue as a padding problem. I might summarize the issue like so:

  • Block ciphers can only operate on data that has a length that is a multiple of the cipher's block size. (AES has a block size of 128 bits.)
  • In order to make variously-sized inputs conform to the block size, the library adds padding. This padding has a particular format (For example, when adding padding of length N, repeat the value N for the last N bytes of the input.)
  • When decrypting, the library checks that correct padding exists. Since your badly-decrypted data is arbitrary noise, it is very unlikely to have a valid pad.

You may turn this check off with decipher.setAutoPadding(false) before you do update. However, note that this will include the padding in your decrypted output. Here is a modified repl.it instance that uses setAutoPadding.

Upvotes: 3

Related Questions