Creating Real-Time Chat App using React And Socket.io with E2E Encryption

Written by rishabh-verma | Published 2020/08/25
Tech Story Tags: programming | javascript | latest-tech-stories | reactjs | socketio | nodejs | realtime | web-development | web-monetization

TLDR We will be using a single secret key to encrypt and decrypt our messages, thus having symmetric encryption architecture. Whatsapp uses the Diffie-Helman technique to achieve Asymmetrical encryption, it is one of those techniques which can be used to produce most secure chat applications, if you want to learn more about this, please refer this link. Here is the example of the chat app we are going to build using React and Socket.io with E2E encryption. We will use aes256 package with your secret key.via the TL;DR App

Github link: backend — link, frontend — link.
So you might be wondering how WhatsApp, telegram type application says that their user’s data is “Encrypted” all across the network.
That means all the messages stored in their database are encrypted, so even if some “third party” try to “tap” the messages while they are on there way to reach there destination, the intercepted message will be in encrypted form.
In this article, I am going to show you how to build a Simple E2E (which is not going to be as secure as Whatsapp uses, but still, it is better than having nothing).
We will be using a single secret key to encrypt and decrypt our messages, thus having symmetric encryption architecture.
Note, Whatsapp uses the Diffie-Helman technique to achieve Asymmetrical encryption, it is one of those techniques which can be used to produce most secure chat applications, if you want to learn more about this, please refer this link.

Working

As shown in the above picture, we will create a secret key that will be stored in frontend (For now, I am storing it in the frontend file itself but for production, you have to save it in the .ENV variables of your server where you have deployed your front-end).
Whenever a user sends the message, we have to encrypt it using aes256 npm package with your secret key. We will repeat the process after receiving the encrypted message, but this time it will be decrypted using the same secret key.

Code

Backend (Node, Express, Socket.io)

Folder Structure
Backend
 |- dummyuser.js
 |- server.js
 |- package.json
Dependencies to install
npm i socket.io express dotenv cors colors
npm i nodemon -d
Go to dummyuser.js
const users = [];

// Join user to chat
function userJoin(id, username, room) {
  const user = { id, username, room };

  users.push(user);
  console.log(users, "users");

  return user;
}
console.log("user out", users);

// Get current user
function getCurrentUser(id) {
  return users.find((user) => user.id === id);
}

// User leaves chat
function userLeave(id) {
  const index = users.findIndex((user) => user.id === id);

  if (index !== -1) {
    return users.splice(index, 1)[0];
  }
}

module.exports = {
  userJoin,
  getCurrentUser,
  userLeave,
};
  1. Here we are creating three functions that will take care of the user. The userjoin() function will add a user to the empty array users.
  2. The User Object consists of 3 keys — id, username, and room name, the room name is basically like a “WhatsApp group” which will tell the user belongs to this particular room.
  3. getcurrentuser(), will take the id of a particular user and returns its user object.
  4. And whenever a user leaves chat (Disconnect) we will be calling userLeave() which accept a user id and will delete that user object from the array users.

Go to server.js

Import packages and initial setup

const express = require("express");
const app = express();
const socket = require("socket.io");
const color = require("colors");
const { getCurrentUser, userLeave, userJoin } = require("./dummyuser");

const port = 8000;


var server = app.listen(
  port,
  console.log(
    `Server is running in ${process.env.NODE_ENV} on port ${process.env.PORT} `
      .yellow.bold
  )
);

const io = socket(server);
Here we are just importing modules, functions from dummyuser.js, listening on port 8000, and initializing the socket.
//everything related to io will go here
io.on("connection", (socket) => {
  //when new user join room
  socket.on("joinRoom", ({ username, roomname }) => {
    //* create user
    const user = userJoin(socket.id, username, roomname);
    console.log(socket.id, "=id");
    socket.join(user.room);

    //* emit message to user to welcome him/her
    socket.emit("message", {
      userId: user.id,
      username: user.username,
      text: `Welcome ${user.username}`,
    });

    //* Broadcast message to everyone except user that he has joined
    socket.broadcast.to(user.room).emit("message", {
      userId: user.id,
      username: user.username,
      text: `${user.username} has joined the chat`,
    });
  });

  //when somebody send text
  socket.on("chat", (text) => {
    //* get user room and emit message
    const user = getCurrentUser(socket.id);

    io.to(user.room).emit("message", {
      userId: user.id,
      username: user.username,
      text: text,
    });
  });

  // Disconnect , when user leave room
  socket.on("disconnect", () => {
    // * delete user from users & emit that user has left the chat
    const user = userLeave(socket.id);

    if (user) {
      io.to(user.room).emit("message", {
        userId: user.id,
        username: user.username,
        text: `${user.username} has left the chat`,
      });
    }
  });
});
After initializing the socket, everything related to sockets will go into io.on(connection , () => “everything will go here”) this callback.
Here we have two functions, socket.on(“joinRoom”), socket.on(“chat”). The joinRoom function will only run whenever a new user joins the room.
Here we will be emitting a welcome message to him/her and broadcasting a message (user has joined ) to all the users (except him/her).
socket.on(“chat”) will handle the back and forth message sending part.
Also, whenever a user disconnects, we will be sending “user has left the chat” message to all the people in the room.

Frontend (React, Redux, Socket.io-client, aes256)

Folder Structure
Dependencies to install
npm i node-sass react-lottie react-redux react-router-dom redux
Initial setup
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducers from "./store/reducer/index";

const store = createStore(rootReducers);
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
Here we are adding redux and importing reducers from ./store/reducer/index
/store/action/index.js
export const process = (encrypt, text, cypher) => {
  return {
    type: "PROCESS",
    payload: {
      encrypt,
      text,
      cypher,
    },
  };
};
/store/reducer/index.js

import { combineReducers } from "redux";
import { ProcessReducer } from "./process";
const rootReducers = combineReducers({
  ProcessReducer: ProcessReducer,
});
export default rootReducers;
/store/reducer/process.js
export const ProcessReducer = (state = {}, action) => {
  switch (action.type) {
    case "PROCESS":
      return { ...action.payload };

    default:
      return state;
  }
};
In the above files, we are adding redux into our React App and creating an action called process which will be responsible for sending messages (both incoming and outgoing) to ‘aes.js’ (which is responsible for encryption and decryption) and getting data from ‘aes.js’ back to our components.

Go to App.js
import React from "react";
import Chat from "./chat/chat";
import Process from "./process/process";
import "./App.scss";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./home/home";
import io from "socket.io-client";
const socket = io("https://chatapprishabh098.azurewebsites.net");
function Appmain(props) {
  return (
    <React.Fragment>
      <div className="right">
        <Chat
          username={props.match.params.username}
          roomname={props.match.params.roomname}
          socket={socket}
        />
      </div>
      <div className="left">
        <Process />
      </div>
    </React.Fragment>
  );
}
function App() {
  return (
    <Router>
      <div className="App">
        <Switch>
          <Route path="/" exact>
            <Home socket={socket} />
          </Route>
          <Route 
          path="/chat/:roomname/:username"      
          component={Appmain} />
        </Switch>
      </div>
    </Router>
  );
}

export default App;
Here we added routes and importing components,
Routes, on-base URL we are rendering home components that are responsible for getting user name and room name.
On path “/chat/roomname/username” we are rendering a component AppMain which returns two div one is the chatbox and the other tells us the process where the encrypted incoming message and the decrypted message is shown.
Add the required styling for App.js, and globals.scss
App.scss

@import "./globals";
.App {
  width: 100%;
  height: 100vh;
  background-color: $backgroundColor;
  display: flex;
  justify-content: center;
  align-items: center;
  .right {
    flex: 2;
  }
  .left {
    flex: 1;
  }
}
_globals.scss
@import url("https://fonts.googleapis.com/css2?family=Muli:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
* {
  margin: 0 auto;
  padding: 0;
  box-sizing: border-box;
  color: white;
  font-family: "Muli", sans-serif;
}

$backgroundColor: #282b34;
$greyColor: #2d343e;
$redColor: #ff1e56;
$yellowColor: #ffac41;

Go to /home/home.js

import React, { useState } from "react";
import "./home.scss";
import { Link } from "react-router-dom";

function Homepage({ socket }) {
  const [username, setusername] = useState("");
  const [roomname, setroomname] = useState("");

  const sendData = () => {
    if (username !== "" && roomname !== "") {
      socket.emit("joinRoom", { username, roomname });
    } else {
      alert("username and roomname are must !");
    }
  };

  return (
    <div className="homepage">
      <h1>Welcome 🙏</h1>
      <input
        placeholder="Enter your username"
        value={username}
        onChange={(e) => setusername(e.target.value)}
      ></input>
      <input
        placeholder="Enter room name"
        value={roomname}
        onChange={(e) => setroomname(e.target.value)}
      ></input>
      <Link to={`/chat/${roomname}/${username}`}>
        <button onClick={sendData}>Join</button>
      </Link>
    </div>
  );
}

export default Homepage;
Here we are taking input from the user (user and room name) and calling socket.emit(“joinRoom”) passing the username and room name this will activate the“ joinRoom” in our backend which will add the user to the room and emit/broadcast message as discussed above in our backend section.
Add styling to home.js,
Home.scss
@import "../globals";
.homepage {
  width: 400px;
  height: 400px;
  background-color: $greyColor;
  display: flex;
  flex-direction: column;
  padding: 2rem;
  justify-content: space-evenly;
  border-radius: 5px;
  input {
    height: 50px;
    width: 80%;
    text-decoration: none;
    background-color: #404450;
    border: none;
    padding-left: 1rem;
    border-radius: 5px;
    &:focus {
      outline: none;
    }
  }
  button {
    font-size: 1rem;
    padding: 0.5rem 1rem 0.5rem 1rem;
    width: 100px;
    border: none;
    background-color: $yellowColor;
    border-radius: 5px;

    color: black;
    &:hover {
      cursor: pointer;
    }
  }
}
Go to /chat/chat.js
import React, { useState, useEffect, useRef } from "react";
import "./chat.scss";
import { DoDecrypt, DoEncrypt } from "../aes.js";
import { useDispatch } from "react-redux";
import { process } from "../store/action/index";

function Chat({ username, roomname, socket }) {
  const [text, setText] = useState("");
  const [messages, setMessages] = useState([]);

  const dispatch = useDispatch();

  const dispatchProcess = (encrypt, msg, cipher) => {
    dispatch(process(encrypt, msg, cipher));
  };

  useEffect(() => {
    socket.on("message", (data) => {
      //decypt
      const ans = DoDecrypt(data.text, data.username);
      dispatchProcess(false, ans, data.text);
      console.log(ans);
      let temp = messages;
      temp.push({
        userId: data.userId,
        username: data.username,
        text: ans,
      });
      setMessages([...temp]);
    });
  }, [socket]);

  const sendData = () => {
    if (text !== "") {
      //encrypt here
      const ans = DoEncrypt(text);
      socket.emit("chat", ans);
      setText("");
    }
  };
  const messagesEndRef = useRef(null);

  const scrollToBottom = () => {
    messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(scrollToBottom, [messages]);

  console.log(messages, "mess");

  return (
    <div className="chat">
      <div className="user-name">
        <h2>
          {username} <span style={{ fontSize: "0.7rem" }}>in {roomname}</span>
        </h2>
      </div>
      <div className="chat-message">
        {messages.map((i) => {
          if (i.username === username) {
            return (
              <div className="message">
                <p>{i.text}</p>
                <span>{i.username}</span>
              </div>
            );
          } else {
            return (
              <div className="message mess-right">
                <p>{i.text} </p>
                <span>{i.username}</span>
              </div>
            );
          }
        })}
        <div ref={messagesEndRef} />
      </div>
      <div className="send">
        <input
          placeholder="enter your message"
          value={text}
          onChange={(e) => setText(e.target.value)}
          onKeyPress={(e) => {
            if (e.key === "Enter") {
              sendData();
            }
          }}
        ></input>
        <button onClick={sendData}>Send</button>
      </div>
    </div>
  );
}
export default Chat;
Here we are taking input from the user and passing the data to process action which will pass it to the aes function for encryption and then emit the same to socket.on(“chat”), whenever a message is received we are passing that again to aes function but this time for decryption.
apply style for chat

chat.scss

@import "../globals";
@mixin scrollbars(
  $size,
  $foreground-color,
  $background-color: mix($foreground-color, white, 50%)
) {
  // For Google Chrome
  &::-webkit-scrollbar {
    width: $size;
    height: $size;
  }

  &::-webkit-scrollbar-thumb {
    background: $foreground-color;
    border-radius: 10px;
  }

  &::-webkit-scrollbar-track {
    background: $background-color;
    border-radius: 10px;
  }

  // For Internet Explorer
  & {
    scrollbar-face-color: $foreground-color;
    scrollbar-track-color: $background-color;
  }
}
.chat {
  width: 400px;
  height: 600px;
  background-color: $greyColor;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  .user-name {
    text-align: start;
    width: 100%;
    h2 {
      font-weight: 300;
      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
      padding-bottom: 1rem;
    }
  }
  .chat-message {
    height: 70%;
    overflow-y: auto;
    @include scrollbars(5px, $backgroundColor, $yellowColor);
    display: flex;
    flex-direction: column;
    width: 100%;
    align-content: flex-start;

    .message {
      margin-left: 0px;
      max-width: 220px;
      padding-left: 0.5rem;

      p {
        font-size: 1rem;
        background-color: #343841;
        padding: 1rem;
        border-radius: 0px 10px 10px 10px;
        font-weight: 300;
        color: #b4b6be;
      }

      span {
        font-size: 0.6rem;
        font-weight: 200;
        color: #b4b6be;
        padding-left: 0.5rem;
      }
    }
    .mess-right {
      margin-left: auto;
      margin-right: 0px;
      display: flex;
      flex-direction: column;
      max-width: 220px;
      padding-right: 0.5rem;
      p {
        text-align: end;
        border-radius: 10px 0px 10px 10px;
        background-color: $redColor;
        color: white;
      }
      span {
        width: 100%;
        text-align: end;
        padding-left: 0rem;
        padding-right: 0.5rem;
      }
    }
  }

  .send {
    width: 100%;
    height: 50px;
    display: flex;
    input {
      width: 80%;
      text-decoration: none;
      background-color: #404450;
      border: none;
      padding-left: 1rem;
      border-radius: 5px 0px 0px 5px;
      &:focus {
        outline: none;
      }
    }
    button {
      width: 20%;
      border: none;
      background-color: $yellowColor;
      border-radius: 0px 5px 5px 0px;
      &:hover {
        cursor: pointer;
      }
    }
  }
}

Go to aes.js

var aes256 = require("aes256");

var key = "obvwoqcbv21801f19d0zibcoavwpnq";

export const DoEncrypt = (text) => {
  var encrypted = aes256.encrypt(key, text);
  return encrypted;
};
export const DoDecrypt = (cipher, username) => {
  if (cipher.startsWith("Welcome")) {
    return cipher;
  }

  if (cipher.startsWith(username)) {
    return cipher;
  }

  var decrypted = aes256.decrypt(key, cipher);
  return decrypted;
};
Here we are importing aes256 and writing a function where we decrypt the incoming message (except welcome message) and encrypting the outgoing message.

Go to process.js (Optional component)

import React, { useState } from "react";
import Lottie from "react-lottie";
import animationData from "../loading.json";
import { useSelector } from "react-redux";
import "./process.scss";
function Process() {
  const [play, setPlay] = useState(false);

  const state = useSelector((state) => state.ProcessReducer);

  const defaultOptions = {
    loop: true,
    autoplay: true,
    animationData: animationData,
    rendererSettings: {
      preserveAspectRatio: "xMidYMid slice",
    },
  };
  return (
    <div className="process">
      <h5>
        Seceret Key : <span>"obvwoqcbv21801f19d0zibcoavwpnq"</span>
      </h5>
      <div className="incomming">
        <h4>Incomming Data</h4>
        <p>{state.cypher}</p>
      </div>
      <Lottie
        options={defaultOptions}
        height={150}
        width={150}
        isStopped={play}
      />
      <div className="crypt">
        <h4>Decypted Data</h4>
        <p>{state.text}</p>
      </div>
    </div>
  );
}
export default Process;
This is just a component (right column) where we are displaying incoming encrypted. Message and decrypting it using our secret key.
add styling to process.js
@import "../globals";
.process {
  width: 500px;
  min-height: 550px;
  margin-right: 10rem;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  align-items: center;
  padding: 2rem;
  h5 {
    margin-bottom: 2rem;
    font-weight: 300;
    color: rgba(255, 255, 255, 0.4);
    span {
      color: yellow;
    }
  }
  .incomming {
    width: 100%;
    h4 {
      color: rgba(255, 255, 255, 0.4);
      font-weight: 300;
    }
    p {
      margin-top: 0.5rem;
      background-color: rgba(0, 0, 0, 0.4);
      padding: 1.2rem;
      font-size: 1rem;
      border-radius: 5px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
  }
  .crypt {
    width: 100%;
    h4 {
      color: rgba(255, 255, 255, 0.4);
      font-weight: 300;
    }
    p {
      margin-top: 0.5rem;
      background-color: rgba(0, 0, 0, 0.4);
      padding: 1.2rem;
      font-size: 1rem;
      border-radius: 5px;
    }
  }
}
Basically process.js is responsible for showing the incoming encrypted and decrypted messages.
That’s It !, we have finally made a Real-Time chat E2E app. Now just start the react app by writing npm start in the terminal and go to localhost:3000 write the user name and room name and also open another tab and go to locahost:3000, write your name and same room name that you have previously written in the first tab.

Written by rishabh-verma | I am autodidact programmer as well as developer
Published by HackerNoon on 2020/08/25