Skip to content

Commit

Permalink
Merge pull request #707 from mynaparrot/encrypt_chat
Browse files Browse the repository at this point in the history
feat: end-to-end encryption (E2EE) for chat + whiteboard
  • Loading branch information
jibon57 authored Jun 12, 2024
2 parents e61d386 + 05867a4 commit cac75db
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 16 deletions.
28 changes: 25 additions & 3 deletions src/components/chat/text-box/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import sanitizeHtml from 'sanitize-html';
import { Room } from 'livekit-client';
import { isEmpty } from 'validator';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';

import { RootState, store, useAppSelector } from '../../../store';
import {
Expand All @@ -17,6 +18,7 @@ import {
DataMsgBodyType,
DataMsgType,
} from '../../../helpers/proto/plugnmeet_datamessage_pb';
import { encryptMessage } from '../../../helpers/websocket/cryptoMessages';

interface ITextBoxAreaProps {
currentRoom: Room;
Expand Down Expand Up @@ -52,6 +54,9 @@ const TextBoxArea = ({
const isLockChatSendMsg = useAppSelector(isLockChatSendMsgSelector);
const isLockSendFile = useAppSelector(isLockSendFileSelector);
const selectedChatOption = useAppSelector(selectedChatOptionSelector);
const e2ee =
store.getState().session.currentRoom.metadata?.room_features
.end_to_end_encryption_features;
const { t } = useTranslation();

const [lockSendMsg, setLockSendMsg] = useState<boolean>(false);
Expand Down Expand Up @@ -120,20 +125,37 @@ const TextBoxArea = ({
//eslint-disable-next-line
}, [chosenEmoji]);

const cleanHtml = (rawText) => {
const cleanHtml = (rawText: string) => {
return sanitizeHtml(rawText, {
allowedTags: ['b', 'i', 'strong', 'br'],
allowedSchemes: ['mailto', 'tel'],
});
};

const sendMsg = async () => {
const msg = cleanHtml(message);
let msg = cleanHtml(message);
if (isEmpty(msg)) {
return;
}
const sid = await currentRoom.getSid();

if (
typeof e2ee !== 'undefined' &&
e2ee.is_enabled &&
e2ee.included_chat_messages &&
e2ee.encryption_key
) {
try {
msg = await encryptMessage(e2ee.encryption_key, msg);
} catch (e: any) {
toast('Encryption error: ' + e.message, {
type: 'error',
});
console.error('Encryption error:' + e.message);
return;
}
}

const sid = await currentRoom.getSid();
const dataMsg = new DataMessage({
type: DataMsgType.USER,
roomSid: sid,
Expand Down
48 changes: 44 additions & 4 deletions src/components/whiteboard/helpers/handleRequestedWhiteboardData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
DataMsgBodyType,
DataMsgType,
} from '../../../helpers/proto/plugnmeet_datamessage_pb';
import { encryptMessage } from '../../../helpers/websocket/cryptoMessages';
import { toast } from 'react-toastify';
import { EndToEndEncryptionFeatures } from '../../../store/slices/interfaces/session';

const broadcastedElementVersions: Map<string, number> = new Map(),
DELETED_ELEMENT_TIMEOUT = 3 * 60 * 60 * 1000; // 3 hours
Expand Down Expand Up @@ -143,11 +146,16 @@ export const broadcastSceneOnChange = (
broadcastScreenDataBySocket(syncableElements, sendTo);
};

export const broadcastScreenDataBySocket = (
export const broadcastScreenDataBySocket = async (
elements: readonly ExcalidrawElement[],
sendTo?: string,
) => {
const session = store.getState().session;
const finalMsg = await handleEncryption(JSON.stringify(elements));
if (typeof finalMsg === 'undefined') {
return;
}

const dataMsg = new DataMessage({
type: DataMsgType.WHITEBOARD,
roomSid: session.currentRoom.sid,
Expand All @@ -158,7 +166,7 @@ export const broadcastScreenDataBySocket = (
sid: session.currentUser?.sid ?? '',
userId: session.currentUser?.userId ?? '',
},
msg: JSON.stringify(elements),
msg: finalMsg,
},
});

Expand Down Expand Up @@ -218,8 +226,13 @@ export const broadcastWhiteboardOfficeFile = (
sendWebsocketMessage(dataMsg.toBinary());
};

export const broadcastMousePointerUpdate = (msg: any) => {
export const broadcastMousePointerUpdate = async (element: any) => {
const session = store.getState().session;
const finalMsg = await handleEncryption(JSON.stringify(element));
if (typeof finalMsg === 'undefined') {
return;
}

const dataMsg = new DataMessage({
type: DataMsgType.WHITEBOARD,
roomSid: session.currentRoom.sid,
Expand All @@ -230,7 +243,7 @@ export const broadcastMousePointerUpdate = (msg: any) => {
sid: session.currentUser?.sid ?? '',
userId: session.currentUser?.userId ?? '',
},
msg: JSON.stringify(msg),
msg: finalMsg,
},
});

Expand Down Expand Up @@ -283,3 +296,30 @@ export const broadcastAppStateChanges = (

sendWebsocketMessage(dataMsg.toBinary());
};

let e2ee: EndToEndEncryptionFeatures | undefined = undefined;
const handleEncryption = async (msg: string) => {
if (!e2ee) {
e2ee =
store.getState().session.currentRoom.metadata?.room_features
.end_to_end_encryption_features;
}
if (
typeof e2ee !== 'undefined' &&
e2ee.is_enabled &&
e2ee.included_whiteboard &&
e2ee.encryption_key
) {
try {
return await encryptMessage(e2ee.encryption_key, msg);
} catch (e: any) {
toast('Encryption error: ' + e.message, {
type: 'error',
});
console.error('Encryption error:' + e.message);
return undefined;
}
} else {
return msg;
}
};
76 changes: 76 additions & 0 deletions src/helpers/websocket/cryptoMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const IV_LENGTH = 12,
algorithm = 'AES-GCM';
let importedKey: null | CryptoKey = null;

const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
};

const base64ToArrayBuffer = (base64: string) => {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};

const importSecretKey = async (secret: string) => {
if (importedKey) {
return importedKey;
}
const rawKey = new TextEncoder().encode(secret);

importedKey = await window.crypto.subtle.importKey(
'raw',
rawKey,
algorithm,
true,
['encrypt', 'decrypt'],
);

return importedKey;
};

const encryptMessage = async (secret: string, message: string) => {
const key = await importSecretKey(secret);
const encoded = new TextEncoder().encode(message);

// Generate a new IV for each encryption to ensure security
const iv = window.crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const cipherText = await window.crypto.subtle.encrypt(
{ name: algorithm, iv: iv },
key,
encoded,
);

const arrayView = new Uint8Array(iv.byteLength + cipherText.byteLength);
arrayView.set(iv);
arrayView.set(new Uint8Array(cipherText), iv.byteLength);

return arrayBufferToBase64(arrayView.buffer);
};

const decryptMessage = async (secret: string, cipherData: string) => {
const key = await importSecretKey(secret);
const data = base64ToArrayBuffer(cipherData);

const iv = data.slice(0, IV_LENGTH);
const cipherText = data.slice(IV_LENGTH);

const textData = await window.crypto.subtle.decrypt(
{ name: algorithm, iv },
key,
cipherText,
);

return new TextDecoder().decode(textData);
};

export { importSecretKey, encryptMessage, decryptMessage };
20 changes: 19 additions & 1 deletion src/helpers/websocket/handleSystemType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../proto/plugnmeet_datamessage_pb';
import { SpeechTextBroadcastFormat } from '../../store/slices/interfaces/speechServices';
import { addSpeechSubtitleText } from '../../store/slices/speechServicesSlice';
import { encryptMessage } from './cryptoMessages';

export const handleSystemTypeData = (body: DataMessage) => {
switch (body.body?.type) {
Expand Down Expand Up @@ -62,10 +63,27 @@ export const handleSystemTypeData = (body: DataMessage) => {
const handleSendChatMsg = (mainBody: DataMessage) => {
const messages = chatMessagesSelector.selectAll(store.getState());
const session = store.getState().session;
const e2ee =
session.currentRoom.metadata?.room_features.end_to_end_encryption_features;

messages
.filter((msg) => msg.from.sid !== 'system')
.slice(-30)
.map(async (msg) => {
let finalMsg = msg.msg;

if (
typeof e2ee !== 'undefined' &&
e2ee.is_enabled &&
e2ee.included_chat_messages &&
e2ee.encryption_key
) {
try {
finalMsg = await encryptMessage(e2ee.encryption_key, msg.msg);
} catch (e: any) {
console.error(e.message);
}
}
const dataMsg: DataMessage = new DataMessage({
type: DataMsgType.USER,
to: mainBody.body?.from?.userId,
Expand All @@ -81,7 +99,7 @@ const handleSendChatMsg = (mainBody: DataMessage) => {
userId: msg.from.userId,
name: msg.from.name,
},
msg: msg.msg,
msg: finalMsg,
isPrivate: msg.isPrivate ? 1 : 0,
},
});
Expand Down
33 changes: 31 additions & 2 deletions src/helpers/websocket/handleUserType.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toast } from 'react-toastify';

import { store } from '../../store';
import { addChatMessage } from '../../store/slices/chatMessagesSlice';
import {
Expand All @@ -10,8 +12,12 @@ import {
DataMsgBodyType,
} from '../proto/plugnmeet_datamessage_pb';
import { IChatMsg } from '../../store/slices/interfaces/dataMessages';
import { decryptMessage } from './cryptoMessages';
import { EndToEndEncryptionFeatures } from '../../store/slices/interfaces/session';

let e2ee: EndToEndEncryptionFeatures | undefined = undefined;

export const handleUserTypeData = (
export const handleUserTypeData = async (
body: DataMsgBody,
message_id?: string,
to?: string,
Expand All @@ -20,6 +26,29 @@ export const handleUserTypeData = (
if (!body.messageId) {
body.messageId = message_id;
}
let finalMsg = body.msg;
if (!e2ee) {
e2ee =
store.getState().session.currentRoom.metadata?.room_features
.end_to_end_encryption_features;
}
if (
body.from?.userId !== 'system' &&
typeof e2ee !== 'undefined' &&
e2ee.is_enabled &&
e2ee.included_chat_messages &&
e2ee.encryption_key
) {
try {
finalMsg = await decryptMessage(e2ee.encryption_key, body.msg);
} catch (e: any) {
toast('Decryption error: ' + e.message, {
type: 'error',
});
console.error('Decryption error:' + e.message);
}
}

const chatMsg: IChatMsg = {
type: 'CHAT',
message_id: body.messageId ?? '',
Expand All @@ -30,7 +59,7 @@ export const handleUserTypeData = (
name: body.from?.name,
},
isPrivate: body.isPrivate === 1,
msg: body.msg,
msg: finalMsg,
};

if (to) {
Expand Down
Loading

0 comments on commit cac75db

Please sign in to comment.