Take your Encryption to the Next Level - Kevin Uriel Fonseca


nodejs image to use for encryption article

What’s Encryption According to Internet?

Encryption is the method by which information is converted into secret code that hides the information’s true meaning. The science of encrypting and decrypting information is called cryptography. In computing, unencrypted data is also known as plaintext and encrypted data is called ciphertext

Encryption

This idea came to me after using WordPress for about 4 years now. Its method of publishing articles is something that I quite enjoy and like a lot. A feature that I like about this CMS is its functionality to set a password for any post I want. However, there’s something with its encryption, it does not works!. The passwords are visible and its hashing system was the most basic one, MD5!.

This was something that quite dissapointed me and one of the reasons I’ve been working on creating my own CMS.

[HEADS UP]: We are going to be working with NodeJS and MongoDB.

Encryption

First thing first, make sure to install these dependencies before proceeding with the article! Remember, you need to have NodeJS installed in your local machine in order for the command to work!:

npm i bcryptjs mongoose crypto

Once installed, let’s work in our model. You can create one from scratch or simply implement this feature to one already pre-done by you.

const bcrypt = require('bcryptjs')const mongoose = require('mongoose')const slugify = require('slugify')BlogSchema = new mongoose.Schema(  {    title: {      type: String,      required: [true, 'Please add a title'],      trim: true,      maxlength: [50, 'Title can not be more than 50 characters'],    },    password: {      type: String,      trim: true,      required: [true, 'Please type a password'],      minLength: 8,      select: false,    },    text: {      type: Object,      trim: true,      required: [true, 'Please add a text'],    },  },  {    toJSON: { virtuals: true },    toObject: { virtuals: true },    timestamps: true,    id: false,  })module.exports = mongoose.model('Blog', BlogSchema)

Take a very good look in the example above; the example has only three fields for the sake of this article’s lenght, these are title, password and text. We will focus on the last two only. Note that the password field is a string but the text field returns an object, why? well, you will see why!

First let’s work in our logic to create a post. As many of you know, whenever we’re working with ExpressJS, the values sent from a form are usually found within the req.body object that is returned by the request. To access to them, we use – req.body.title req.body.user, req.body.text – and so on.

// @desc    Create new blog// @route   POST /api/v1/blogs// @access  Private// @status  DONEexports.createBlog = asyncHandler(async (req, res, next) => {  if (req.body.text === ``) {    return res.status(500).json({ success: false, data: 'There was an error with the server' });  }  // Encrypt text  req.body.text = encrypText(req.body.text)  // Create document  const blog = await Blog.create(req.body)  blog.save({ validateBeforeSave: true })  // Send response  res.status(201).json({ success: true, data: blog })});

As previously shown in the code above, we’re making sure to throw an error if the text is coming empty since there’s nothing to encrypt, however if there’s a string within said field, then encrypt it with the encryptText() function and return the output. Take a look in what this looks like in the code below:

const crypto = require('crypto')// Set encryption algorithconst algorithm = 'aes-256-ctr'// Private keyconst key = `${process.env.ENCRYPTION_KEY}`// Random 16 digit initialization vectorconst iv = crypto.randomBytes(16)exports.encrypText = (text) => {  const cipher = crypto.createCipheriv(algorithm, key, iv)  const encryptedData = Buffer.concat([cipher.update(text), cipher.final()])  return {    iv: iv.toString('hex'),    encryptedData: encryptedData.toString('hex'),  }}

The function is already making use of the crypto library and we are also setting the type of hashing we want which is aes-256-ctr. Now, take a look in the data returned after running this function:

{  iv: '5a3470c6b8e00a5da27830058d42061c',  encryptedData: '75d5d853d0d2c5509d80e51b34beca6cc1906b0bb30dbe2b03566d72d0fca9bc56'}

This output is the reason why we have made our text field an object or to put it simply an object for a field which accepts objects.

The function is now working as expected and returning the proper hash after passing the parameter of text to it. The next step is to create a new document into our database by running the API HTTP request of api/v1/blogs (this is on my case; you can name your API call as you want). I recommend you to use POSTMAN, however feel free to use any software or website that you desire. The data should look similar to this:

"success": true,"data": [  {    "_id": "633ee5661ca197e7d3fa63b3",    "title": "Hola de nuevo con el testing de password",    "text": {      "iv": "0dfbf440728dd1502d7a395fcf3a3876",      "encryptedData": "45ec32fca0905289f1daa9ce89beccd0e14cfd2ef19dff2d669d157559f5563987a21fc420e6775c5c9a7e7395b9423dcb39bdc849899d9c50"    },    "password": "IF YOU CAN READ THIS, IT MEANS WE HAVE NOT BEEN ABLE TO IMPLEMENT A FUNCTION TO PASSWORD THE ARTICLES YET",    "createdAt": "2022-10-06T14:25:42.006Z",    "updatedAt": "2022-10-06T14:25:42.006Z"  }]

This is more than enough to encrypt your data. Additionally, we are missing a way to decrypt our text to the public view who needs to able to read it. The next function will also make use of the crypto library.

Decryption

exports.decrypText = (text) => {  // Convert initialize vector from base64 to hex  const originalData = Buffer.from(text.iv, 'hex');  // Decrypt the string using encryption algorith and private key  const decipher = crypto.createDecipheriv(algorithm, key, originalData);  const decryptedData = Buffer.concat([    decipher.update(Buffer.from(text.encryptedData, 'hex')),    decipher.final()  ]);  return decryptedData.toString();};

The function takes an argument which in our case is text again. Check the code below to see it in action:

// @desc    Get all single blogs// @route   GET /api/v1/blogs/:id// @access  Private// @status  DONEexports.getBlog = asyncHandler(async (req, res, next) => {  const blog = await Blog.findById({ _id: req.params.id })  if (!req.params.id.match(/^[0-9a-fA-F]{24}$/) || !blog) {    return res.status(404).json({ success: false, data: 'Document not found' })  }  // Decrypt text  blog.text = decrypText(blog.text)  res.status(200).json({ success: true, data: blog })})

Once running the api/v1/blogs/:id API HTTP request, the data will be displayed in a human readable format and will be fetched to anyone calling it.

2nd Layer of Encryption, Passwords

Everything is coming to an end and we’re about to make it better by implementing passwords just as WordPress does. This will require us to modify our Blog model by implenting a static method called matchPassword() and to an extent, our crateBlog() and getBlog() functions will receive some modifications also. Try to understand the code below, this function should be added before the module.exports = mongoose.model('Blog', BlogSchema) line.

// Verify password from user against password from postBlogSchema.methods.matchPassword = async function (enteredPassword) {  return await bcrypt.compare(enteredPassword, this.password)}

As briefly mentioned, the matchPassword() method will allow us to verify the password sent by the user againts the password stored in the article coming from the DB. I say this again, this works by using the method of compare() which takes the password given and the password from X post.

The code above is to retrieve our data after a password has been sent by X user. Moreover, we do need to implement the function to set passwords to the articles themselves. Take a look into the next code:

exports.encryptPassword = async (text) => {  const salt = await bcrypt.genSalt(10)  const password = await bcrypt.hash(text, salt)  return password}

The code above will generate a salt of 10 digits and then will concatenate it with the string(field password). This in fact should transform a hard-coded string into something impossible to read for the human mind. Take a look into how to implement it in our createBlog() function:

// @desc    Create new blog// @route   POST /api/v1/blogs// @access  Private// @status  DONEexports.createBlog = asyncHandler(async (req, res, next) => {  if (req.body.text === ``) {    return next(new ErrorResponse(`Please add some text`, 500))  }  // Encrypt text  req.body.text = encrypText(req.body.text)  // Generate password  if (req.body.password !== `` && req.body.password !== undefined && req.body.password !== null) {    req.body.password = await encryptPassword(req.body.password)  }  // Create document  const blog = await Blog.create(req.body)  blog.save({ validateBeforeSave: true })  // Send response  res.status(201).json({ success: true, data: blog })});

Quite simply, is not it? We are also making sure to run the function only (by checking it against not being empty, undefined or null) when there’s a password coming from out front-end; remember setting passwords is not a requirement but an option that will be up to the writer to decide.

{  "success": true,  "data": {      "_id": "633ee5661ca197e7d3fa63b3",      "title": "Hola de nuevo con el testing de password",      "text": {        "iv": "0dfbf440728dd1502d7a395fcf3a3876",        "encryptedData": "45ec32fca0905289f1daa9ce89beccd0e14cfd2ef19dff2d669d157559f5563987a21fc420e6775c5c9a7e7395b9423dcb39bdc849899d9c50"      },      "password": "$2a$10$0kc9NA7v4Ocempr9nE35huG9wvZzqmymJZRlhusFoNSSiDQbGCv2a",      "createdAt": "2022-10-06T14:25:42.006Z",      "updatedAt": "2022-10-06T14:25:42.006Z"  }}

Are you able to see the difference from the JSON example prior to the one above?. If you see it, then great for you, otherwise let me tell you that it is the password value!. Now, that’s what I call a good password. Here it is the update method as well; this function will work great with the HTTP PUT method.

// @desc    Update blog// @route   PUT /api/v1/blogs/:id// @access  Private// @status  DONEexports.updateBlog = asyncHandler(async (req, res, next) => {  let blog = await Blog.findById(req.params.id);  // Check post  if (!req.params.id.match(/^[0-9a-fA-F]{24}$/) || !blog) {    return res.status(500).json({ success: false, data: 'There was an error with the server' });  }  blog = await Blog.findByIdAndUpdate(    { _id: req.params.id },    {      $set: {        ...req.body,        text: encrypText(req.body.text),        password: req.body.password ? await encryptPassword(req.body.password) : undefined      }    },    {      new: true,      runValidators: true,      setDefaultsOnInsert: false    }  );  res.status(200).json({ success: true, data: blog });});

You should have learned how to encrypt, decrypt strings and how to set passwords. The following section will solely focus on the fetching of any given article with or without password.

Fetch the Articles to the proper Users

What I’m talking about is the function of getBlog() which name might differ with yours.This function has only one purpose and that is making a call to the DB and fetch the article according to the ID given from X url. Nevertheless, the function needs to hide the passwords and the user needs to send them for posts that have them. Check this line below as it will allow to hide the password:

// Hide password againblog.password = undefined;

That by itself is enough to make your data at least hard to read for everyone. Despite that, there are people who need to understand the hidden data, an example is, you.

  //  Check if post has password  if (blog.password !== '' && blog.password !== undefined && blog.password !== null) {    // Verify user has sent password from front-end    if (!confirmblogpassword) {      return res.status(400).json({ success: false, data: 'Invalid token' });    }    // Match user password with post password    const isMatch = await blog.matchPassword(confirmblogpassword);    // Throw error if password do not match    if (!isMatch) {      return res.status(401).json({ success: false, data: 'Passwords do not match!' });    }  }

For the sake’s of this post’s lenght. I will simply put the final getBlog() function and will leave it like that.

/ @desc    Get all single blogs// @route   GET /api/v1/blogs/:id// @access  Private// @status  DONEexports.getBlog = asyncHandler(async (req, res, next) => {  // Retrive password from user  const { confirmblogpassword } = req.body;    const blog = await Blog.findById({ _id: req.params.id });  if (!req.params.id.match(/^[0-9a-fA-F]{24}$/) || !blog) {    return res.status(404).json({ success: false, data: 'Document not found' });  }    //  Check if post has password  if (blog.password !== '' && blog.password !== undefined && blog.password !== null) {    // Verify user has sent password from front-end    if (!confirmblogpassword) {      return res.status(400).json({ success: false, data: 'Invalid token' });    }    // Match user password with post password    const isMatch = await blog.matchPassword(confirmblogpassword);    // Throw error if password do not match    if (!isMatch) {      return res.status(401).json({ success: false, data: 'Passwords do not match!' });    }  }    // Hide password again  blog.password = undefined;  // Decrypt text  blog.text = decrypText(blog.text);  res.status(200).json({ success: true, data: blog });});

[NOTE]: In my example, I have used confirmblogpassword which should tell you the input should look like this <input type="password" name="confirmblogpassword" />.

Make sure to really pay attention to the getBlog() function since this is not the best approach. I made it this way just for the simplicity of this post. As it is right now, you will have to send an email (within the createBlog() function) to your users to let them know what posts have passwords and hopefully include it in said email. Obviously posts with no password will still work.

Please remember to check my previous articles. Thanks for reading!.

Bye bye 🙂

SIDEBAR
FOOTER