Link link: Here is the example of the chat app we are going to build ✌.
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 .
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.
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,
};
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.
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;
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
@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;
}
}
}
}
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.
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.
Previously published at https://medium.com/@vermarishabh0987/how-to-build-a-real-time-chat-web-app-using-node-reactjs-socket-io-having-e2e-encryption-18fbbde8a190