Create an End-to-End Encrypted Chat App with JavaScript

In this comprehensive tutorial, you will learn how to create a secure, end-to-end encrypted chat application using JavaScript, Node.js, and the Signal Protocol. We'll walk you through each step, making it easy for you to build your own encrypted chat app.

Table of Contents

Introduction

End-to-end encryption is a vital security feature for messaging applications. It ensures that only the intended recipients can read the messages, protecting user privacy and data from potential eavesdroppers.

The Signal Protocol is a widely used cryptographic protocol that provides end-to-end encryption for instant messaging. It is implemented in popular messaging apps like WhatsApp and Signal.

In this tutorial, we'll show you how to build an end-to-end encrypted chat application using JavaScript, Node.js, and the Signal Protocol.

Prerequisites

To follow this tutorial, you should have a basic understanding of:

  • JavaScript (ES6)
  • Node.js
  • HTML and CSS
  • Git

You will also need the following software installed on your computer:

  • Node.js (version 14 or higher)
  • npm (version 6 or higher)
  • A code editor (e.g., Visual Studio Code)
  • A web browser (e.g., Google Chrome)

Setting Up the Project

First, create a new directory for your project and navigate to it in your terminal:

mkdir encrypted-chat-app
cd encrypted-chat-app

Initialize a new npm project with the default settings:

npm init -y

Install the necessary dependencies:

npm install express ws signal-protocol

Create an index.js file in the project root:

touch index.js

Building the Backend

Creating the Server

In index.js, set up a basic Express server:

const express = require("express");
const app = express();
const port = process.env.PORT || 3000;

app.use(express.static("public"));

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Setting Up WebSocket Connection

Import the ws module and create a WebSocket server:

const WebSocket = require("ws");

const wss = new WebSocket.Server({ server: app.listen(port) });

wss.on("connection", (ws) => {
  console.log("Client connected");

  ws.on("message", (message) => {
    // Handle incoming messages here
  });

  ws.on("close", () => {
    console.log("Client disconnected");
  });
});

Handling Messages

Add a function to broadcast messages to all connected clients:

function broadcast(data) {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

Update the message event handler to broadcast incoming messages:

ws.on("message", (message) => {
  console.log(`Received message: ${message}`);
  broadcast(message);
});

Building the Frontend

Creating the UI

Create a public directory in your project root and add an index.html file with the following content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Encrypted Chat App</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="app">
      <h1>Encrypted Chat App</h1>
      <div id="messages"></div>
      <form id="message-form">
        <input type="text" id="message-input" placeholder="Type your message" />
        <button type="submit">Send</button>
      </form>
    </div>
    <script src="app.js"></script>
  </body>
</html>

Add a styles.css file in the public directory for styling:

body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
}

#app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

#messages {
  border: 1px solid #ccc;
  height: 300px;
  margin-bottom: 10px;
  overflow-y: scroll;
  padding: 10px;
}

#message-form {
  display: flex;
}

#message-input {
  flex-grow: 1;
  margin-right: 10px;
}

Connecting to the WebSocket Server

In the public directory, create an app.js file and establish a connection to the WebSocket server:

const ws = new WebSocket("ws://localhost:3000");

ws.addEventListener("open", () => {
  console.log("Connected to server");
});

ws.addEventListener("message", (event) => {
  // Handle incoming messages here
});

Sending and Receiving Messages

Add functions to send and display messages:

const messageForm = document.getElementById("message-form");
const messageInput = document.getElementById("message-input");
const messages = document.getElementById("messages");

function sendMessage() {
  const message = messageInput.value.trim();
  if (message !== "") {
    ws.send(message);
    messageInput.value = "";
  }
}

function displayMessage(message) {
  const messageElement = document.createElement("div");
  messageElement.textContent = message;
  messages.appendChild(messageElement);
}

messageForm.addEventListener("submit", (event) => {
  event.preventDefault();
  sendMessage();
});

ws.addEventListener("message", (event) => {
  displayMessage(event.data);
});

End-to-End Encryption with Signal Protocol

Generating Keys

Install the libsignal-protocol package:

npm install libsignal-protocol

In app.js, import the libsignal-protocol and generate keys for each user:

const libsignal = require("libsignal");

async function generateKeys() {
  const keyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
  const registrationId = await libsignal.KeyHelper.generateRegistrationId();
  return { keyPair, registrationId };
}

Encrypting Messages

Create a function to encrypt messages using the Signal Protocol:

async function encryptMessage(receiverPublicKey, message) {
  const sessionBuilder = new libsignal.SessionBuilder(store, receiverPublicKey);
  await sessionBuilder.init();

  const sessionCipher = new libsignal.SessionCipher(store, receiverPublicKey);
  const encryptedMessage = await sessionCipher.encrypt(message);
  return encryptedMessage;
}

Decrypting Messages

Create a function to decrypt messages

An AI coworker, not just a copilot

View VelocityAI