Implementing Public Key Cryptography in JavaScript

January 25, 2021

Cryptography refers to the encoding and decoding of messages to maintain confidentiality, integrity, and authentication of information in transit. Public key cryptography is also known as asymmetric cryptography. In this method, there is a public key and a private key. The public key is known widely whereas the private key is the secret to a communicating pair.

When a pair wants to communicate, the sender encrypts the message using the public key of the recipient to come up with the ciphertext. When the recipient receives the message, they decrypt the message using their private key. The private key of the sender and the public key of the receiver is encoded in the ciphertext. This eliminates the key distribution problem.

Prerequisites

Before we begin it would help if you have the following:

  • Node.js installed on your computer.

  • Some basic knowledge of the JavaScript programming language.

  • Some basic knowledge of cryptography.

What we will cover

Algorithms using public-key cryptography

Since its initial release in 1976, different algorithms have applied this mechanism. The following are some of the algorithms using public-key cryptography:

Libraries for public-key cryptography in JavaScript

There are different libraries for implementing public-key cryptography in JavaScript. The following are the most commonly used.

  • NaCL: It’s a high-speed library for carrying out encryption, decryption, and network communication. It uses an elliptic curve cryptography algorithm.

  • TweetNaCL: It was among the first cryptographic libraries to be released and it was originally written in the C programming language. TweeNaCL.js is the JavaScript version of the library. It uses the Diffie Hellman algorithm.

Using TweetNaCL.js to implement public-key cryptography

For this article, we’ll use TweetNaCL.js to implement the concept of public-key cryptography.

First, let’s install the dependencies to use in the application. You can use npm or yarn to install these packages.

npm install tweetnacl tweetnacl-util

or

yarn add tweetnacl tweetnacl-util

Practical scenario

We have two communicating pairs, David and Viktoria. When David is sending a message to Viktoria, he encrypts it using Viktoria’s public key. When Viktoria receives it, she decrypts it using her private key. When she decides to reply to the message, she encrypts her message using David’s public key, and on David receiving the message, he decrypts it using his private key.

Importing libraries

//import the libraries
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');

Generating the keys

//Generate the keys
const david = nacl.box.keyPair();
const viktoria = nacl.box.keyPair();

We have generated the keys for both David and Viktoria. A pair consists of the public key and the private key. The keys are of type Uint8Array(32).

For example, David’s key pair will look like this:

{
  publicKey: Uint8Array(32) [
    159,  32, 160, 185, 143,  29,  55,  23,
    111, 203,  90, 224,  64,  90,  65,  75,
     80, 149,  12, 124,  83, 145,  72, 162,
     96, 163, 121, 157,  62,  78, 203,  52 
  ],
  secretKey: Uint8Array(32) [
     72, 210, 106, 229, 196,  53,   2,  88,
    124,  87, 128, 174, 185,   0, 192,  52,
      5, 162,  11,  39,  23, 183, 103, 165,
     40, 128, 179, 242,  38, 132,  78, 241 
  ]
}

David encrypting the message

function davidEncrypting(){
    const one_time_code = nacl.randomBytes(24);

    //Get the message from david
    const plain_text = "Hello there Viktoria";

    //Get the cipher text
    const cipher_text = nacl.box(
        nacl.util.decodeUTF8(plain_text),
        one_time_code,
        viktoria.publicKey,
        david.secretKey
    );

    //message to be sent to Viktoria
    const message_in_transit = {cipher_text,one_time_code};

    return message_in_transit;
};
  1. We’ll generate a one-time code, a code i.e., only valid for the current process.

  2. We’ll get the plain text from David.

  3. We’ll compose the ciphertext using nacl.box() passing the following parameters:

    • A string of Unicode characters generated by passing plain text as a parameter through decodeUTF8().

    • One-time code.

    • Viktoria’s public key.

    • David’s private key.

The message to be sent to the recipient as per the library is:

  • Ciphertext.

  • One-time code.

Viktoria decrypting the message

function viktoriaDecrypting(message){
    //Get the decoded message
    let decoded_message = nacl.box.open(message.cipher_text, message.one_time_code, david.publicKey, viktoria.secretKey);

    //Get the human readable message
    let plain_text = nacl.util.encodeUTF8(decoded_message)

    //return the plaintext
    return plain_text;
};

Output:

Hello there Viktoria

We are decoding the message using the ciphertext, one-time code, David’s public key, and Viktoria’s secret key. The message is then encoded to UTF8 using encodeUTF8() so that its human-readable.

Man-in-the-middle attack

In public-key cryptography, the public key is disseminated widely. This means that an attacker may get the public key of another party. If party A is communicating with party B, an attacker may impersonate himself or herself such that when party A is sending a message to party B, the message reaches the attacker before reaching party B.

The attacker then modifies the message and sends the modified message to party B. When party B decides to reply, the message is again sent to the attacker who modifies the message and sends the modified message to party A. In such a situation, the integrity of the message is not preserved. This is a major threat to public-key cryptography.

Using pre-computed keys

To curb the above threat, we use pre-computed keys. Here, while encrypting and decrypting, instead of using the public key of the other party which could be impersonated, we use a shared key. A shared key is a special combination key of the recipient’s public key and the sender’s secret key.

David encrypting the message

function davidEncrypting(){
    //David computes a one time shared key
    const david_shared_key = nacl.box.before(viktoria.publicKey,david.secretKey);

    //David also computes a one time code.
    const one_time_code = nacl.randomBytes(24);

    //Davids message
    const plain_text = "Hey!!, our communication is now more secure";

    //Getting the cipher text
    const cipher_text = nacl.box.after(
        nacl.util.decodeUTF8(plain_text),
        one_time_code,
        david_shared_key 
    );

    //message to be transited.
    const message_in_transit = {cipher_text,one_time_code};

    return message_in_transit;
};
  1. We’ll compute a shared key based on Viktoria’s public key and David’s secret key.

  2. We’ll generate a one-time code.

  3. We’ll get the plain text from David.

  4. We’ll compute the ciphertext using nacl.box.after() passing the following parameters:

    • A string of Unicode characters generated by passing the plain text through decodeUTF8().

    • One-time code.

    • David’s shared key.

The message to be sent to the recipient is comprised of:

  • Ciphertext.

  • One-time code.

Viktoria decrypting the message

function viktoriaDecrypting(message){
    //Getting Viktoria's shared key
    const viktoria_shared_key = nacl.box.before(david.publicKey,viktoria.secretKey);

    //Get the decoded message
    let decoded_message = nacl.box.open.after(message.cipher_text,message.one_time_code,viktoria_shared_key);

    //Get the human readable message
    let plain_text = nacl.util.encodeUTF8(decoded_message)

    //return the message
    return plain_text;
};

Output:

Hey!!, our communication is now more secure
  1. We’ll get Viktoria’s shared key.

  2. We’ll decode Viktoria’s message using the ciphertext, one-time code, and her shared key.

  3. We’ll encode the message to UTF8 using encodeUTF8() so that its human-readable.

Maintaining public keys

Public key infrastructure is a body responsible for maintaining and registering public keys. Practical areas that use public key infrastructure are banks. All banks have their keys stored and maintained by one body. So when one bank wants to transfer funds to another they get the keys from the common body. Public key infrastructure ensures the credibility of public keys thereby preventing man in the middle attacks.

Maintaining private or secret keys

It’s very important to maintain these keys since they are crucial and critical. Currently, they are stored online in databases or through some other medium. They are usually encrypted using encryption algorithms such as Advanced Encryption Standard (AES) to promote integrity before storing them.

Platforms that employ public-key cryptography

Some of the platforms that use public-key cryptography in production include:

Conclusion

Public key cryptography solves the key distribution problem but suffers the threat of man-in-the-middle attack. There are different methods to solve it and among them is using pre-computed keys. The method is still in demand and is used by a lot of successful companies.

In this article, we have covered an introduction to public-key cryptography, algorithms that use public-key cryptography, libraries to use when implementing public-key cryptography in JavaScript.

We also implemented public-key cryptography using tweetnacl.js, we went over man-in-the-middle attack, a brief example using pre-computed keys, how we maintain public keys and private keys, and we mentioned platforms using public-key cryptography today.

You can access the finalized code from here.

Happy Coding!

Resources


Peer Review Contributions by: Mohan Raj


About the author

Kennedy Mwangi

Kennedy is an Information technology graduate from Karatina University. He is fluent in web development with both the front-end and back-end using JavaScript. He is very passionate about Linux and Android development.

This article was contributed by a student member of Section's Engineering Education Program. Please report any errors or innaccuracies to enged@section.io.