diff --git a/frontend/src/components/TradeBox/EncryptedChat.js b/frontend/src/components/TradeBox/EncryptedChat.js deleted file mode 100644 index 9e99990af..000000000 --- a/frontend/src/components/TradeBox/EncryptedChat.js +++ /dev/null @@ -1,565 +0,0 @@ -import React, { Component } from 'react'; -import { withTranslation } from 'react-i18next'; -import { - Button, - IconButton, - Badge, - Tooltip, - TextField, - Grid, - Container, - Card, - CardHeader, - Paper, - Avatar, - Typography, -} from '@mui/material'; -import ReconnectingWebSocket from 'reconnecting-websocket'; -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 } 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'; - -class Chat extends Component { - constructor(props) { - super(props); - } - - state = { - own_pub_key: systemClient.getCookie('pub_key').split('\\').join('\n'), - own_enc_priv_key: systemClient.getCookie('enc_priv_key').split('\\').join('\n'), - peer_pub_key: null, - token: systemClient.getCookie('robot_token'), - messages: [], - value: '', - connected: false, - connection: null, - peer_connected: false, - audit: false, - showPGP: new Array(), - waitingEcho: false, - lastSent: '---BLANK---', - latestIndex: 0, - scrollNow: false, - }; - - componentDidMount() { - websocketClient - .open(`ws://${window.location.host}/ws/chat/${this.props.orderId}/`) - .then((connection) => { - console.log('Connected!'); - - connection.send({ - message: this.state.own_pub_key, - nick: this.props.ur_nick, - }); - - connection.onMessage(this.onMessage); - connection.onClose(() => { - console.log('Socket is closed. Reconnect will be attempted'); - this.setState({ connected: false }); - }); - connection.onError(() => { - console.error('Socket encountered error: Closing socket'); - this.setState({ connected: false }); - }); - - this.setState({ connected: true, connection }); - }); - } - - componentDidUpdate() { - // Only fire the scroll and audio when the reason for Update is a new message - if (this.state.scrollNow) { - const audio = new Audio(`/static/assets/sounds/chat-open.mp3`); - audio.play(); - this.scrollToBottom(); - this.setState({ scrollNow: false }); - } - } - - onMessage = (message) => { - const dataFromServer = JSON.parse(message.data); - console.log('Got reply!', dataFromServer.type); - console.log('PGP message index', dataFromServer.index, ' latestIndex ', this.state.latestIndex); - if (dataFromServer) { - console.log(dataFromServer); - this.setState({ peer_connected: dataFromServer.peer_connected }); - - // If we receive our own key on a message - if (dataFromServer.message == this.state.own_pub_key) { - console.log('OWN PUB KEY RECEIVED!!'); - } - - // If we receive a public key other than ours (our peer key!) - if ( - dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` && - dataFromServer.message != this.state.own_pub_key - ) { - if (dataFromServer.message == this.state.peer_pub_key) { - console.log('PEER HAS RECONNECTED USING HIS PREVIOUSLY KNOWN PUBKEY'); - } else if ( - (dataFromServer.message != this.state.peer_pub_key) & - (this.state.peer_pub_key != null) - ) { - console.log('PEER PUBKEY HAS CHANGED'); - } - console.log('PEER PUBKEY RECEIVED!!'); - this.setState({ peer_pub_key: dataFromServer.message }); - - // After receiving the peer pubkey we ask the server for the historic messages if any - this.state.connection.send({ - message: `-----SERVE HISTORY-----`, - nick: this.props.ur_nick, - }); - } - - // If we receive an encrypted message - else if ( - dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----` && - dataFromServer.index > this.state.latestIndex - ) { - decryptMessage( - dataFromServer.message.split('\\').join('\n'), - dataFromServer.user_nick == this.props.ur_nick - ? this.state.own_pub_key - : this.state.peer_pub_key, - this.state.own_enc_priv_key, - this.state.token, - ).then((decryptedData) => - this.setState((state) => ({ - scrollNow: true, - waitingEcho: - this.state.waitingEcho == true - ? decryptedData.decryptedMessage != this.state.lastSent - : false, - lastSent: - decryptedData.decryptedMessage == this.state.lastSent - ? '----BLANK----' - : this.state.lastSent, - latestIndex: - dataFromServer.index > this.state.latestIndex - ? dataFromServer.index - : this.state.latestIndex, - messages: [ - ...state.messages, - { - index: dataFromServer.index, - encryptedMessage: dataFromServer.message.split('\\').join('\n'), - plainTextMessage: decryptedData.decryptedMessage, - validSignature: decryptedData.validSignature, - userNick: dataFromServer.user_nick, - time: dataFromServer.time, - }, - ].sort(function (a, b) { - // order the message array by their index (increasing) - return 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) == '#') { - console.log('Got plaintext message', dataFromServer.message); - this.setState((state) => ({ - scrollNow: true, - messages: [ - ...state.messages, - { - index: this.state.latestIndex + 0.001, - encryptedMessage: dataFromServer.message, - plainTextMessage: dataFromServer.message, - validSignature: false, - userNick: dataFromServer.user_nick, - time: new Date().toString(), - }, - ], - })); - } - } - }; - - scrollToBottom = () => { - this.messagesEnd.scrollIntoView({ behavior: 'smooth' }); - }; - - onButtonClicked = (e) => { - // If input string contains token. Do not set message - if (this.state.value.indexOf(this.state.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.`, - ); - this.setState({ value: '' }); - } - - // If input string contains '#' send unencrypted and unlogged message - else if (this.state.value.substring(0, 1) == '#') { - this.state.connection.send({ - message: this.state.value, - nick: this.props.ur_nick, - }); - this.setState({ value: '' }); - } - - // Else if message is not empty send message - else if (this.state.value != '') { - this.setState({ value: '', waitingEcho: true, lastSent: this.state.value }); - encryptMessage( - this.state.value, - this.state.own_pub_key, - this.state.peer_pub_key, - this.state.own_enc_priv_key, - this.state.token, - ).then( - (encryptedMessage) => - console.log('Sending Encrypted MESSAGE', encryptedMessage) & - this.state.connection.send({ - message: encryptedMessage.split('\n').join('\\'), - nick: this.props.ur_nick, - }), - ); - } - e.preventDefault(); - }; - - createJsonFile = () => { - return { - credentials: { - own_public_key: this.state.own_pub_key, - peer_public_key: this.state.peer_pub_key, - encrypted_private_key: this.state.own_enc_priv_key, - passphrase: this.state.token, - }, - messages: this.state.messages, - }; - }; - - messageCard = (props) => { - const { t } = this.props; - return ( - - - } - style={{ backgroundColor: props.cardColor }} - title={ - -
-
- {props.message.userNick} - {props.message.validSignature ? ( - - ) : ( - - )} -
-
- - this.setState((prevState) => { - const newShowPGP = [...prevState.showPGP]; - newShowPGP[props.index] = !newShowPGP[props.index]; - return { showPGP: newShowPGP }; - }) - } - > - - -
-
- - - systemClient.copyToClipboard( - this.state.showPGP[props.index] - ? props.message.encryptedMessage - : props.message.plainTextMessage, - ) - } - > - - - -
-
-
- } - subheader={ - this.state.showPGP[props.index] ? ( - - {' '} - {props.message.time}
{'Valid signature: ' + props.message.validSignature}{' '} -
{props.message.encryptedMessage}{' '} -
- ) : ( - props.message.plainTextMessage - ) - } - subheaderTypographyProps={{ - sx: { - wordWrap: 'break-word', - width: '200px', - color: '#444444', - fontSize: this.state.showPGP[props.index] ? 11 : null, - }, - }} - /> -
- ); - }; - - render() { - const { t } = this.props; - return ( - - - - - - - {t('You') + ': '} - {this.state.connected ? t('connected') : t('disconnected')} - - - - - - - - {t('Peer') + ': '} - {this.state.peer_connected ? t('connected') : t('disconnected')} - - - - - -
- - {this.state.messages.map((message, index) => ( -
  • - {message.userNick == this.props.ur_nick ? ( - - ) : ( - - )} -
  • - ))} -
    { - this.messagesEnd = el; - }} - >
    -
    -
    - - - { - this.setState({ value: e.target.value }); - this.value = this.state.value; - }} - sx={{ width: 219 }} - /> - - - - - -
    -
    - -
    - - - this.setState({ audit: false })} - orderId={Number(this.props.orderId)} - messages={this.state.messages} - own_pub_key={this.state.own_pub_key} - own_enc_priv_key={this.state.own_enc_priv_key} - peer_pub_key={this.state.peer_pub_key ? this.state.peer_pub_key : 'Not received yet'} - passphrase={this.state.token} - onClickBack={() => this.setState({ audit: false })} - /> - - - - - - - - - - - - - - - ); - } -} - -export default withTranslation()(Chat); diff --git a/frontend/src/components/TradeBox/EncryptedChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/index.tsx new file mode 100644 index 000000000..bc55c3ad4 --- /dev/null +++ b/frontend/src/components/TradeBox/EncryptedChat/index.tsx @@ -0,0 +1,523 @@ +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; diff --git a/frontend/src/components/TradeBox/index.js b/frontend/src/components/TradeBox/index.js index c40acd5f0..c962bd653 100644 --- a/frontend/src/components/TradeBox/index.js +++ b/frontend/src/components/TradeBox/index.js @@ -29,7 +29,7 @@ import { import { LoadingButton } from '@mui/lab'; import QRCode from 'react-qr-code'; import Countdown, { zeroPad } from 'react-countdown'; -import Chat from './EncryptedChat'; +import EncryptedChat from './EncryptedChat'; import TradeSummary from './TradeSummary'; import MediaQuery from 'react-responsive'; import { systemClient } from '../../services/System'; @@ -1469,7 +1469,7 @@ class TradeBox extends Component { )} - + {showDisputeButton ? this.showOpenDisputeButton() : ''} {showSendButton ? this.showFiatSentButton() : ''} diff --git a/frontend/src/services/Websocket/WebsocketConnectionWeb/index.ts b/frontend/src/services/Websocket/WebsocketConnectionWeb/index.ts index 500f67ec4..4e6229ea6 100644 --- a/frontend/src/services/Websocket/WebsocketConnectionWeb/index.ts +++ b/frontend/src/services/Websocket/WebsocketConnectionWeb/index.ts @@ -17,7 +17,7 @@ class WebsocketConnectionWeb implements WebsocketConnection { ); }; - public onMessage: (event: (message: object) => void) => void = (event) => { + public onMessage: (event: (message: any) => void) => void = (event) => { this.rws.addEventListener('message', event); }; diff --git a/frontend/src/services/Websocket/index.ts b/frontend/src/services/Websocket/index.ts index 0e30c7642..70efd88fa 100644 --- a/frontend/src/services/Websocket/index.ts +++ b/frontend/src/services/Websocket/index.ts @@ -2,7 +2,7 @@ import WebsocketWebClient from './WebsocketWebClient'; export interface WebsocketConnection { send: (message: object) => void; - onMessage: (event: (message: object) => void) => void; + onMessage: (event: (message: any) => void) => void; onClose: (event: () => void) => void; onError: (event: () => void) => void; }