A Practical Guide To Asymmetric Encryption - Part 2

A Practical Guide To Asymmetric Encryption - Part 2

Content

Background

We want to encrypt & decrypt information using Asymmetric Encryption that was explained in the previous article. We also want to know, how this can be added to real-world applications. It is going to be a hands-on article so it is expected that you follow along.

Prerequisites

Unlike the previous article, this one has some crucial prerequisites that you must know:

  1. You should have beginner's knowledge of Javascript & Node Js module ecosystem. I have Node Js  v14.15.1 installed. Any version from v10 to the latest would do.
  2. All the theory related to asymmetric encryption is covered in detail in my last article, definitely give it a read before proceeding with this one.
  3. I am using free editor Microsoft Vscode however you can continue with any other editor too.
Richard Attenborough as John Hammond in Jurassic Park, Image Credits Universal Pictures
Richard Attenborough as John Hammond in Jurassic Park, Image Credits Universal Pictures

Getting Started

I have prepared a mini-project starter to follow along. You can download it from the Github repository's Starter Code commit. Below is the files & folders structure:

├── data
│   ├── encryptedData.json
│   ├── keys.json
│   └── rawData.json
├── generateKey.js
├── encryptData.js
├── decryptData.js
├── main.js
└── LICENSE

We have a data directory where we would store private & public cryptographic keys. We have other two files to store raw data & encrypted data i.e. an important message. Json structure of raw message looks like below:

{
  "senderId": "abcd-1234",
  "receiverId": "pqrs-6789",
  "createdAt": "2021-06-12T07:05:53.769Z",
  "message": "Hey Robin, I am having meeting at 6pm with Richard on 4th Evenue, Backer's Vistro. Please bring documents with you, see ya!",
  "isDelivered": "true",
  "isRead": "false"
}

The structure is a real-world message entity example, you might want to save a message like this in a database table or collection. All the properties are self-explanatory. The message seems to be critical here: "Hey Robin, I am having meeting at 6pm with Richard on 4th Evenue, Backer's Vistro. Please bring the documents with you, see ya!"

Next, we have are 4 files generateKey.js, encryptData.js, decryptData.js & main.js. The plan is simple: 

  1. We want to first generate public & private keys, & store them in the keys.json file.
  2. Fetch the raw message from rawData.json, encrypt it with the public key from keys.json & save the encrypted message in encryptedData.json
  3. Finally, we could bring encrypted data from encryptedData.json and decrypt it with the private key from keys.json
Quick Revision of Asymmetric Encryption
Quick Revision of Asymmetric Encryption

The above 3 steps & the diagram are sufficient to get a complete idea of the encryption & decryption process.

1. Asymmetric Keys

We are going to use Node's inbuilt modules throughout. Open generateKey.js file in the editor.

const fs = require('fs')
const path = require('path')
const crypto = require('crypto')

We want to read and write json files and that can be done with the fs module. The path module is for having json file paths. The crypto module has several methods and we would cover just a few. Later, I would suggest you look up official docs and get an idea of other methods in the crypto module.

// ...

const generateAndSaveKeys = () => {
  // Generate Keys -> Key Objects
  const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048 * 3,
  })

  // Convert KeyObjects to Strings -> To save easily
  const publicKeyStr = publicKey.export({ type: 'pkcs1', format: 'pem' }).toString('hex')
  const privateKeyStr = privateKey.export({ type: 'pkcs1', format: 'pem' }).toString('hex')
  
  // ...
}

Above we added a function in the file to generate and save the public & private keys.  generateKeyPairSync is an inbuilt function from the crypto module, that takes in algorithm and config object. We have chosen the rsa algorithm & modulusLength in config which is roughly equal to the length of public key + private key (combined). Larger the key size, larger string you can encrypt & more secure will be the encryption. 

As per NSA's Commercial National Security Algorithm Suite, we should have at least 3072 bits or larger rsa keys.

We get Key Objects as the output of generateKeyPairSync function. To save these keys for later use, we need to convert them into a string.

Key Object has a method called export which takes encoding options type & format. PKCS1 is an acronym for Public-Key Cryptography Standards #1. You can read about it here. It's fine even if you do not know that 🙂. PEM is Privacy Enhanced Mail, it tells how we want to have file content and it will begin with headers & footers like below:

-----BEGIN ABC DEFGH KEY-----
...
...
-----END ABC DEFGH KEY-----

Then we convert output of the export method to a hexadecimal string. Adding below to function will help us to save these keys in the keys.json file & completes the code we want to have in this file.

//...
  
const generateAndSaveKeys = () => {
  //...
  
  // Convert KeyObjects to Strings -> To save easily
  const publicKeyStr = publicKey.export({ type: 'pkcs1', format: 'pem' }).toString('hex')
  const privateKeyStr = privateKey.export({ type: 'pkcs1', format: 'pem' }).toString('hex')

  // Form object to save
  const keys = {
    publicKey: publicKeyStr,
    privateKey: privateKeyStr,
  }

  // Save Key to data/keys.json
  const filePath = path.join(__dirname, '/data/keys.json')
  fs.writeFileSync(filePath, JSON.stringify(keys))
}

Finally, we export the function with module.exports = generateAndSaveKeys at the bottom of the file. Find here the complete code of this file.

2. Encrypt Data With Public Key

The moment we have been long waiting for. If you follow this well, I bet you could do decrypting part on your own. Open encryptData.js file & bring the same 3 modules we used in the above section. Then let's define a function encryptAndSave in it.

const fs = require('fs')
const path = require('path')
const crypto = require('crypto')

const encryptAndSave = () => {
  // 1. Bring Public Key From data/keys.json

  // 2. Bring Raw Data From data/rawData.json

  // 3. Encrypt Raw Data

  // 4. Form Data To Save

  // 5. Save Data to data/encryptedData.json
}

module.exports = encryptAndSave

Above is the skeleton of our function. I have written down in comments what are the steps we would follow. First, let's bring the public key string and convert it to a key object so that we can later use it to encrypt data.

const encryptAndSave = () => {
  // 1. Bring Public Key From data/keys.json
  const keysJsonStr = fs.readFileSync(path.join(__dirname, './data/keys.json')).toString()
  const { publicKey } = JSON.parse(keysJsonStr)
  const publicKeyObj = crypto.createPublicKey({ key: publicKey, type: 'pkcs1', format: 'pem' })

  // ...
}

We brought the entire file content as a string then converted it to Json with the parse method which helped us to extract publicKey (object de-structuring). The crypto module has a method called createPublicKey which helps us to form Key Object from Key string. We have specified the same encoding parameters that we used while creating Key string.

We would now bring raw data and encrypt it like below:

const encryptAndSave = () => {
  // ...
  
  // 2. Bring Raw Data From data/rawData.json
  const rawDataStr = fs.readFileSync(path.join(__dirname, './data/rawData.json')).toString()
  const rawDataObj = JSON.parse(rawDataStr)[0]

  // 3. Encrypt Raw Data
  const encryptedData = crypto.publicEncrypt(
    {
      key: publicKeyObj,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256',
    },
    Buffer.from(JSON.stringify(rawDataObj))
  )
  const encryptedDataStr = encryptedData.toString('base64')

  //...
}

It looks big addition but it's fine. Just like before, we read the file rawData.json as a string and converted it to Json with parse method, you see [0] at the end because parsed string becomes an array of objects (rawData.json has an array of messages - have a look at file) and we want first message (index = 0) from the array.

We used the crypto module's publicEncrypt method which takes in a public key object, padding type & hashing format of padding. Wait! what is padding? It is like a wrapper of dummy data around original data, something like below:

9bf7787dcThis is an important message & should be encrypted.5ca846e45fb3

Notice the random characters at the start and end of the message above. That's the padding. This will enhance the security of encryption. You can read about OAEP here. The publicEncrypt method further takes a Buffer which will be encrypted using the public key. We converted rawDataObj to string with Json stringify method and converted it to Buffer using from method.

The publicEncrypt method returns a Buffer containing encrypted data. We converted that to a base64 string for saving it in json file.

If you have followed till here then you did it 🤝, there isn't much left now in this article! We will save encrypted data, bring it back & decrypt it. The decryption code is very similar to the encryption code above.

Below is the format of json object which we want to save in the encryptedData.json file:

{
  "id": "...",
  "data": "..."
}

As per our file structure, this object is inside an array. Since we have only one message here, we will keep it simple & directly save stringified array having a single object into the file.

const encryptAndSave = () => {
  //...

  // 4. Form Data To Save
  const encryptedDataArr = [
    {
      id: new Date().getTime(),
      data: encryptedDataStr,
    },
  ]

  // 5. Save Data to data/encryptedData.json
  const encryptedDataArrStr = JSON.stringify(encryptedDataArr)
  fs.writeFileSync(path.join(__dirname, './data/encryptedData.json'), encryptedDataArrStr)
}

module.exports = encryptAndSave

The above code is self-explanatory. We form an array with an object inside it. The id property has time value in epochs for example 1623524380978 which is 13th June Sunday, 12:30 am. It has precision up to milliseconds. Ideally, we would use something like uuid to create unique ids. The purpose of id here is to have a unique reference corresponding to every encrypted data string.

This completes encryptAndSave function. The complete code for this file is available here.

3. Decrypt Data With Private key

Last step of the process. I would say you can try this one on your own. All the code you need is already discussed and few new things can be referred to in the crypto module docs.

Open the decryptData.js file and bring the 3 modules that we have used throughout. Add a function decryptAndPrintData which will have similar steps to encryptData.js. For now, we will print decrypted data to the console (perhaps you would want to send this data as an API response to the authorised request).

const fs = require('fs')
const path = require('path')
const crypto = require('crypto')

const decryptAndPrintData = () => {
  // 1. Bring Private Key From data/keys.json

  // 2. Bring Encrypted Data From data/encryptedData.json

  // 3. Decrypt Data

  // 4. Print Decrypted Data
}

module.exports = decryptAndPrintData

The below code brings the private key and forms a Key Object. For creating a public key object, we used crypto's createPublicKey method. Here instead, we have used the createPrivateKey method, encoding parameters remaining the same.

//...

const decryptAndPrintData = () => {
  // 1. Bring Private Key From data/keys.json
  const keysJsonStr = fs.readFileSync(path.join(__dirname, './data/keys.json')).toString()
  const { privateKey } = JSON.parse(keysJsonStr)
  const privateKeyObj = crypto.createPrivateKey({ key: privateKey, type: 'pkcs1', format: 'pem' })
  
  //... 
}

Bringing encrypted data from json file should be straightforward now.

//...

const decryptAndPrintData = () => {
  //...
  
  // 2. Bring Encrypted Data From data/encryptedData.json
  const encryptedDataStr = fs.readFileSync(path.join(__dirname, './data/encryptedData.json')).toString()
  const encryptedDataObj = JSON.parse(encryptedDataStr)[0]

  //...
}

While encrypting data we used publicEncrypt, here we are using the privateDecrypt method to decrypt data. The parameters are same except for two things. Firstly, we have provided the private key object instead of the public key object. Second, earlier we passed Buffer created from a Json string, this time it is created from a base64 string (which we got as the output while encrypting).

//...

const decryptAndPrintData = () => {
  //...

  // 3. Decrypt Data
  const decryptedData = crypto.privateDecrypt(
    {
      key: privateKeyObj,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256',
    },
    Buffer.from(encryptedDataObj.data, 'base64')
  )

  // 4. Print Decrypted Data
  const decryptedDataObj = JSON.parse(decryptedData.toString())
  console.log("Decrypted Data : ", decryptedDataObj)
}

module.exports = decryptAndPrintData

We get a Buffer of decrypted data from the privateDecrypt method, we convert it to string first and then to Json object with the parse method.

Finally, we print decrypted data with console.log 🙌 This completes our last critical function, code for this file can be found in this commit.

Driver Function

We have one more file left, main.js. We want to write driver code in this, such that all steps function gets called inside one main function. It is very simple indeed. 

const generateAndSaveKeys = require('./generateKey')
const encryptAndSave = require('./encryptData')
const decryptAndPrintData = require('./decryptData')

const mainRunner = () => {
  // 1. Generate & Save Asymmetric Keys
  generateAndSaveKeys()

  // 2. Print Raw Data, Encrypt Data, Print It & Save It
  encryptAndSave()

  // 3. Decrypt Data & Print It
  decryptAndPrintData()
}

mainRunner()

As you can see, I am calling every function we created so far. I went ahead and added two console log statements in the encryptAndSave function to print raw data & encrypted data. The complete code for this file and additions of console logs can be found in this commit.

You can now run the main.js file and see everything in action! Execute the below command in root directory of project.

node main.js
Terminal Preview Showing (a) Original Raw Data, (b) Encrypted Data & (c) Decrypted Data, From Top To Bottom Respectively.
Terminal Preview Showing (a) Original Raw Data, (b) Encrypted Data & (c) Decrypted Data, From Top To Bottom Respectively.

Conclusion

This article aimed to give you a hands-on foundational knowledge of asymmetric encryption. This can be useful as the starting point for building an application with highly secure data. The entire project is added to Github public repository.

If you ever want to send me an encrypted message, below is my public key 😉

-----BEGIN RSA PUBLIC KEY-----\nMIIDCgKCAwEAl/ZZtwifIGkOdI9KXVkE3mdAMfkuRPti9hN3HAcltVGeHyf1QUil\nkNuTLnX/iSn2rG5vgIhEQN2U/Dfyi6V/MHweHiwPfUjBTQs6IQdx+9Dh7y5JxMuG\nXTfy0d4tL/VwS44W1BoXkkBcsN7CLHHLeztHoHL39tCKVlwWC4yqLLGH5t1pw8rH\nN2wC7m3/M3kKScSKVJXI5b0dI8oa9okqGKcj6ycQL/SiQf3Hn4yPoagbqRa63r6W\ntrMh8SqMt35yS7/ck9ztXuouWoNBSEsBaFrJFd5yXTdTSOWwDiKmgFXVDzT3SfUg\nrnieOPX3SbVG/vI2bX2AGWrW1fxThh7jqZcjo41/mXmWnUeKj8cTT4Ljrrw2XWTC\nwPqJJzIM3olYxj8eW3aC5N0WUtU/Rlm/ISCFf3yc928QMbf3Q4pL4oYZl/bnppXB\nUumm+JSyNBly2c53eUYQEFFHxjSbMORzzKv3e4A+YYt058UM7/PCPyqwVAEtWKvL\nAl26XvQPpgPzN722oiAZuotWu1kd83oLqHkRCE1MLRKb6LEAVGGlYsSbvvlEc60I\nJg1GjrxofsBkGMXVPmlOlslksboucFJIHOlHcUSUgqCW/quUV3NCCWGyJWlAj5N6\njoTHz/WbHYg0xwOwKGS1wWTxC2VeOywuE/I8LdKXd8XYHtuaxEQIOBewMEOcKCdM\naGbh6K/XyqRSwJeiAHcAsq0WEIW1eLiS1sDdOoziepzO/SozXyc4CspyotOB2SPu\n+blPsWNzXG82tA21PFrCeakDuG9mT8L7lMEceK4yKrR9tMqmHyUyKb8hkd83NlzN\nqeHvGM5MG5wlBqV6KlUNi0fzc7ajoua0CAMES16APnqwYoJtuzTp/GKMBrGdR8CS\nYsst1loirNe3Js1uxRo2Cy/Pi2lY8+JWnLpqVRWF3ZtTEMUTuwVrBKPGy8ZKfMgq\n4aQyRmDUwbJJk0OpIRFEkFbX2p/8acazYgxqxvTfu4DTV/9SNaYArYOgMVUOQwQl\n92oGzIkspGshAgMBAAE=\n-----END RSA PUBLIC KEY-----\n
If you found this article useful and interested in sponsoring my next article, consider buying me a cup of coffee securely using the link below :) See you next time!