import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, IconButton, Tooltip, TextField, Grid, Container, Card, CardHeader, Paper, Typography, } from '@mui/material'; import { encryptMessage, decryptMessage } from '../../../utils/pgp'; import { saveAsJson } from '../../../utils/saveFile'; import { AuditPGPDialog } from '../../Dialogs'; import RobotAvatar from '../../RobotAvatar'; import { systemClient } from '../../../services/System'; import { websocketClient, WebsocketConnection } from '../../../services/Websocket'; // Icons import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import ContentCopy from '@mui/icons-material/ContentCopy'; import VisibilityIcon from '@mui/icons-material/Visibility'; import CircularProgress from '@mui/material/CircularProgress'; import KeyIcon from '@mui/icons-material/Key'; import { ExportIcon } from '../../Icons'; import { useTheme } from '@mui/system'; interface Props { orderId: number; userNick: string; } interface EncryptedChatMessage { userNick: string; validSignature: boolean; plainTextMessage: string; encryptedMessage: string; time: string; index: number; } const EncryptedChat: React.FC = ({ orderId, userNick }: Props): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); const audio = new Audio(`/static/assets/sounds/chat-open.mp3`); const [connected, setConnected] = useState(false); const [peerConnected, setPeerConnected] = useState(false); const [ownPubKey] = useState( (systemClient.getCookie('pub_key') ?? '').split('\\').join('\n'), ); const [ownEncPrivKey] = useState( (systemClient.getCookie('enc_priv_key') ?? '').split('\\').join('\n'), ); const [peerPubKey, setPeerPubKey] = useState(); const [token] = useState(systemClient.getCookie('robot_token') || ''); const [messages, setMessages] = useState([]); const [serverMessages, setServerMessages] = useState([]); const [value, setValue] = useState(''); const [connection, setConnection] = useState(); const [audit, setAudit] = useState(false); const [showPGP, setShowPGP] = useState([]); const [waitingEcho, setWaitingEcho] = useState(false); const [lastSent, setLastSent] = useState('---BLANK---'); const [messageCount, setMessageCount] = useState(0); const [receivedIndexes, setReceivedIndexes] = useState([]); useEffect(() => { if (!connected) { connectWebsocket(); } }, [connected]); useEffect(() => { if (messages.length > messageCount) { audio.play(); setMessageCount(messages.length); } }, [messages, messageCount]); useEffect(() => { if (serverMessages) { serverMessages.forEach(onMessage); } }, [serverMessages]); const connectWebsocket = () => { websocketClient.open(`ws://${window.location.host}/ws/chat/${orderId}/`).then((connection) => { setConnection(connection); setConnected(true); connection.send({ message: ownPubKey, nick: userNick, }); connection.onMessage((message) => setServerMessages((prev) => [...prev, message])); connection.onClose(() => setConnected(false)); connection.onError(() => setConnected(false)); }); }; const createJsonFile: () => object = () => { return { credentials: { own_public_key: ownPubKey, peer_public_key: peerPubKey, encrypted_private_key: ownEncPrivKey, passphrase: token, }, messages: messages, }; }; const onMessage: (message: any) => void = (message) => { const dataFromServer = JSON.parse(message.data); if (dataFromServer && !receivedIndexes.includes(dataFromServer.index)) { setReceivedIndexes((prev) => [...prev, dataFromServer.index]); setPeerConnected(dataFromServer.peer_connected); // If we receive a public key other than ours (our peer key!) if ( connection && dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` && dataFromServer.message != ownPubKey ) { setPeerPubKey(dataFromServer.message); connection.send({ message: `-----SERVE HISTORY-----`, nick: userNick, }); } // If we receive an encrypted message else if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) { decryptMessage( dataFromServer.message.split('\\').join('\n'), dataFromServer.user_nick == userNick ? ownPubKey : peerPubKey, ownEncPrivKey, token, ).then((decryptedData) => { setWaitingEcho(waitingEcho ? decryptedData.decryptedMessage !== lastSent : false); setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent); setMessages((prev) => { const existingMessage = prev.find((item) => item.index === dataFromServer.index); if (existingMessage) { return prev; } else { return [ ...prev, { index: dataFromServer.index, encryptedMessage: dataFromServer.message.split('\\').join('\n'), plainTextMessage: decryptedData.decryptedMessage, validSignature: decryptedData.validSignature, userNick: dataFromServer.user_nick, time: dataFromServer.time, } as EncryptedChatMessage, ].sort((a, b) => a.index - b.index); } }); }); } // We allow plaintext communication. The user must write # to start // If we receive an plaintext message else if (dataFromServer.message.substring(0, 1) == '#') { setMessages((prev) => { const existingMessage = prev.find( (item) => item.plainTextMessage === dataFromServer.message, ); if (existingMessage) { return prev; } else { return [ ...prev, { index: prev.length + 0.001, encryptedMessage: dataFromServer.message, plainTextMessage: dataFromServer.message, validSignature: false, userNick: dataFromServer.user_nick, time: new Date().toString(), } as EncryptedChatMessage, ].sort((a, b) => a.index - b.index); } }); } } }; const onButtonClicked = (e: any) => { if (token && value.indexOf(token) !== -1) { alert( `Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, ); setValue(''); } // If input string contains '#' send unencrypted and unlogged message else if (connection && value.substring(0, 1) == '#') { connection.send({ message: value, nick: userNick, }); setValue(''); } // Else if message is not empty send message else if (value != '') { setValue(''); setWaitingEcho(true); setLastSent(value); encryptMessage(value, ownPubKey, peerPubKey, ownEncPrivKey, token).then( (encryptedMessage) => { if (connection) { connection.send({ message: encryptedMessage.toString().split('\n').join('\\'), nick: userNick, }); } }, ); } e.preventDefault(); }; const messageCard: ( message: EncryptedChatMessage, index: number, cardColor: string, userConnected: boolean, ) => JSX.Element = (message, index, cardColor, userConnected) => { return ( } style={{ backgroundColor: cardColor }} title={
{message.userNick} {message.validSignature ? ( ) : ( )}
{ const newShowPGP = [...showPGP]; newShowPGP[index] = !newShowPGP[index]; setShowPGP(newShowPGP); }} >
systemClient.copyToClipboard( showPGP[index] ? message.encryptedMessage : message.plainTextMessage, ) } >
} subheader={ showPGP[index] ? ( {' '} {message.time}
{'Valid signature: ' + message.validSignature}
{' '} {message.encryptedMessage}{' '}
) : ( message.plainTextMessage ) } subheaderTypographyProps={{ sx: { wordWrap: 'break-word', width: '14.3em', position: 'relative', right: '1.5em', textAlign: 'left', fontSize: showPGP[index] ? theme.typography.fontSize * 0.78 : null, }, }} />
); }; const connectedColor = theme.palette.mode === 'light' ? '#b5e3b7' : '#153717'; const connectedTextColor = theme.palette.getContrastText(connectedColor); const ownCardColor = theme.palette.mode === 'light' ? '#d1e6fa' : '#082745'; const peerCardColor = theme.palette.mode === 'light' ? '#f2d5f6' : '#380d3f'; return ( {t('You') + ': '} {connected ? t('connected') : t('disconnected')} {t('Peer') + ': '} {peerConnected ? t('connected') : t('disconnected')}
{messages.map((message, index) => (
  • {message.userNick == userNick ? messageCard(message, index, ownCardColor, connected) : messageCard(message, index, peerCardColor, peerConnected)}
  • ))}
    { if (messages.length > messageCount) el?.scrollIntoView(); }} />
    { setValue(e.target.value); }} sx={{ width: '13.7em' }} />
    setAudit(false)} orderId={Number(orderId)} messages={messages} own_pub_key={ownPubKey || ''} own_enc_priv_key={ownEncPrivKey || ''} peer_pub_key={peerPubKey || 'Not received yet'} passphrase={token || ''} onClickBack={() => setAudit(false)} /> ); }; export default EncryptedChat;