AES 256 GCM encryption decryption in nodejs
Asked Answered
S

3

7

I am implementing a basic encryption/decryption set of functions in nodejs and I keep getting the following error in the decryption part:
Error: Unsupported state or unable to authenticate data

This is my code so far:

import crypto from 'crypto'
import logger from './logger'

const ALGORITHM = 'aes-256-gcm'

export const encrypt = (keyBuffer, dataBuffer, aadBuffer) => {
  // iv stands for "initialization vector"
  const iv = Buffer.from(crypto.randomBytes(12), 'utf8')
  logger.debug('iv: ', iv)
  const encryptor = crypto.createCipheriv(ALGORITHM, keyBuffer, iv)
  logger.debug('encryptor: ', encryptor)
  logger.debug('dataBuffer: ', dataBuffer)
  return Buffer.concat([iv, encryptor.update(dataBuffer, 'utf8'), encryptor.final()])
}

export const decrypt = (keyBuffer, dataBuffer, aadBuffer) => {
  const iv = dataBuffer.slice(0, 96)

  const decryptor = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv)
  return Buffer.concat([decryptor.update(dataBuffer.slice(96), 'utf8'), decryptor.final()])
}

My error happens in the last line of the decrypt function. I am storing the iv as part of the dataBuffer.

Thanks in advance!

Shumaker answered 12/11, 2018 at 19:1 Comment(1)
I might be wrong on this, but isn't the slice specifying the number of bytes (not bits)? If that is the case, then you should be slicing 12 bytes, not 96.Nates
S
9

I realized I had made a couple of mistakes with the original code that I posted, one of them as @TheGreatContini remarked was the size of the slicing which was being done in bits instead of bytes as it should be. Still, the biggest piece that I was missing was the authTag which always should be included in the decipher function setup.

Here is my working code for anybody interested for future references:

import crypto from 'crypto'
import logger from './logger'

const ALGORITHM = 'aes-256-gcm'

export const encrypt = (keyBuffer, dataBuffer, aadBuffer) => {
  // iv stands for "initialization vector"
  const iv = crypto.randomBytes(12)
  const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv)
  const encryptedBuffer = Buffer.concat([cipher.update(dataBuffer), cipher.final()])
  const authTag = cipher.getAuthTag()
  let bufferLength = Buffer.alloc(1)
  bufferLength.writeUInt8(iv.length, 0)
  return Buffer.concat([bufferLength, iv, authTag, encryptedBuffer])
}

export const decrypt = (keyBuffer, dataBuffer, aadBuffer) => {
  const ivSize = dataBuffer.readUInt8(0)
  const iv = dataBuffer.slice(1, ivSize + 1)
  // The authTag is by default 16 bytes in AES-GCM
  const authTag = dataBuffer.slice(ivSize + 1, ivSize + 17)
  const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv)
  decipher.setAuthTag(authTag)
  return Buffer.concat([decipher.update(dataBuffer.slice(ivSize + 17)), decipher.final()])
}
Shumaker answered 13/11, 2018 at 2:3 Comment(0)
C
1

There are some enhancements not present in the solution vanvasquez provided that I would like to post and share here. These namely include:

  1. A way to securely generate the keyBuffer
  2. Removing bufferLength because it is not necessary
  3. Adding a salt value (this makes the encryption more secure)
  4. Sharing an example of how this may be called

I've got a slightly longer example posted here explaining how the code below may be used with MessagePack in order to reduce the size of the encrypted text (useful if you are saving this encrypted text on file or sending it to another client/machine).

const crypto = require("crypto");

const password = "pw";
const algorithm = "aes-256-gcm";
const hash = "sha512";
const iterations = 2145;
const keyLen = 32;
const ivLen = 12;
const saltLen = 32;

// Create random values
const iv = crypto.randomBytes(24).toString("hex").slice(0, ivLen);
const salt = crypto.randomBytes(64).toString("hex").slice(0, saltLen);

let rawText = "string to encrypt";

// encrypt
const ivBuffer = Buffer.from(iv, "utf8");
const saltBuffer = Buffer.from(salt, "utf8");

const key = crypto.pbkdf2Sync(password, saltBuffer, iterations, keyLen, hash);
const cipher = crypto.createCipheriv(algorithm, key, ivBuffer);
const encrypted = Buffer.concat([cipher.update(Buffer.from(rawText, "utf8")), cipher.final()]);
const tag = cipher.getAuthTag();

let encryptedBuffer = Buffer.concat([saltBuffer, ivBuffer, tag, encrypted]);
let output = encryptedBuffer.toString("base64");


// decrypt
let buffer2 = Buffer.from(output, "base64");

const copiedBuf = Uint8Array.prototype.slice.call(buffer2);
const salt2 = copiedBuf.slice(0, saltLen);
const iv2 = copiedBuf.slice(saltLen, saltLen + ivLen);
const tag2 = copiedBuf.slice(saltLen + ivLen, saltLen + ivLen + 16);
const encrypted2 = copiedBuf.slice(saltLen + ivLen + 16);
const key2 = crypto.pbkdf2Sync(password, salt2, iterations, keyLen, hash);

const decipher = crypto.createDecipheriv(algorithm, key2, iv2);
decipher.setAuthTag(tag2);

const decrypted = Buffer.concat([decipher.update(encrypted2), decipher.final()]);

// result
console.log(decrypted.toString());
Carmarthenshire answered 4/5, 2024 at 5:34 Comment(1)
This is the best answer I have ever seenDunton
L
0

Here's a simple Javascript solution for encryption and decryption:

const crypto = require("crypto");
const assert = require("assert");

const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(12);

function encrypt(data) {
    const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
    return Buffer.concat([cipher.update(data), cipher.final(), cipher.getAuthTag()]).toString("base64");
}

function decrypt(data) {
    const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
    const buffer = Buffer.from(data, "base64");
    decipher.setAuthTag(buffer.subarray(-16));
    return Buffer.concat([decipher.update(buffer.subarray(0, -16)), decipher.final()]).toString();
}

const sample = "The quick brown fox jumps over the lazy dog";
assert(decrypt(encrypt(sample)) === sample);
Lindbergh answered 3/6, 2024 at 14:6 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.