via GIPHY
I love bumping into issue and then find solutions, it's activates kid inside me that want to learn more. It becomes like itch in your head that needs to go away.
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 token and symmetric encryption for a storing tokens in db.
But for our use case, we need both large data andr authenticity to work (only one entity can sign the encryption). It didn't work straight away.
The issue was 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 this. Here's a basic flow of how it would work.

Some common techniques are DES, AES, etc. This type of encryption is used when in high trust scenarios, where keys are not exposed.
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
Code to run Assymetric encryption with various example
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
Oops!! 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".
So 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, licensing system, etc.
Combining Asymmetric with Encryption
I love this approach, it simple and sweet. Quite similiar to Intialization vector approach for making this 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?