blogs
Setup terminal to be fast and charming
💻
2022-10-03
Better GIF quality with ffmpeg
📺
2022-10-02
Assymetric encryption with power of symmetric encryption in NodeJS
🔑
2021-12-12
Creating customized blog using Gatsby + Notion
🧭
2021-07-09
Fixing ambiguous AWS ELB 504 random errors at scale
🤕
2020-08-12

Assymetric encryption with power of symmetric encryption in NodeJS

🔑 | 2021-12-12

I generally bump into weird issues, I get excited and also scratch my head at same time. Few days back, I picked a project to create license key system for crusher.

The goal was simple

  • To create unique license for every user :D.
  • Must work with offline system, as we offer self hosting.
  • Only we should be able to create it. Encryption should be done by private key.

I had previously worked with encryption, most of them were involved tokens and symmetric encryption for a storing tokens in db.

For our use case, we had large data and requirment that only we should be able to encrypt it. I tried small data, it worked. Voila, then I entered large data it threw an exception.

In hindsight it was not so trivial issue, we wanted power of symmetric encryption with capabilties of asymmettics.

Let's get to the basics

Symmetric encryption

In symmetric encryption both parties share the same key, they can encode/decode stuff using that key. Here's a basic flow of it

Some common techniques are DES, AES, etc. This type of encryption is used when in high trust scenarios, where keys are not exposed.

For eg- Your spouse and you can share the same key to your house :D.

Asymmetric encryption

Both parties have different key, the goal is to have exclusivity on either encryption/decryption side. It is to have exclusive decryption or proving authenticity.

Two pair of keys are used which are mathematically related, two large prime modulus. As two are related and one is also public, therefore AES is generally computational heavy and also output due to cipher blocks can be large.

Talk is cheap, show me the code

Let's try to encrypt small data.

Hit run to see the output

const crypto = require("crypto") const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 4096, }) var encryptStringWithRsaPublicKey = function(toEncrypt, relativeOrAbsolutePathToPublicKey) { var buffer = Buffer.from(toEncrypt); var encrypted = crypto.publicEncrypt(publicKey, buffer); return encrypted.toString("base64"); }; var decryptStringWithRsaPrivateKey = function(toDecrypt, relativeOrAbsolutePathtoPrivateKey) { var buffer = Buffer.from(toDecrypt, "base64"); var decrypted = crypto.privateDecrypt(privateKey, buffer); return decrypted.toString("utf8"); }; let encryptedText = encryptStringWithRsaPublicKey("small_string") console.log(encryptedText) let decryptedText = decryptStringWithRsaPrivateKey(encryptedText) console.log(decryptedText)

Now with large data.

const crypto = require("crypto") const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 4096, }) var encryptStringWithRsaPublicKey = function(toEncrypt, relativeOrAbsolutePathToPublicKey) { var buffer = Buffer.from(toEncrypt); var encrypted = crypto.publicEncrypt(publicKey, buffer); return encrypted.toString("base64"); }; var decryptStringWithRsaPrivateKey = function(toDecrypt, relativeOrAbsolutePathtoPrivateKey) { var buffer = Buffer.from(toDecrypt, "base64"); var decrypted = crypto.privateDecrypt(privateKey, buffer); return decrypted.toString("utf8"); }; let encryptedText = encryptStringWithRsaPublicKey("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.") console.log("Encrypted ",encryptedText) let decryptedText = decryptStringWithRsaPrivateKey(encryptedText) console.log("Decrypted ",decryptedText)

Hit run to see the output

This doesn't work. One way to overcome is to increase modulus length, but doing so will take more time and require a larger buffer. Generally, AES will throw exception "The data is larger than the buffer".

How do we make things more secure if we have large data and one key is public. This can be quite common when system are offline like licensing system, etc.

Combining Asymmetric with Encryption

I love this approach, it simple and sweet. Quite similiar to Intialization vector approach for making something secure.

In this

1.) We generate public/private key pair once.

2.) Generate unique symmetric key each time for encryption.

3.) Encode data using symmetric key.

4.) Encode symmetric key.

5.) Append data + encoded symmetric key with combination string.

This technique has advantage of both Symmetric and Assymetric encryption. Code in Nodejs

const crypto = require("crypto") const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 4096, }) /* Constants to be used by both encrypt and decrypt*/ var algorithm = 'aes256'; var inputEncoding = 'utf8'; var outputEncoding = 'hex'; var ivlength = 16 // AES blocksize var key = Buffer.from('ciw7p02f70000ysjon7gztjn7c2x7GfJ', 'latin1'); // key must be 32 bytes for aes256 var iv = crypto.randomBytes(ivlength); const generateKey = () => { const symmetricKey = Buffer.from('ciw7p02f70000ysjon7gztjn7c2x7GfJ', 'latin1').toString(); return symmetricKey; } function encrypt() { const symmetricKey = generateKey(); var data = 'So, for a time this number fluctuated above and below the 1 billion mark. For example, in August 2012 a full 40 million hostnames were removed from 242 IP addresses. This considerably reduced the number of existing websites for a period of time. By March 2016, the web no longer went below a billion websites. It is amazing to consider the sheer growth of the Internet which started with 1 website in 1991 to over a billion today.So, for a time this number fluctuated above and below the 1 billion mark. For example, in August 2012 a full 40 million hostnames were removed from 242 IP addresses. This considerably reduced the number of existing websites for a period of time. By March 2016, the web no longer went below a billion websites. It is amazing to consider the sheer growth of the Internet which started with 1 website in 1991 to over a billion today.So, for a time this number fluctuated above and below the 1 billion mark. For example, in August 2012 a full 40 million hostnames were removed from 242 IP addresses. This considerably reduced the number of existing websites for a period of time. By March 2016, the web no longer went below a billion websites. It is amazing to consider the sheer growth of the Internet which started with 1 website in 1991 to over a billion today.'; var iv = crypto.randomBytes(ivlength); var cipher = crypto.createCipheriv(algorithm, key, iv); var ciphered = cipher.update(data, inputEncoding, outputEncoding); ciphered += cipher.final(outputEncoding); var ciphertext = iv.toString(outputEncoding) + ':' + ciphered; const symmetricEncryptedKey = crypto.publicEncrypt( { key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha1", }, // We convert the data string to a buffer using Buffer.from Buffer.from(symmetricKey) ) return symmetricEncryptedKey.toString("base64") + "::::" + ciphertext.toString(); } function decrypt(data) { const key =data.split("::::")[0]; const cipheredText = data.split("::::")[1] const decryptedPrivateKey = crypto.privateDecrypt( { key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha1", }, Buffer.from(key, "base64") ) var components = cipheredText.split(':'); var iv_from_ciphertext = Buffer.from(components.shift(), outputEncoding); var decipher = crypto.createDecipheriv(algorithm, Buffer.from(decryptedPrivateKey), iv_from_ciphertext); var deciphered = decipher.update(components.join(':'), outputEncoding, inputEncoding); deciphered += decipher.final(inputEncoding); return deciphered; } const encrypted = encrypt(); const decryptData = decrypt(encrypted) console.log(encrypted) console.log("Decrypt", decryptData)

Voila!! We know have power of both symmetric and asymmetric encryption.

Libraries for this

Tink is one of the most popular libraries for this. At this point they don't have NodeJS docs which forced me to implement this. It has tons of features including padding, algorithm, etc.

Food for thought

  • Does SSL use symmetric or asymmetric encryption?

  • Should we encrypt JWT token with asymmetric encryption?

  • If DB gets compromised and key get compromised, how to do you prevent user info?