I generally bump into weird issues; I get excited and also scratch my head at the same time. A few days back, I picked a project to create a license key system for Crusher.
The goal was simple:
- To create a unique license for every user :D
- Must work with offline systems, 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 involved tokens and symmetric encryption for storing tokens in DB.
For our use case, we had large data and a requirement that only we should be able to encrypt it. I tried small data, it worked. Voila! Then I entered large data, and it threw an exception.
In hindsight, it was not such a trivial issue; we wanted the power of symmetric encryption with capabilities of asymmetric encryption.
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 in high-trust scenarios, where keys are not exposed.
For example - You and your spouse can share the same key to your house :D
Asymmetric encryption
Both parties have different keys; the goal is to have exclusivity on either encryption/decryption side. It is to have exclusive decryption or proving authenticity.
Two pairs of keys are used which are mathematically related, using two large prime moduli. As the two are related and one is also public, RSA is generally computationally heavy, and also the 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 this is to increase modulus length, but doing so will take more time and require a larger buffer.
Generally, AES will throw the 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 systems are offline like licensing systems, etc.
Combining Asymmetric with Encryption
I love this approach; it's simple and sweet. Quite similar to the initialization 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 advantages of both Symmetric and Asymmetric encryption.
Code in Node.js:
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 now have the 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 Node.js docs, which forced me to implement this.
It has tons of features including padding, algorithms, etc.
Food for thought
-
Does SSL use symmetric or asymmetric encryption?
-
Should we encrypt JWT tokens with asymmetric encryption?
-
If DB gets compromised and keys get compromised, how do you prevent user info?