nodejs / help

:sparkles: Need help with Node.js? File an Issue here. :rocket:
1.44k stars 276 forks source link

AES GCM Auth Tag validation issue #4439

Open egorbeliy opened 3 days ago

egorbeliy commented 3 days ago

Version

v21.7.1

Platform

Darwin Egors-MacBook-Pro.local 23.4.0 Darwin Kernel Version 23.4.0: Wed Feb 21 21:45:49 PST 2024; root:xnu-10063.101.15~2/RELEASE_ARM64_T6020 arm64

Subsystem

crypto

What steps will reproduce the bug?

I tried to decrypt the cipher text and i was wondering that i don't need to use Auth Tag which is mandatory in AES GCM. You can run the code bellow:

import { createDecipheriv, randomBytes } from 'crypto'

const algorithm = 'aes-256-gcm'
const keyHex = '9b25a4b717d0c827c926565758b99b89a24f83c03a6a8319fb0fc809787ae929'
const ivHex = 'ded281917f01b9d5f0a5abce'
const key: Buffer = Buffer.from(keyHex, 'hex');
const iv: Buffer = Buffer.from(ivHex, 'hex');

// const encrypt = (text: string): string => {
//   const cipher = createCipheriv(algorithm, key, iv)
//   const start = cipher.update(text, 'utf8')
//   const end = cipher.final()
//   return Buffer.concat([ start, end]).toString('base64')
// }
// const encrypted = encrypt('testValue')

const encrypted = "njvgROnsKdsG" //testValue string in base64 encrypted by the key and iv above
console.log('encrypted ---- ', encrypted)

const decrypt = (encrypted: string): string => {
  const buffer = Buffer.from(encrypted, 'base64')
  const decipher = createDecipheriv(algorithm, key, iv)
  const decrypted = Buffer.concat([decipher.update(buffer), decipher.final()])
  return decrypted.toString('utf8')
}

const decrypted = decrypt(encrypted)

console.log('decrypted ---- ',decrypted)

How often does it reproduce? Is there a required condition?

No response

What is the expected behavior? Why is that the expected behavior?

Mac authentication error

What do you see instead?

successfully decrypted text

Additional information

No response

tniessen commented 3 days ago

To decrypt, you need to use createDecipheriv() instead of createCipheriv(). You then need to call decipher.setAuthTag() with the correct authentication tag, which you can obtain from the the original cipher after encryption as cipher.getAuthTag().

egorbeliy commented 3 days ago

it does absolutely the same if i use createDecipheriv(). So decryption works without AuthTag and the issue is relevant. i have changed the code for your convenience.

egorbeliy commented 3 days ago

@tniessen please test it out because it's critical security issue.

tniessen commented 3 days ago

@egorbeliy In Node.js 22, I am seeing this error when running your code:

Uncaught Error: Unsupported state or unable to authenticate data
    at Decipheriv.final (node:internal/crypto/cipher:184:29)
    at decrypt (REPL14:4:70)
egorbeliy commented 3 days ago

But for me it works well, is node version the same?

tsc aes-issue.ts 
node aes-issue.js

Feel free to check the video PoC https://drive.google.com/file/d/1HRQW7toSjHuPTrF4b6s5XoS1Kb1msSzM/view?usp=sharing

tniessen commented 2 days ago
  1. Node.js 21 is not supported anymore, so instead please test with Node.js 20 or Node.js 22.
  2. If there is a difference in behavior across versions of Node.js, it is likely due to differences in behavior across versions of OpenSSL. Since much of the node:crypto module is a thin wrapper around OpenSSL, we generally don't provide stronger security guarantees than OpenSSL. In other words, if this issue exists in a supported version of Node.js, it will be necessary to demonstrate that the issue does not also exist in OpenSSL.
  3. If you are right and this is a security issue, then this was reported in violation of our security policies.
egorbeliy commented 2 days ago

so i got the same behaviour on v20 and v22. Basically i got your point, you won't check it. thanks.

tniessen commented 2 days ago

@egorbeliy Alright, I'll give it another try. Here's the code I am using (main.mjs):

import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const algorithm = 'aes-256-gcm';
const keyHex = '9b25a4b717d0c827c926565758b99b89a24f83c03a6a8319fb0fc809787ae929';
const ivHex = 'ded281917f01b9d5f0a5abce';
const key = Buffer.from(keyHex, 'hex');
const iv = Buffer.from(ivHex, 'hex');

const encrypt = (text) => {
  const cipher = createCipheriv(algorithm, key, iv);
  const start = cipher.update(text, 'utf8');
  const end = cipher.final();
  return Buffer.concat([start, end]).toString('base64');
};
const encrypted = encrypt('testValue');
console.assert(encrypted === 'njvgROnsKdsG');
console.log('encrypted ---- ', encrypted);

const decrypt = (encrypted) => {
  const buffer = Buffer.from(encrypted, 'base64');
  const decipher = createDecipheriv(algorithm, key, iv);
  const decrypted = Buffer.concat([decipher.update(buffer), decipher.final()]);
  return decrypted.toString('utf8');
};

const decrypted = decrypt(encrypted);
console.log('decrypted ---- ', decrypted);

Here's what happens in Node.js 22.3.0 on Linux 6.1.92-1-MANJARO:

$ cat package.json 
{
  "dependencies": {
    "typescript": "^5.5.3"
  }
}
$ node -v
v22.3.0
$ node -p process.versions.openssl
3.3.1
$ node main.mjs 
encrypted ----  njvgROnsKdsG
node:internal/crypto/cipher:184
  const ret = this[kHandle].final();
                            ^

Error: Unsupported state or unable to authenticate data
    at Decipheriv.final (node:internal/crypto/cipher:184:29)
    at decrypt (file:///home/tniessen/testgcm/main.mjs:22:70)
    at file:///home/tniessen/testgcm/main.mjs:26:19
    at ModuleJob.run (node:internal/modules/esm/module_job:262:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:474:24)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:109:5)

Node.js v22.3.0

Here's what happens in Node.js 22.1.0 on Windows 10:

encrypted ----  njvgROnsKdsG
node:internal/crypto/cipher:184
  const ret = this[kHandle].final();
                            ^

Error: Unsupported state or unable to authenticate data
    at Decipheriv.final (node:internal/crypto/cipher:184:29)
    at decrypt (file:///C:/Users/Tobias/main.mjs:22:70)
    at file:///C:/Users/Tobias/main.mjs:26:19
    at ModuleJob.run (node:internal/modules/esm/module_job:262:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:474:24)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:119:5)

Node.js v22.1.0

Here's what happens in Node.js 20.15.0 on Linux 6.5.0-1022-azure:

$ node main.mjs 
encrypted ----  njvgROnsKdsG
node:internal/crypto/cipher:193
  const ret = this[kHandle].final();
                            ^

Error: Unsupported state or unable to authenticate data
    at Decipheriv.final (node:internal/crypto/cipher:193:29)
    at decrypt (file:///home/tniessen/dev/dblp/main.mjs:22:70)
    at file:///home/tniessen/dev/dblp/main.mjs:26:19
    at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:123:5)

Node.js v20.15.0

Could you please test the code I provided above to avoid any dependency on third-party tools, such as typescript?

egorbeliy commented 2 days ago

don't see the reason to test your code, coz i've reported the bug with my own. thanks for your help.

tniessen commented 2 days ago

@egorbeliy I was trying to help. If you don't want to debug this issue together, there is not much I can do for you.

RedYetiDev commented 12 hours ago

don't see the reason to test your code, coz i've reported the bug with my own.

thanks for your help.

Also, please note that @tniessen's code is nearly identical to yours——so any discrepancies between your execution and his will help to debug and address the issue (as @tniessen said).

This information is crucial in the process of this issue, and without it, there's not much that be done to help.