This post will explain the RSA algorithm, and how we can implement RSA Encryption, Decryption and Signing in Node.js using its standard library.

banner

RSA (Rivest–Shamir–Adleman) encryption is one of the most widely used algorithms for secure data encryption.

It is an asymmetric encryption algorithm, which is just another way to say “one-way”. In this case, it’s easy for anyone to encrypt a piece of data, but only possible for someone with the correct “key” to decrypt it.

If you want to skip the explanation and just see the working source code, you can view it here

RSA Encryption In A Nutshell

RSA works by generating a public and a private key. The public and private keys are generated together and form a key pair.

key pair

The public key can be used to encrypt any arbitrary piece of data, but cannot decrypt it.

encryption

The private key can be used to decrypt any piece of data that was encrypted by it’s corresponding public key.

decryption

This means we can give our public key to whoever we want. They can then encrypt any information they want to send us, and the only way to access this information is by using our private key to decrypt it.

key distribution

The details of how the keys are generated, and how information is encrypted and decrypted is beyond the scope of this post, but if you want to delve into the details, there is a great video on the topic

Key Generation

The first thing we want to do is generate the public and private key pairs. These keys are randomly generated, and will be used for all following operations.

We use the crypto standard library for generating the keys:

const crypto = require("crypto");

// The `generateKeyPairSync` method accepts two arguments:
// 1. The type ok keys we want, which in this case is "rsa"
// 2. An object with the properties of the key
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
  // The standard secure default length for RSA keys is 2048 bits
  modulusLength: 2048,
});

// use the public and private keys
// ...

The publicKey and privateKey variables will be used for encryption and decryption respectively.

Encryption

We will use the publicEncrypt method for encrypting an arbitrary message. We must provide a few inputs to this method:

  1. The public key that we generated in the previous step
  2. The padding scheme (we will use OAEP padding for this)
  3. The hashing algorithm (we will be using SHA256, which is a recommended secure hashing function as of this date)
  4. The data we want to encrypt. This is in the from of a buffer since the encrypt method accepts encrypt raw bytes.
// This is the data we want to encrypt
const data = "my secret data";

const encryptedData = crypto.publicEncrypt(
  {
    key: publicKey,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    oaepHash: "sha256",
  },
  // We convert the data string to a buffer using `Buffer.from`
  Buffer.from(data)
);

// The encrypted data is in the form of bytes, so we print it in base64 format
// so that it's displayed in a more readable form
console.log("encypted data: ", encryptedData.toString("base64"));

This will print out the encrypted bytes as a base64 string, which look more or less like garbage.

Decryption

To access the information contained in the encrypted bytes, they need to be decrypted.

The only way we can decrypt them is by using the private key corresponding to the public key we encrypted them with.

The crypto library contains the privateDecrypt method which we will use to get the original information back from the encrypted data.

The data we have to provide for decryption is:

  1. The encrypted data (called the cipher text)
  2. The hash that we used to encrypt the data
  3. The padding scheme that we used to encrypt the data
  4. The private key, which we generated previously
const decryptedData = crypto.privateDecrypt(
  {
    key: privateKey,
    // In order to decrypt the data, we need to specify the
    // same hashing function and padding scheme that we used to
    // encrypt the data in the previous step
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    oaepHash: "sha256",
  },
  encryptedData
);

// The decrypted data is of the Buffer type, which we can convert to a
// string to reveal the original data
console.log("decrypted data: ", decryptedData.toString());

Signing And Verification

RSA keys are also used for signing and verification. Signing is different from encryption, in that it enables you to assert authenticity, rather than confidentiality.

What this means is that instead of masking the contents of the original message (like what was done in encryption), a piece of data is generated from the message, called the “signature”.

signing

Anyone who has the signature, the message, and the public key, can use RSA verification to make sure that the message actually came from the party by whom the public key is issued. If the data or signature don’t match, the verification process fails.

verification

Note that only the party with the private key can sign a message, but anyone with the public key can verify it.

// Create some sample data that we want to sign
const verifiableData = "this need to be verified";

// The signature method takes the data we want to sign, the
// hashing algorithm, and the padding scheme, and generates
// a signature in the form of bytes
const signature = crypto.sign("sha256", Buffer.from(verifiableData), {
  key: privateKey,
  padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
});

console.log(signature.toString("base64"));

// To verify the data, we provide the same hashing algorithm and
// padding scheme we provided to generate the signature, along
// with the signature itself, the data that we want to
// verify against the signature, and the public key
const isVerified = crypto.verify(
  "sha256",
  Buffer.from(verifiableData),
  {
    key: publicKey,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
  },
  signature
);

// isVerified should be `true` if the signature is valid
console.log("signature verified: ", isVerified);

Conclusion

In this post we have seen how to generate RSA public and private keys and how to use them to encrypt, decrypt, sign and verify arbitrary data.

There are some limitations that you should take note of:

  1. The data you are trying to encrypt should be much shorter than the bit strength of your keys. For example, the EncryptOEAP documentation says “The message must be no longer than the length of the public modulus minus twice the hash length, minus a further 2.”
  2. The hashing algorithm used should also be appropriate for your use case. SHA256 (which is used in the examples here) is considered sufficient for most use cases, but you may want to consider something like SHA512 for more data-critical applications.

You can find the complete working source code for all examples here