Skip to content

Instantly share code, notes, and snippets.

@AndiDittrich
Last active November 10, 2024 10:42
Show Gist options
  • Save AndiDittrich/4629e7db04819244e843 to your computer and use it in GitHub Desktop.
Save AndiDittrich/4629e7db04819244e843 to your computer and use it in GitHub Desktop.
Node.js - AES Encryption/Decryption with AES-256-GCM using random Initialization Vector + Salt
// SPDX-License-Identifier: MPL-2.0
// AES Encryption/Decryption with AES-256-GCM using random Initialization Vector + Salt
// ----------------------------------------------------------------------------------------
// the encrypted datablock is base64 encoded for easy data exchange.
// if you have the option to store data binary save consider to remove the encoding to reduce storage size
// ----------------------------------------------------------------------------------------
// format of encrypted data - used by this example. not an official format
//
// +--------------------+-----------------------+----------------+----------------+
// | SALT | Initialization Vector | Auth Tag | Payload |
// | Used to derive key | AES GCM XOR Init | Data Integrity | Encrypted Data |
// | 64 Bytes, random | 16 Bytes, random | 16 Bytes | (N-96) Bytes |
// +--------------------+-----------------------+----------------+----------------+
//
// ----------------------------------------------------------------------------------------
// Input/Output Vars
//
// MASTERKEY: the key used for encryption/decryption.
// it has to be cryptographic safe - this means randomBytes or derived by pbkdf2 (for example)
// TEXT: data (utf8 string) which should be encoded. modify the code to use Buffer for binary data!
// ENCDATA: encrypted data as base64 string (format mentioned on top)
// load the build-in crypto functions
const _crypto = require('crypto');
// encrypt/decrypt functions
module.exports = {
/**
* Encrypts text by given key
* @param String text to encrypt
* @param Buffer masterkey
* @returns String encrypted text, base64 encoded
*/
encrypt: function (text, masterkey){
// random initialization vector
const iv = _crypto.randomBytes(16);
// random salt
const salt = _crypto.randomBytes(64);
// derive encryption key: 32 byte key length
// in assumption the masterkey is a cryptographic and NOT a password there is no need for
// a large number of iterations. It may can replaced by HKDF
// the value of 2145 is randomly chosen!
const key = _crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512');
// AES 256 GCM Mode
const cipher = _crypto.createCipheriv('aes-256-gcm', key, iv);
// encrypt the given text
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
// extract the auth tag
const tag = cipher.getAuthTag();
// generate output
return Buffer.concat([salt, iv, tag, encrypted]).toString('base64');
},
/**
* Decrypts text by given key
* @param String base64 encoded input data
* @param Buffer masterkey
* @returns String decrypted (original) text
*/
decrypt: function (encdata, masterkey){
// base64 decoding
const bData = Buffer.from(encdata, 'base64');
// convert data to buffers
const salt = bData.slice(0, 64);
const iv = bData.slice(64, 80);
const tag = bData.slice(80, 96);
const text = bData.slice(96);
// derive key using; 32 byte key length
const key = _crypto.pbkdf2Sync(masterkey, salt , 2145, 32, 'sha512');
// AES 256 GCM Mode
const decipher = _crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
// encrypt the given text
const decrypted = decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');
return decrypted;
}
};
@Matheus-Ribeiro95
Copy link

My script keeps returning null in decrypt function.

@AndiDittrich
Copy link
Author

@santhosh77h
masterkey is your shared encryption/decryption key, it will be derived by the pbkdf2 function for each unique dataset

@Matheus-Ribeiro95
did you tried to print the error message within the catch block ?

@jamesmosier
Copy link

jamesmosier commented Oct 23, 2017

@AndiDittrich what is the best way to generate the masterkey? What is the best "cryptography method" to do this? I am fairly novice when it comes to cryptography so anything to point me in the right direction would be great.

Thanks for the Gist as well!

Edit: I found an article that explains it a little bit. Let me know if this is on the right track for masterkey generation. Here is what I did to generate the masterkey:

var crypto = require('crypto');
var initializationVector = crypto.randomBytes(32);
console.log(initializationVector.toString('base64'));

@SamWeiss1990
Copy link

great!!

@sravanbommana
Copy link

@AndiDittrich decrypt function returning null.
Getting error message like Invalid IV length.
Thanks for the help!

@niryeffet
Copy link

niryeffet commented Mar 16, 2018

@sravan-ui, Fix for the IV length problem: https://gist.github.com/niryeffet/99595f7a70db38acf238d7c3366b3f49

@sravanbommana
Copy link

@niryeffet , Thank you so much! It's working now.

@et
Copy link

et commented Mar 22, 2018

I haven't done a cryptography class in awhile but is there a compelling reason to choose GCM over CBC or ECB?

@AndiDittrich
Copy link
Author

@et

gcm provides an "integrity check" using the auth-tag. cbc doesn't (could implemented by using hmac).
ecb shouldn't be used because its using the same key for each block.

@SReject
Copy link

SReject commented May 1, 2018

As recommended by Nodejs's documentation, you should be using Buffer.from(data, 'base64') instead of new Buffer(data, 'base64')

@afalahi
Copy link

afalahi commented Sep 24, 2018

Hey, great gist, but why are we using a derived key along with a nonce? Isn't the nonce supposed to protect the master key from multiple ciphertexts?

@jkossis
Copy link

jkossis commented Oct 3, 2018

@AndiDittrich, what was the motivation behind choosing 2145 as the number of iterations for pbkdf2Sync? I see a lot of competing opinions (many say to use at least 10000), and wanted to see what your though process was. I appreciate the insight.

@AndiDittrich
Copy link
Author

@afalahi
for each encryption we derive a unique key from our masterkey using salt - thats a common practice.
additionally aes-gcm/cbc requires a unique IV (in relation to the derived key) to be secure.
in this special case we avoid possible IV collisions (not checked in the snippet; IV is only 16 bytes long) by deriving unique keys (with a long salt)

@jkossis
of course...it's a random picked number. generally the pbkdf2 function is designed to derive a cryptographic secure key from a user generated password - therefore it is recommended to use a high number of iteration (currently 65k...128k) to slow down the process (brute forcing the password).
in this particular case we derive a cryptographic key from a cryptographic master key - which is randomly generated.

@Sathish474
Copy link

Sathish474 commented Oct 24, 2019

is there any way we can restrict the size of the encrypted value using AES-256-GCM mode(I am using 32bit length master key)? Any help is highly appreciated. Thanks!

@AndiDittrich
Copy link
Author

the size of the encrypted data is fully predictable: data.length +96 bytes
just limit the input buffer length.

btw. you should use a stronger masterkey...

@nareshnelluri
Copy link

Hi AndiDittrich,

Is there anyway to consume this in angular application?

@AndiDittrich
Copy link
Author

in case the browser supports crypto api you can use this as regular js module

@adjeteyboy
Copy link

I need similar for aes-128-cbc. Any help?

@AndiDittrich
Copy link
Author

just replace the cipher and modify the iv and key length (/2)

@SilviaIenciu
Copy link

Hi AndiDittrich,
What is license of your gist files?

@AndiDittrich
Copy link
Author

Hi AndiDittrich,
What is license of your gist files?

MPL-2.0

@Khane1
Copy link

Khane1 commented Feb 3, 2021

Hi AndiDittrich im trying to do encryption for visa and the requirement is to use an already set “Key ID” and "shared secret". I recognized the masterkey in your example is the shared secret but i don't know where i can plug the Key ID. Its has 36 characters.... https://developer.visa.com/capabilities/visa-in-app-provisioning/docs-authentication.... any help rendered is appreciated thanks.

@AndiDittrich
Copy link
Author

please ask their support for assistance

@Rob-pw
Copy link

Rob-pw commented Feb 7, 2021

Note on using a random initialisation vector: https://crypto.stackexchange.com/a/67565/84696, starts to fail around 2^33 or about 4 billion messages signed with the same key (collisions increase to around 50% after this point) - dreadful paraphrasing.

@AndiDittrich
Copy link
Author

of course, therefore a "unique" key is derived via pbkdf2

@limplash
Copy link

limplash commented Jul 9, 2021

I made a small typescript package based on this gist

@anant1222
Copy link

I'm getting "Unsupported state or unable to authenticate data." this error, when trying to run this line code
var decrypted = decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');

i don't know what to do.pls help regarding this , if some had faced same issues.

@randomAn0nym0us
Copy link

Hi,

I have size constraints on the cipher text length.
Hence implemented the below with smaller salt and iv.
If you can afford a bigger cipher text with bigger salt, then use andi's code.

https://gist.github.com/randomAn0nym0us/ab0f152668bf9c9b8e1e8aebadd0d8f2

*** I have different key lengths, slightly different logic to derive iterations, refinedSalt
and different order of concatenation for my company code.

But shared the overview of the changes. Hope this satisfies the license requirements
to share the modifications without exposing somewhat sensitive info.

Thank you for sharing your code.

@hieuntcasso
Copy link

how about with cryptoJS

@f3ndot
Copy link

f3ndot commented Sep 20, 2023

The IV for AES-GCM should be 12 bytes/96-bit per NIST:

For IVs, it is recommended that implementations restrict support to the length of 96 bits, to
promote interoperability, efficiency, and simplicity of design.

See Section 8.2 in the NIST 800-38D pdf to details as to why

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment