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  DONE
exports.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 algorith
const algorithm = 'aes-256-ctr'

// Private key
const key = `${process.env.ENCRYPTION_KEY}`

// Random 16 digit initialization vector
const 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  DONE
exports.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 post
BlogSchema.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  DONE
exports.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  DONE
exports.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 again
blog.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  DONE
exports.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 🙂

If you want to know more about Web Development with NodeJS, take a look into this book:

Leave a Reply

Back to Top