-
-
Save randomAn0nym0us/ab0f152668bf9c9b8e1e8aebadd0d8f2 to your computer and use it in GitHub Desktop.
Node.js - AES-256-GCM - random Initialization Vector + Salt - smaller size
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ---------------------------------------------------------------------------------------- | |
// ---------------------------------------------------------------------------------------- | |
// 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