AES Cipher Utility
Registered members can download the FREE Get Started Apps. I initially implemented MySQL for this site and the Get Started App. The Get Started App is the project I used to compose articles about setting up VS Code and developing Node with Express and the Embedded JavaScript (EJS) view engine. I decided to migrate this site to PostgreSQL. Rather than replacing the MySQL Get Started App, I decided to add a PostgreSQL Get Started App. Both apps will run without a database but can integrate with either MySQL or PostgreSQL by configuring environment variables in the .env file.
The Advanced Encryption Standard (AES), also known by its original name Rijndael is a specification for the encryption of electronic data established by the U.S. National Institute of Standards and Technology (NIST) in 2001. The Node.js native crypto module is the recommended choice for new development due to better security, maintenance, and performance.
This series will focus on the utilities I use for this site which implement user security and communications crucial to user registration and user password storage.
I migrated the AES Cipher utility from my ASP.NET Core websites. I implement the Node.js native crypto module. All new projects should use the native crypto module (or Web Crypto API) for robust and secure cryptographic operations. I use "aes-256-cbc" encryption with a random initialization vector (iv) for confirm email and forgot password email tokens. These tokens are url safe encoded with the Node.js native "base64url" encoding.
The AES Cipher Utility allows Base64 or Hexadecimal string formats and 128, 192, and 256 key length options. The utility validates inputs and uses JavaScript fetch endpoints which call functions from a utilties-controller.mjs module.
utilties-controller.mjs
UtilitiesController.PostApiGetEncryptedTextJson = (req, res) => {
const { keyString, ivString, plainText, keyByteSize, useHex } = req.body;
let expectedKeyStringLength; //24/32, 32/48, 44/64
if (useHex)
expectedKeyStringLength = keyByteSize * 2;
else
{
switch (keyByteSize) {
case 16:
expectedKeyStringLength = 24;
break;
case 24:
expectedKeyStringLength = 32;
break;
case 32:
expectedKeyStringLength = 44;
break;
default:
expectedKeyStringLength = 24;
}
}
const errors = [];
if (keyString.length == 0)
errors.push("Key is required.");
else
{
if (keyString.length != expectedKeyStringLength)
errors.push("Key must be " + expectedKeyStringLength + " characters long.");
if (useHex) {
if (!AesCipherUtility.isValidHex(keyString)) errors.push("Key must be hex characters.");
} else {
if (!AesCipherUtility.isValidBase64(keyString)) errors.push("Key must be Base64 characters.");
}
}
if (ivString.length == 0)
errors.push("Initialization Vector is required.");
else
{
if (useHex) {
if (ivString.length != 32 || !AesCipherUtility.isValidHex(ivString))
errors.push("Initialization Vector must be 32 hex characters.");
} else {
if (ivString.length != 24 || !AesCipherUtility.isValidBase64(ivString))
errors.push("Initialization Vector must be 24 base64 characters.");
}
}
if (plainText.length == 0) errors.push("Plain Text is required.");
let jsonData;
if (errors.Count > 0) {
jsonData = { Success: false, Errors: errors };
} else {
let encryptedText;
try
{
encryptedText = AesCipherUtility.encryptText(plainText, keyString, ivString, useHex);
jsonData = { Success: true, EncryptedText: encryptedText };
}
catch (err)
{
errors.push("Exception = " + err.message);
jsonData = { Success: false, Errors: errors };
}
}
res.json(jsonData);
}
UtilitiesController.PostApiGetDecryptedTextJson = (req, res) => {
const { keyString, ivString, encryptedText, keyByteSize, useHex } = req.body;
let expectedKeyStringLength; //24/32, 32/48, 44/64
if (useHex)
expectedKeyStringLength = keyByteSize * 2;
else
{
switch (keyByteSize) {
case 16:
expectedKeyStringLength = 24;
break;
case 24:
expectedKeyStringLength = 32;
break;
case 32:
expectedKeyStringLength = 44;
break;
default:
expectedKeyStringLength = 24;
}
}
const errors = [];
if (keyString.length == 0)
errors.push("Key is required.");
else
{
if (keyString.length != expectedKeyStringLength)
errors.push("Key must be " + expectedKeyStringLength + " characters long.");
if (useHex) {
if (!AesCipherUtility.isValidHex(keyString)) errors.push("Key must be hex characters.");
} else {
if (!AesCipherUtility.isValidBase64(keyString)) errors.push("Key must be Base64 characters.");
}
}
if (ivString.length == 0)
errors.push("Initialization Vector is required.");
else
{
if (useHex) {
if (ivString.length != 32 || !AesCipherUtility.isValidHex(ivString))
errors.push("Initialization Vector must be 32 hex characters.");
} else {
if (ivString.length != 24 || !AesCipherUtility.isValidBase64(ivString))
errors.push("Initialization Vector must be 24 base64 characters.");
}
}
if (encryptedText.length == 0) errors.push("Encrypted Text is required.");
let jsonData;
if (errors.Count > 0) {
jsonData = { Success: false, Errors: errors };
} else {
let decryptedText;
try
{
decryptedText = AesCipherUtility.decryptText(encryptedText, keyString, ivString, useHex);
jsonData = { Success: true, DecryptedText: decryptedText };
}
catch (err)
{
errors.push("Exception = " + err.message);
jsonData = { Success: false, Errors: errors };
}
}
res.json(jsonData);
}
The UtilitiesController imports the AesCipherUtility module which handles the encryption and decryption functions.
aes-cipher-utility.mjs
import crypto from'crypto';
const AesCipherUtility = {};
// Nodejs encryption with CTR
const algorithm128 = 'aes-128-cbc';
const algorithm192 = 'aes-192-cbc';
const algorithm256 = 'aes-256-cbc';
AesCipherUtility.getNewRandom = (byteSize = 16, useHex = false) => {
const randomBytes = crypto.randomBytes(byteSize);
return useHex ? randomBytes.toString('hex') : randomBytes.toString('base64');
}
AesCipherUtility.encryptText = (plainText, keyString, ivString, useHex = false) => {
const key = useHex ? Buffer.from(keyString, 'hex') : Buffer.from(keyString, 'base64');
const iv = useHex ? Buffer.from(ivString, 'hex') : Buffer.from(ivString, 'base64');
let algorithm = algorithm128;
switch (key.length) {
case 24:
algorithm = algorithm192;
break;
case 32:
algorithm = algorithm256;
break;
default:
break;
}
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(plainText);
const final = cipher.final();
encrypted = Buffer.concat([encrypted, final], encrypted.length + final.length );
return useHex ? encrypted.toString('hex') : encrypted.toString('base64');
}
AesCipherUtility.decryptText = (encryptedText, keyString, ivString, useHex = false) => {
const key = useHex ? Buffer.from(keyString, 'hex') : Buffer.from(keyString, 'base64');
const iv = useHex ? Buffer.from(ivString, 'hex') : Buffer.from(ivString, 'base64');
const encrypted = useHex ? Buffer.from(encryptedText, 'hex') : Buffer.from(encryptedText, 'base64');
let algorithm = algorithm128;
switch (key.length) {
case 24:
algorithm = algorithm192;
break;
case 32:
algorithm = algorithm256;
break;
default:
break;
}
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted);
const final = decipher.final();
decrypted = Buffer.concat([decrypted, final], decrypted.length + final.length);
return decrypted.toString();
}
AesCipherUtility.isValidBase64 = (str) => {
const base64RegExp = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
return base64RegExp.test(str);
}
AesCipherUtility.isValidHex = (str) => {
// Regular expression to match one or more hexadecimal characters
const hexRegExp = /^[0-9a-fA-F]+$/;
return hexRegExp.test(str);
}
export default AesCipherUtility;