Skip to content

Instantly share code, notes, and snippets.

@randomAn0nym0us
Last active October 5, 2022 04:28
Show Gist options
  • Save randomAn0nym0us/ab0f152668bf9c9b8e1e8aebadd0d8f2 to your computer and use it in GitHub Desktop.
Save randomAn0nym0us/ab0f152668bf9c9b8e1e8aebadd0d8f2 to your computer and use it in GitHub Desktop.
Node.js - AES-256-GCM - random Initialization Vector + Salt - smaller size
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
// original code from - https://gist.github.com/AndiDittrich/4629e7db04819244e843#file-aesutil-js
// SPDX-License-Identifier: MPL-2.0
// https://www.mozilla.org/en-US/MPL/2.0/
// *** I have different key lengths
// *** and slightly different logic to derive iterations, refinedSalt
// *** and different order of concatenation for my company code
// but below is the overview of the changes
// hope this satisfies the license requirements to share the modifications
// without exposing somewhat sensitive info
// ideally you would want a bigger salt ( 32 or 64 byte and higher ) and avoid all the extra logic
// However, I am working with size constraints
// if you don't have size constraints, then refer the original gist code by AndiDittrich
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
// you can test this code at
// https://www.tutorialspoint.com/execute_nodejs_online.php
// you can get random keys from
// https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
const _crypto = require('crypto');
// ------------------------------ PARAMS ------------------
// masterKey 4096 bit ( 512 byte ) string
const masterKey = 'secret-512-bytes';
// plain text string
const data = 'secret-plain-text';
// expected length of the cipher for same length plain text
// use this if your plain text input is of constant length
// this changes depending on your plain text
const expectedCipherLen = 84;
// ------------------------------ ENCRYPT FUNCTION ------------------
function encrypt(data, masterKey) {
// get 12 byte ( 96 bit ) initialization vector - iv
// 12 byte seems to be the recommended length for aes-gcm
// https://crypto.stackexchange.com/questions/41601/aes-gcm-recommended-iv-size-why-12-bytes
const iv = _crypto.randomBytes(12);
// get 16 byte ( 128 bit ) salt
// use a bigger one like 32 or 64 byte and higher if you are not limited by size like me
// according to NIST recommendations, the minimum salt length should be 16 bytes.
// https://hexdocs.pm/pbkdf2_elixir/Pbkdf2.Base.html#gen_salt/1-salt-length-recommendations
const salt = _crypto.randomBytes(16);
// calculate iterations based on iv
// acii value of an iv char * 10
// e.g., last to 3rd char would be iv64.charAt(ivLen-3) - (avoiding base64 padded chars ==)
// so, iterations will be somewhat different for each encryption
// this I hope will make it more difficult to crack maybe
// Also, iterations count does not have to be kept secret
// https://crypto.stackexchange.com/questions/60860/should-number-of-iterations-of-pbkdf2-stay-secret
// get base64 encoded iv
const iv64 = iv.toString('base64');
const ivLen = iv64.length;
const iterations = 10000 + iv64.charAt(ivLen-3).charCodeAt(0) * 10;
// get num array of iterations
const iterArr = String(iterations).split("").map((num)=>{
return Number(num)
})
// get uint8Array to use with other buffers
const iterUint8Arr = Uint8Array.from(iterArr);
// get a bigger salt based on the randomly generated values we have
// ( salt, iv, iterations) combinations
// slicing 64 because the base length varies below or above 64
// we end up with a randomly chopped of salt at the end
// and I hope this refinedSalt will be safer than just the 16 byte salt
// if the logic of getting the refinedSalt is kept secret
// not sure if this is a good or a bad idea
let refinedSalt = Buffer.concat([
iv + iterUint8Arr + salt + iv + iterUint8Arr + salt
]);
refinedSalt = Uint8Array.prototype.slice.call(refinedSalt, 0, 64);
// get a 32 byte key from the 512 byte masterKey
const key = _crypto.pbkdf2Sync(masterKey, refinedSalt, iterations, 32, 'sha512');
const cipher = _crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
// get 128 bit (16 byte) authentication tag - MAC signature
const tag = cipher.getAuthTag();
// add all together
// decryption only seems to work if encrypted data is at the end
return Buffer.concat([salt, iv, tag, encrypted]).toString('base64');
}
// ------------------------------ DECRYPT FUNCTION ------------------
function decrypt(encryptedData, masterKey, expectedCipherLen) {
if (encryptedData.length !== expectedCipherLen) {
// maybe wait for some random milliseconds before returning
// to avoid the attacker getting to know the inner workings
// also can save the IP and prevent brute force by blocking it on 2nd unsuccessful attempt
// after maybe giving a warning not to sniff on the first unsuccessful attempt
// because all legitemate use cases should only ever send valid data
// and invalid data will only be sent by an attacker or some one curious
// hence the first time warning
return false;
}
try {
const bufferData = Buffer.from(encryptedData, 'base64');
// bufferData.slice() seems to be deprecated
// https://github.com/nodejs/node/issues/28087
const salt = Uint8Array.prototype.slice.call(bufferData, 0, 16);
const iv = Uint8Array.prototype.slice.call(bufferData, 16, 28);
const tag = Uint8Array.prototype.slice.call(bufferData, 28, 44);
const data = Uint8Array.prototype.slice.call(bufferData, 44, expectedCipherLen);
// repeat same logic as encrypt to decrypt
const iv64 = iv.toString('base64');
const ivLen = iv64.length;
const iterations = 10000 + iv64.charAt(ivLen-3).charCodeAt(0) * 10;
const iterArr = String(iterations).split("").map((num)=>{
return Number(num)
})
const iterUint8Arr = Uint8Array.from(iterArr);
let refinedSalt = Buffer.concat([
iv + iterUint8Arr + salt + iv + iterUint8Arr + salt
]);
refinedSalt = Uint8Array.prototype.slice.call(refinedSalt, 0, 64);
const key = _crypto.pbkdf2Sync(masterKey, refinedSalt, iterations, 32, 'sha512');
const decipher = _crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
const decrypted = decipher.update(data, 'binary', 'utf8') + decipher.final('utf8');
return decrypted;
} catch (e) {
console.log(`Error while decrypting: ${e}`);
return false;
}
}
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
const encryptedData = encrypt(data, masterKey);
console.log( "encryptedData is: " + encryptedData );
const decryptedData = decrypt(encryptedData, masterKey, expectedCipherLen);
console.log( "decryptedData is: " + decryptedData );
if ( ! decryptedData ) {
// Save the IP and prevent brute force by blocking it on 2nd unsuccessful attempt
// after maybe giving a warning not to sniff on the first unsuccessful attempt
// because all legitemate use cases should only ever send valid data
// and invalid data will only be sent by an attacker or some one curious
// hence the first time warning
}
// -------------------------------- END ---------------------------------------------------
// ----------------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment