End to end encryption in Nodejs using diffiehelman algorithm

Inakujoel
Cuesoft
Published in
6 min readDec 22, 2022

End-to-end encryption is a method of encrypting communication between two parties so that only the intended recipient can read the message. This is achieved by encrypting the message at the sender’s end and then decrypting it at the recipient’s end, using a shared secret key.

One way to implement end-to-end encryption in Node.js is to use the Diffie-Hellman key exchange algorithm. This algorithm allows two parties to generate a shared secret key over an insecure channel, without any prior knowledge of each other’s secret keys.

For this task, a basic knowledge of Nodejs and TypeScript is required, as the phase of setting up the server and routing are all skipped.

let us create a file under the util or helper folder, ( or literally any folder that suits your need )named keygeneration.ts (or whatever you want to name it as)

import { createDiffieHellman, DiffieHellman } from "crypto";

export class Keygeneration {
private initiator!: DiffieHellman;
private recipient!: DiffieHellman;

initiatorKey(): { key: string; prime: string; generator: string } {
const first = createDiffieHellman(512);
this.initiator = first;
const key = first.generateKeys("base64");
const prime = first.getPrime("base64");
const generator = first.getGenerator("base64");
return {
key,
prime,
generator
};
}

recipientKey(prime: string, generator: string): string {
const second = createDiffieHellman(
Buffer.from(prime, "base64"),
Buffer.from(generator, "base64")
);
this.recipient = second;
return second.generateKeys("base64");
}

initiatorSecret(recipientKey: string): string {
const secret = this.initiator.computeSecret(
recipientKey,
"base64",
"base64"
);
return secret;
}

recipientSecret(initiatorKey: string): string {
const secret = this.recipient.computeSecret(
initiatorKey,
"base64",
"base64"
);
return secret;
}
}

Now what does this class do?

As diffiehelman requires two parties working together to generate a common secret for encryption purposes, that gave rise to two classes of users, the “initiator” and the “recipient”.

“Initiator” in this context refers to the party that intiated the key exchange transaction while the “recipient” is the party that responds to the initiators request.

what does the initiatorKey method do?

This method is called by the party that want to initiate communication between the parties, it uses the diffiehelman to generate initiator’s public key, prime, and generator in string format.

By default this method returns these values as buffer types, but due to the fact that they are to be sent over the http/https protocol or any mechanism you deem fit to transmit the data eg message brokers like kafta, rabbitMQ etc or a simple redis will suffice, base64 encoding format works well as buffers are quite difficult to handle when sent over http protocol, strings are a lot more easier to handle. For this example we would be using http as our medium.

what does recipientKey method do?

This method is what must be called on the receiving service end, it receives the prime and generator values sent from the initiator to generate its own public key and then sent back to the initiator

This is the first half of the transaction, both parties are now privy to eachother’s public keys, what’s left is to generate the secret key that would be used for the encryption process. For the second half, both parties would then proceed to generate the secret key; the initiator uses the initiatorSecret method and the recipient uses the recipientSecret method.

Now we can proceed to the actual encryption, let us a create a file called encrypter.ts or whatever you want call it.

import {
BinaryLike,
createCipheriv,
createDecipheriv,
randomBytes,
scryptSync
} from "crypto";

export class Encrypter {
private algorithm: string;
private key: Buffer;

constructor(encryptionKey: BinaryLike, salt: string) {
this.algorithm = "aes-192-cbc";
this.key = scryptSync(encryptionKey, salt, 24);
}

encrypt(clearText: string): string {
const iv = randomBytes(16);
const cipher = createCipheriv(this.algorithm, this.key, iv);
const encrypted = cipher.update(clearText, "utf8", "hex");
return [
encrypted + cipher.final("hex"),
Buffer.from(iv).toString("hex")
].join("|");
}

decrypt(encryptedText: string): string {
const [encrypted, iv] = encryptedText.split("|");
if (!iv) throw new Error("IV not found");
const decipher = createDecipheriv(
this.algorithm,
this.key,
Buffer.from(iv, "hex")
);
return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
}
}

What does this class do?

This is where the actual encryption takes place, this class takes in the encryption key already created and a random salt for the encryption process.

what does the encrypt method do?

It uses the “aes-192-cbc” algorithm to encrypt whatever payload you want to encrypt, exact details of the inner workngs are beyond the scope of this article, but what it basically do is that, it takes any data and returns an unrecognizable text ( encrypted form).

An alternate method; decrypt method, does the oppositeof what the encrypt method does, it decrypts the encrypted payload to its original state as it were before.

Now all these looks great, as the building blocks are ready, what remains is the integrating of what we’ve just built.

One issue we might have to contend with, is where to store the secret key that have been generated, one might decide to store it in a database, redis, or whatever you deem fit.

For this tutorial, we will using redis, as it very fast for read and write operations.

import { randomBytes } from "crypto";
import { createClient } from "redis";
import { Keygeneration } from "./keygeneration";
import { Encrypter } from "./encrypter";

export class Encryption {
private keygeneration: Keygeneration;
private redisClient : ReturnType<typeof createClient>;

constructor() {
this.keygeneration = new Keygeneration();
this.redisClient = createClient()
this.redisClient.connect()
}

async getInitiatorSecret(url?: string) {
const keyDetails = this.keygeneration.initiatorKey();
const { prime, generator, key } = keyDetails;
let { data }= await axios.post(url, keyDetails);
const secretKey = this.keygeneration.initiatorSecret(data.public_key);
await this.redisClient.set(data.public_key, secretKey);
return { publicKey: key };
}

async recipientKeys(
prime: string,
generator: string,
key: string
): Promise<{ public_key: string }> {
const public_key = this.keygeneration.recipientKey(prime, generator);
const secretKey = this.keygeneration.recipientSecret(key);
await this.redisClient.set(key, secretKey);
return { public_key };
}

async decryptPayload(
headerValue: string,
data: { payload: string; salt: string }
) {
const secretKey = await this.redisClient.get(headerValue);
if (!secretKey) {
throw new Error("Invalid header value");
}
const encryption = new Encrypter(secretKey, data.salt);
return JSON.parse(encryption.decrypt(data.payload));
}

async encryptPayload(publicKey: string, payload: object) {
const salt = randomBytes(16).toString("base64");
const secretKey = await this.redisClient.get(publicKey);
if (!secretKey) {
throw new Error("Invalid public key value");
}
const encryption = new Encrypter(secretKey, salt);
return { payload: encryption.encrypt(JSON.stringify(payload)), salt };
}
}

What does this class do?

This class servers as a wrapper for the key generation and encryption classes we created earlier on, One point to note is that there is need to be an endpoint on the receiving party end that would be receiving the key, prime and generation payload. let us name this file encryption.ts.

How does information flow? The initiating service or party simply calls the initiatorSecret method, it makes a call to the recipient service designated endpoint that receives the payload, the recipient service sends back its public key to the initiator, the initiator uses the recipient’s public key to generate the shared secret, the initiator then uses it’s own public key to store the secret key in memory

On the recipient’s end when the designated endpoint is called, the recipientKeys method is called, which takes the payload sent, generates it’s own secret key, stores the secretkey against the initiator’s public key in redis, then finally send its own public key back to the initiator so that it can also generate it’s own secret key.

After all these has been done, what’s left is the actual encrypting of the payloads to be sent.

The initiator calls the encryptPayload method with it’s public key and the data to be sent, it returns the encrypted payload and salt which can now be sent to the desired recipient’s endpoint.

Note: the initiator’s public key must always be place in an header when sending the encrypted payload

On the recipient’s end, for the said endpoint, the decryptPayload method is called with the incoming header value and the encrypted payload as arguments to be decrypted.

After decryption, your logic can now be applied to the data that has been sent, if there is need for data to be returned back to the initiator, the recipient service now calls the encryptPayload with the initial initiator’s public key and the new data to be sent as arguments. Then the initiator on it’s end will now call the decryptPayload method with it’s own public key to get the returned data.

An example of this implementation can be found in this repository

--

--