Skip to content

Commit

Permalink
feat: #310 -e2e encrypted oneway audio call added
Browse files Browse the repository at this point in the history
  • Loading branch information
muke1908 committed Sep 19, 2024
1 parent e84381b commit 3ae874e
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 226 deletions.
13 changes: 5 additions & 8 deletions backend/api/call/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,19 @@ router.post(
if (!valid) {
return res.sendStatus(404);
}
const usersInChannel = clients.getClientsByChannel(channel);
const usersInChannelArr = Object.keys(usersInChannel);
const ifSenderIsInChannel = usersInChannelArr.find((u) => u === sender);

if (!ifSenderIsInChannel) {

if (!clients.isSenderInChannel(channel, sender)) {
console.error('Sender is not in channel');
return res.status(401).send({ error: "Permission denied" });
}

const receiver = usersInChannelArr.find((u) => u !== sender);
const receiver = clients.getReceiverIDBySenderID(sender, channel);
if(!receiver) {
console.error('No receiver is in the channel');
return res.status(406).send({ error: "No user available to accept offer" });
return res.status(406).send({ error: "No user available to accept call" });
}

const receiverSid = usersInChannel[receiver].sid;
const receiverSid = clients.getSIDByIDs(receiver, channel).sid;
socketEmit<SOCKET_TOPIC.WEBRTC_SESSION_DESCRIPTION>(SOCKET_TOPIC.WEBRTC_SESSION_DESCRIPTION, receiverSid, description);
return res.send({ status: "ok" });
})
Expand Down
12 changes: 4 additions & 8 deletions backend/api/messaging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ router.post(
if (!valid) {
return res.sendStatus(404);
}
const usersInChannel = clients.getClientsByChannel(channel);
const usersInChannelArr = Object.keys(usersInChannel);
const ifSenderIsInChannel = usersInChannelArr.find((u) => u === sender);

if (!ifSenderIsInChannel) {
if (!clients.isSenderInChannel(channel, sender)) {
console.error('Sender is not in channel');
return res.status(401).send({ error: "Permission denied" });
}

const receiver = usersInChannelArr.find((u) => u !== sender);
const receiver = clients.getReceiverIDBySenderID(sender, channel);
if(!receiver) {
console.error('No receiver is in the channel');
return;
Expand All @@ -53,10 +50,9 @@ router.post(
};

if (image) {
const { imageurl } = await uploadImage(image);
dataToPublish.image = imageurl;
return res.status(400).send({ message: "Image not supported" });
}
const receiverSid = usersInChannel[receiver].sid;
const receiverSid = clients.getSIDByIDs(receiver, channel).sid;
socketEmit<SOCKET_TOPIC.CHAT_MESSAGE>(SOCKET_TOPIC.CHAT_MESSAGE, receiverSid, dataToPublish);
return res.send({ message: "message sent", id, timestamp });
})
Expand Down
6 changes: 6 additions & 0 deletions backend/socket.io/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ class Clients implements ClientRecordInterface{
delete this.clientRecord[channelID][userID];
}

isSenderInChannel(channel: string, sender: string): boolean {
const usersInChannel = this.getClientsByChannel(channel);
const usersInChannelArr = Object.keys(usersInChannel);
return !!usersInChannelArr.find((u) => u === sender);
}

}

const clientInstance = new Clients();
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Button/Style.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.button {
padding: 10px 20px;
padding: 6px 20px;
display: inline-block;
border-radius: 3px;
border-color: transparent;
Expand Down
20 changes: 5 additions & 15 deletions client/src/components/DeleteChatLink/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
import React, { useContext } from "react";
import { ThemeContext } from "../../ThemeContext";
import styles from "./Style.module.css";
import React from "react";
import Button from "../Button";

const DeleteChatLink = ({ handleDeleteLink }: any) => {
const [darkMode] = useContext(ThemeContext);

const deleteHandler = async () => {
if (window.confirm("Delete chat link forever?")) await handleDeleteLink();
};

return (
<div>
<div
className={`${styles.deleteButton} ${!darkMode && styles.lightModeDelete}`}
role="button"
onClick={deleteHandler}
>
Delete
</div>
<Button onClick={deleteHandler} label = "Delete" type="secondary"/>
</div>
);
};
)
}

export default DeleteChatLink;
114 changes: 78 additions & 36 deletions client/src/components/Messaging/UserStatusInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import styles from "./styles/UserStatusInfo.module.css";
import ThemeToggle from "../ThemeToggle/index";
import imageRetryIcon from "./assets/image-retry.png";
import DeleteChatLink from "../DeleteChatLink";
import Button from "../Button";
import { IChatE2EE } from "@chat-e2ee/service";

export const UserStatusInfo = ({
online,
getSetUsers,
channelID,
handleDeleteLink,
startCall,
endCall
chate2ee
}: {
online: any;
getSetUsers: any;
channelID: any;
handleDeleteLink: any;
startCall: any,
endCall: any
chate2ee: IChatE2EE
}) => {
const [ call, setCall ] = useState(null);
const [loading, setLoading] = useState(false);
const [ callState, setCallState ] = useState(undefined);

useEffect(() => {
chate2ee.onCallAdded((call) => {
setCall(call);
});

chate2ee.onCallRemoved(() => {
setCall(null);
});

chate2ee.onPCStateChanged((state) => {
setCallState(state);
});
}, []);

const makeCall = async () => {
if(call) {
console.error('call is already active');
return;
}

const newCall = await chate2ee.startCall();
setCall(newCall);
}

const stopCall = async() => {
chate2ee.endCall();
setCall(null);
}

const fetchKeyAgain = async () => {
if (loading) return;
Expand All @@ -30,43 +61,54 @@ export const UserStatusInfo = ({
};

return (
<div className={styles.userInfo}>
{online ? (
<span className={styles.userInfoOnline}>
Alice {"<"}Online{">"}
</span>
) : (
<div className={styles.userOnlineWaiting}>
Waiting for Alice to join...
<img
className={
loading ? `${styles.retryImageIcon} ${styles.loading}` : `${styles.retryImageIcon}`
}
src={imageRetryIcon}
onClick={fetchKeyAgain}
alt="retry-icon"
/>
</div>
)}
{
online && <CallButton call={startCall}/>
}
<DeleteChatLink handleDeleteLink={handleDeleteLink} />
<ThemeToggle />
</div>
<>
{ call && (<CallStatus state={callState}/>) }
<div className={styles.userInfo}>
{online ? (
<span className={styles.userInfoOnline}>
{"<"}Online{">"}
</span>
) : (
<div className={styles.userOnlineWaiting}>
Waiting for Alice to join...
<img
className={
loading ? `${styles.retryImageIcon} ${styles.loading}` : `${styles.retryImageIcon}`
}
src={imageRetryIcon}
onClick={fetchKeyAgain}
alt="retry-icon"
/>
</div>
)}
{
online && <CallButton makeCall={makeCall} stopCall={stopCall} call={call}/>
}
<DeleteChatLink handleDeleteLink={handleDeleteLink} />
<ThemeToggle />
</div>
</>
);
};


const CallButton = ({ call }: {call: any}) => {
const CallStatus = ({state}: {state:any}) => {
return(
<div className={styles.callStatusBar}>Call Status: {state}</div>
)
}

const CallButton = ({ makeCall, stopCall, call }: { makeCall: any, stopCall: any, call: any }) => {
const callButtonHandler = () => {
if(call) {
stopCall();
}else {
makeCall();
}
}
return (
<div>
<div
role="button"
onClick={call}
>
Call
</div>
<Button onClick={callButtonHandler} label = { call ? 'Stop' : 'Call' } type="primary"/>
</div>
)
}
10 changes: 10 additions & 0 deletions client/src/components/Messaging/styles/UserStatusInfo.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
.callStatusBar {
position: fixed;
background: #4caf50;
width: 100%;
left: 0;
padding: 2px;
font-size: 12px;
color: white;
bottom: 0;
}
.userInfo {
padding: 15px 0px 15px 0px;
font-weight: 400;
Expand Down
3 changes: 1 addition & 2 deletions client/src/pages/messaging/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,7 @@ const Chat = () => {
getSetUsers={getSetUsers}
channelID={channelID}
handleDeleteLink={handleDeleteLink}
startCall={() => chate2ee.startCall()}
endCall={() => chate2ee.endCall()}
chate2ee={chate2ee}
/>

<div className={styles.messageContainer}>
Expand Down
2 changes: 1 addition & 1 deletion service/src/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cryptoUtils } from "./crypto";
import { cryptoUtils } from "./cryptoRSA";

describe('cryptoUtils', () => {
const mockBase64String = 'ZW5jcnlwdGVkLXRleHQ='; // decoded = encrypted-text
Expand Down
86 changes: 86 additions & 0 deletions service/src/cryptoAES.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Symmetric key encryption used for encrypting Audio/Video data
*/
export class AesGcmEncryption {
private aesKeyLocal?: CryptoKey;
private aesKeyRemote?: CryptoKey;

public async int(): Promise<CryptoKey> {
if(this.aesKeyLocal) {
return this.aesKeyLocal;
}
const key = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);

this.aesKeyLocal = key;
return this.aesKeyLocal;
}

public getRemoteAesKey(): CryptoKey {
if(!this.aesKeyRemote) {
throw new Error("AES key from remote not set.");
}
return this.aesKeyRemote;
}

/**
* To Do:
* this key is plain text, can be used to decrypt data.
* Should not be transmitted over network.
* Use cryptoUtils to encrypt the key and exchange.
*/
public async getRawAesKeyToExport(): Promise<string> {
if(!this.aesKeyLocal) {
throw new Error('AES key not generated');
}
const jsonWebKey = await crypto.subtle.exportKey("jwk", this.aesKeyLocal);
return JSON.stringify(jsonWebKey);
}

public async setRemoteAesKey(key: string): Promise<void> {
const jsonWebKey = JSON.parse(key);
this.aesKeyRemote = await crypto.subtle.importKey(
"jwk",
jsonWebKey,
{ name: "AES-GCM" },
true, // Key is usable for decryption
["decrypt"] // Usage options for the key
);

}

public async encryptData(data: ArrayBuffer) {
// Generate an Initialization Vector (IV) for AES-GCM (12 bytes)
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt the frame data using AES-GCM
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
this.aesKeyLocal, // Symmetric key for encryption
data // The frame data to be encrypted
);


return { encryptedData: new Uint8Array(encryptedData) , iv };
}

public async decryptData(data: Uint8Array, iv: Uint8Array): Promise<ArrayBuffer> {
if(!this.aesKeyRemote) {
throw new Error('Remote AES key not set.')
}
return crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
this.aesKeyRemote, // Symmetric key for decryption
data // The encrypted frame data
);
}

}
Loading

0 comments on commit 3ae874e

Please sign in to comment.