DEV Community

Cover image for Encrypt and Decrypt Data in Node.js using aes-256-cbc
Ugbem Job
Ugbem Job

Posted on

Encrypt and Decrypt Data in Node.js using aes-256-cbc

This tutorial aims at teaching you how to encrypt and decrypt data in Node.js.
The method provided here is pretty straightforward and easy to understand, as it has been written with the intention of enabling other programmers and developers to learn how to encrypt data in their applications.
This encryption method can be used for files, messages, or any other data that your application needs to encrypt.

In this tutorial, the Encryption and Decryption methods provided are based on the AES-256 algorithm, and why we are using this because it is one of the most popular encryption algorithms in use today, and it’s well known for being secure.

NOTE: The AES-256 algorithm is a symmetric-key algorithm, meaning that the same key is used for encryption and decryption. This is in contrast to asymmetric-key algorithms, which use a public key for encryption and a private key for decryption.

Prerequisites

  • Node.js
  • NPM or Yarn
  • Knowledge of Javascript

Getting Started

To get started, you need to create a new project folder and initialize npm or yarn in it. To do this, run the following commands in your terminal:

I'd be using yarn in this tutorial, but you can use npm if you prefer.

mkdir nodejs-encryption
cd nodejs-encryption
yarn init -y
Enter fullscreen mode Exit fullscreen mode

This will create a new folder called nodejs-encryption and initialize yarn in it. The -y flag is used to skip the interactive mode and use the default values.

Installing Dependencies

To install the dependencies, run the following command in your terminal:

yarn add express dotenv
yarn add -D nodemon
Enter fullscreen mode Exit fullscreen mode

This will install the express and dotenv modules and the nodemon module will be installed in the devDependencies.

Modifying the package.json file

We will be using the module type in the package.json file to enable the use of ES6 modules in our project also we will be adding the start and dev scripts to the scripts object in the package.json file.


"type": "module",
"scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }

Enter fullscreen mode Exit fullscreen mode

Creating the Config File

To create the config file, create a new file called config.js in the root directory of your project. In this file, you will be storing the secret key, secret iv, and encryption method. The secret key and secret iv are used to generate the secret hash, which is used for encryption and decryption. The encryption method is used to specify the encryption algorithm to use. In this tutorial, we will be using the AES-256 algorithm.

// config.js
import dotenv from 'dotenv'

dotenv.config()

const { NODE_ENV, PORT, SECRET_KEY, SECRET_IV, ECNRYPTION_METHOD } = process.env

export default {
  env: NODE_ENV,
  port: PORT,
  secret_key: SECRET_KEY,
  secret_iv: SECRET_IV,
  ecnryption_method: ECNRYPTION_METHOD,
}
Enter fullscreen mode Exit fullscreen mode

NOTE: The config file is imported in the encryption.js file, so you need to ensure that the config file is in the same directory as the encryption.js file.

Creating the Server File

To create the server file, create a new file called server.js in the root directory of your project.

// server.js
import express from 'express'

const app = express()

app.use(express.json())
app.use(express.urlencoded({ extended: false }))

app.listen(3000, () => {
  console.log('Server is running on port 3000')
})
Enter fullscreen mode Exit fullscreen mode

This will create a new express server and listen on port 3000.

Creating the Encryption Module

To create the encryption module, create a new file called encryption.js in the root directory of your project.


In this file, you will be importing the crypto module and the config file. The config file contains secret_key, secret_iv, and ecnryption_method.

These variables are then assigned to their own variables. If any of these variables are missing, an error is thrown.

A secret hash is then generated with crypto to use for encryption. This hash is created using the sha512 method and the secret_key and secret_iv from the config file. The hash is then cut down to 32 characters for the key and 16 characters for the iv.

NOTE: iv stands for initialization vector. The initialization vector is a random value that is used to encrypt the first block of plaintext. The initialization vector is required to decrypt the ciphertext, so it must be transmitted or stored alongside the ciphertext.

Two functions are then exported: encryptData() and decryptData(). The encryptData() function takes in data as an argument and uses the cipheriv method from crypto to encrypt it, converting it to hexadecimal format before converting it to base64 format. The decryptData() function takes in encrypted data as an argument and converts it to utf8 format before using decipheriv from crypto to decrypt it, converting it back to utf8 format.

// encryption.js
import crypto from 'crypto'
import config from './config.js'

const { secret_key, secret_iv, ecnryption_method } = config

if (!secret_key || !secret_iv || !ecnryption_method) {
  throw new Error('secretKey, secretIV, and ecnryptionMethod are required')
}

// Generate secret hash with crypto to use for encryption
const key = crypto
  .createHash('sha512')
  .update(secret_key)
  .digest('hex')
  .substring(0, 32)
const encryptionIV = crypto
  .createHash('sha512')
  .update(secret_iv)
  .digest('hex')
  .substring(0, 16)

// Encrypt data
export function encryptData(data) {
  const cipher = crypto.createCipheriv(ecnryption_method, key, encryptionIV)
  return Buffer.from(
    cipher.update(data, 'utf8', 'hex') + cipher.final('hex')
  ).toString('base64') // Encrypts data and converts to hex and base64
}

// Decrypt data
export function decryptData(encryptedData) {
  const buff = Buffer.from(encryptedData, 'base64')
  const decipher = crypto.createDecipheriv(ecnryption_method, key, encryptionIV)
  return (
    decipher.update(buff.toString('utf8'), 'hex', 'utf8') +
    decipher.final('utf8')
  ) // Decrypts data and converts to utf8
}
Enter fullscreen mode Exit fullscreen mode

Creating the Routes

To create the routes, create a new file called routes.js in the root directory of your project.

In this file, you will be importing the express router and the encryption module. The encryption module contains the encryptData() and decryptData() functions.

The encryptData() function is used to encrypt the data sent in the request body and the decryptData() function is used to decrypt the data sent in the request body.

// routes.js
import express from 'express'
import { encryptData, decryptData } from './encryption.js'

const router = express.Router()

router.post('/encrypt', (req, res) => {
  const { data } = req.body
  const encryptedData = encryptData(data)
  res.json({ encryptedData })
})

router.post('/decrypt', (req, res) => {
  const { encryptedData } = req.body
  const data = decryptData(encryptedData)
  res.json({ data })
})

export default router
Enter fullscreen mode Exit fullscreen mode

Creating the .env File

To create the .env file, create a new file called .env in the root directory of your project.

In this file, you will be storing the secret_key, secret_iv, and encryption_method. The secret_key and secret_iv are used to generate the secret hash, which is used for encryption and decryption. The encryption_method is used to specify the encryption algorithm to use.

In this tutorial, we will be using the AES-256 algorithm.


NODE_ENV=development
PORT=3000
SECRET_KEY=secretKey
SECRET_IV=secretIV
ECNRYPTION_METHOD=aes-256-cbc

Enter fullscreen mode Exit fullscreen mode

NOTE: The .env file is imported in the config.js file, which is used to store the secret_key, secret_iv, and encryption_method.

Creating the .gitignore File

To create the .gitignore file, create a new file called .gitignore in the root directory of your project. In this file, you will be ignoring the node_modules folder and the .env file.

node_modules
.env
Enter fullscreen mode Exit fullscreen mode

Importing the Routes in the Server File

To import the routes in the server file, import the routes file in the server.js file. Our updated server.js file will look like this:

// server.js
import express from 'express'
import routes from './routes.js'

const app = express()

app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(routes)

app.listen(3000, () => {
  console.log('Server is running on port 3000')
})
Enter fullscreen mode Exit fullscreen mode

Testing the API

To test the API, start the server by running the following command in the terminal:

yarn dev
Enter fullscreen mode Exit fullscreen mode

NOTE: The dev script is defined in the package.json file.

To test the encrypt route, send a POST request to http://localhost:3000/encrypt with the following data in the request body:

{
  "data": "Hello World"
}
Enter fullscreen mode Exit fullscreen mode

To test the decrypt route, send a POST request to
http://localhost:3000/decrypt with the following data in the request body:

{
  "data": "encryptedData"
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this tutorial, you learned how to create an API that encrypts and decrypts data using Node.js and Express. You also learned how to use the crypto module to encrypt and decrypt data.

You can find the source code for this tutorial on GitHub repo.

If you have any questions, feel free to ask them in the comments section below.

Happy coding!

References

Top comments (8)

Collapse
 
noz profile image
Rich • Edited

This is a very sensitive topic to address. You have presented the article well but I'm afraid there is a problem with your example that defeats the point of the IV. If you read the definition of IV in your actual post:

NOTE: iv stands for initialization vector. The initialization vector is a random value that is used to encrypt the first block of plaintext. The initialization vector is required to decrypt the ciphertext, so it must be transmitted or stored alongside the ciphertext.

What this description may fail to impress is that an IV should be unique enough never to be used more than once with a single key. Your example essentially uses a static IV each and every time. This is bad practice and I don't recommend advising anyone to do this going forward.

What you should do is ensure the IV is suitably random (crypto-random) for each and every call and then store that IV as part of the generated cipher-text. It is perfectly OK for this to be done as you do not have to protect the IV from the cipher-text. This IV can then be used as part of the decryption process in subsequent calls.

There are more things you can do to harden up use of your key too. I recommend reviewing this very useful Gist as of writing: Node.js - AES Encryption/Decryption with AES-256-GCM using random Initialization Vector + Salt

Thanks for the article, but it would be good to update accordingly, at least around the use of the IV.

Collapse
 
jobizil profile image
Ugbem Job

Thank you so much for pointing this out and shedding more light on IV, I'll make an adjustment and proceed with this practice henceforth.

Collapse
 
hugotox profile image
Hugo Pineda

Thanks for this article!! I have a question about this bit

// Generate secret hash with crypto to use for encryption
const key = crypto
  .createHash('sha512')
  .update(secret_key)
  .digest('hex')
  .substring(0, 32)
const encryptionIV = crypto
  .createHash('sha512')
  .update(secret_iv)
  .digest('hex')
  .substring(0, 16)
Enter fullscreen mode Exit fullscreen mode

If I understood correctly, this will generate the key and encryptionIV values, which will be constants, so why not just saving those already calculated values in the .env file directly?

Collapse
 
jobizil profile image
Ugbem Job

It's not a good practice to save them in the env as every encryption hash is expected to have its own unique encryption key.

Collapse
 
aberba profile image
Lawrence Aberba • Edited

Here is how to do it right:

// NOTE: encryption Key must be 256 bits (32 characters)
// e.g xfn9P8L9rIpKtWKj68IZ3G865WfdYXNY

 function aesEncrypt(text) {
    let iv = crypto.randomBytes(IV_LENGTH);
    let cipher = crypto.createCipheriv(
        "aes-256-cbc",
        Buffer.from(process.env.ENCRYPTION_KEY),
        iv
    );

    let encrypted = cipher.update(text);

    encrypted = Buffer.concat([encrypted, cipher.final()]);

    return iv.toString("hex") + ":" + encrypted.toString("hex");
}

 function aesDecrypt(text) {
    let textParts = text.split(":");
    let iv = Buffer.from(textParts.shift(), "hex");
    let encryptedText = Buffer.from(textParts.join(":"), "hex");
    let decipher = crypto.createDecipheriv(
        "aes-256-cbc",
        Buffer.from(ENCRYPTION_KEY),
        iv
    );

    // By default node uses PKCS padding, but Python uses null-byte
    // padding instead. So calling cipher.setAutoPadding(false); after
    // you create the decipher instance will make it work as expected:
    //decipher.setAutoPadding(false);

    let decrypted = decipher.update(encryptedText);

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

    return decrypted.toString();
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
jobizil profile image
Ugbem Job

Thank you for reading my article. I try as much as possible to write beginner-friendly articles.

Collapse
 
ronald3217 profile image
Ronald3217

Thank you very much, I am learning nodejs and I have tried to implement this feature to my project. And this is the most comprehensive article I've ever read.