Node.js · Report #3463949
Missing AES-GCM Authentication Tag Validation and Improper Deprecation Handling
How it was found
Summary:
In Node.js' crypto module, the createDecipheriv states that "the authTagLength option defaults to 16 bytes and must be set to a different value if a different length is used." (here)
The authentication tag's length is however not validated against that default value and can be truncated down to either 4, 8, 12, 13, 14, or 15 bytes. (here)
This allows an attacker to brute force an authentication tag after modifying a ciphertext and recover the key material to compute more authentication tags offline afterwards.
Description:
These authenticated encryption schemes are frequently used to protect some session or some token. An attacker could exploit this flaw by changing a valid ciphertext and by bruteforcing the new authentication tag on the server (with at most 12,884,902,656 requests : (2**32) * 3 + 2 * 256) to gain a valid session/token.
This also results in a nonce reuse with AES-GCM which leaks the key needed to compute any further authentication tags (see this blogpost). Note that 12 billion requests are not necessarily needed as one could use the truncated tag to recover the needed key (see this cryptopals challenge).
With the key needed to compute arbitrary authentication tags, an attacker can manipulate a ciphertext to decrypt it and to forge a new one.
I understand implicit shorter tag lengths have been deprecated with this PR, the deprecation warning however is only emitted when the tag is actually shorter than 16 bytes.
For people using the function as documented (i.e. expecting the authTagLength option to default to 16 bytes), the warning is never shown unless an attack is on the way.
Steps To Reproduce:
The following script shows how the default authTagLength of 16 bytes is not enforced and allows for implicit shorter tags.
It also shows that the deprecation warning is only shown when an attacker truncates the authentication tag (or when it's explicitly truncated, but that's not the scenario where people are in unknown danger).
const {
createCipheriv,
createDecipheriv
} = require('crypto');
const key = 'key0123456789key';
const nonce = '123456789012';
var algo = 'aes-128-gcm';
const cipher = createCipheriv(algo, key, nonce);
const plaintext = 'This is some plaintext';
const ciphertext = cipher.update(plaintext, 'utf8');
cipher.final();
const tag = cipher.getAuthTag();
var decipher = createDecipheriv(algo, key, nonce);
decipher.setAuthTag(tag);
var decryptedPlaintext = decipher.update(ciphertext, null, 'utf8') + decipher.final();
console.log('Decrypted with full tag: ' + decryptedPlaintext);
console.log('---------------------------------');
decipher = createDecipheriv(algo, key, nonce);
// Truncating the authentication tag
decipher.setAuthTag(tag.subarray(0, 4));
decryptedPlaintext = decipher.update(ciphertext, null, 'utf8') + decipher.final();
console.log('Decrypted with truncated tag: ' + decryptedPlaintext);
Observe how the deprecation warning message is only shown for the latter and how a truncated authentication tag successfully allowed decryption (even if "the authTagLength option defaults to 16 bytes and must be set to a different value if a different length is used.)
{F5115107}
For code references:
The deprecation warning message is emitted here.
The auth tag is set with setAuthTag in which the tag length taken here is set implicitly here.
Impact
- Ciphertexts can be modified to decrypt to an arbitrary value
- Ciphertexts can be decrypted by observing parsing differences
- For example, if the decrypted data is expected to be JSON, it's possible to observe valid/invalid JSON parsing to decrypt the ciphertext just like with padding oracles
- The GCM internal GHASH key can be recovered (to compute more arbitrary authentication tags without need for bruteforce)
- Requires rotation of the encryption keys
Considering how the createDecipheriv function is documented, this completely breaks the integrity and confidentiality of the encryption scheme that people expect from this API.