diff --git a/chat/consumers.py b/chat/consumers.py index c641814b..37647a30 100644 --- a/chat/consumers.py +++ b/chat/consumers.py @@ -2,6 +2,7 @@ from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async from api.models import Order from chat.models import ChatRoom +from django.utils import timezone import json @@ -109,6 +110,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): async def receive(self, text_data): text_data_json = json.loads(text_data) + print(text_data) message = text_data_json["message"] peer_connected = await self.is_peer_connected() @@ -131,6 +133,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): "message": message, "user_nick": nick, "peer_connected": peer_connected, + "time":str(timezone.now()), })) pass diff --git a/frontend/src/components/Chat.js b/frontend/src/components/Chat.js index 7f99c31b..170a71b3 100644 --- a/frontend/src/components/Chat.js +++ b/frontend/src/components/Chat.js @@ -2,9 +2,11 @@ import React, { Component } from 'react'; import { withTranslation, Trans} from "react-i18next"; import {Button, Link, Badge, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText, Typography} from "@mui/material"; import ReconnectingWebSocket from 'reconnecting-websocket'; -import * as openpgp from 'openpgp/lightweight'; class Chat extends Component { + // Deprecated chat component + // Will still be used for ~1 week, until users change to robots with PGP keys + constructor(props) { super(props); } diff --git a/frontend/src/components/EncryptedChat.js b/frontend/src/components/EncryptedChat.js new file mode 100644 index 00000000..59db5365 --- /dev/null +++ b/frontend/src/components/EncryptedChat.js @@ -0,0 +1,240 @@ +import React, { Component } from 'react'; +import { withTranslation } from "react-i18next"; +import {Button, Badge, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, Typography} from "@mui/material"; +import ReconnectingWebSocket from 'reconnecting-websocket'; +import { encryptMessage , decryptMessage} from "../utils/pgp"; +import { getCookie } from "../utils/cookies"; +import { saveAsTxt } from "../utils/saveFile"; + +class Chat extends Component { + constructor(props) { + super(props); + } + + state = { + own_pub_key: getCookie('pub_key').split('\\').join('\n'), + own_enc_priv_key: getCookie('enc_priv_key').split('\\').join('\n'), + peer_pub_key: null, + token: getCookie('robot_token'), + messages: [], + value:'', + connected: false, + peer_connected: false, + audit: false, + }; + + rws = new ReconnectingWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.orderId + '/'); + + componentDidMount() { + this.rws.addEventListener('open', () => { + console.log('Connected!'); + this.setState({connected: true}); + if ( this.state.peer_pub_key == null){ + this.rws.send(JSON.stringify({ + type: "message", + message: "----PLEASE SEND YOUR PUBKEY----", + nick: this.props.ur_nick, + })); + } + this.rws.send(JSON.stringify({ + type: "message", + message: this.state.own_pub_key, + nick: this.props.ur_nick, + })); + }); + + this.rws.addEventListener('message', (message) => { + + const dataFromServer = JSON.parse(message.data); + console.log('Got reply!', dataFromServer.type); + + if (dataFromServer){ + console.log(dataFromServer) + + // If we receive our own key on a message + if (dataFromServer.message == this.state.own_pub_key){console.log("ECHO OF OWN PUB KEY RECEIVED!!")} + + // If we receive a request to send our public key + if (dataFromServer.message == `----PLEASE SEND YOUR PUBKEY----`) { + this.rws.send(JSON.stringify({ + type: "message", + message: this.state.own_pub_key, + nick: this.props.ur_nick, + })); + } else + + // 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) { + console.log("PEER KEY RECEIVED!!") + this.setState({peer_pub_key:dataFromServer.message}) + } else + + // If we receive an encrypted message + if (dataFromServer.message.substring(0,27) == `-----BEGIN PGP MESSAGE-----`){ + 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) => + ({ + messages: [...state.messages, + { + encryptedMessage: dataFromServer.message.split('\\').join('\n'), + plainTextMessage: decryptedData.decryptedMessage, + validSignature: decryptedData.validSignature, + userNick: dataFromServer.user_nick, + time: dataFromServer.time + }], + }) + )); + } + this.setState({peer_connected: dataFromServer.peer_connected}) + } + }); + + this.rws.addEventListener('close', () => { + console.log('Socket is closed. Reconnect will be attempted'); + this.setState({connected: false}); + }); + + this.rws.addEventListener('error', () => { + console.error('Socket encountered error: Closing socket'); + }); + + // Encryption/Decryption Example + // console.log(encryptMessage('Example text to encrypt!', + // getCookie('pub_key').split('\\').join('\n'), + // getCookie('enc_priv_key').split('\\').join('\n'), + // getCookie('robot_token')) + // .then((encryptedMessage)=> decryptMessage( + // encryptedMessage, + // getCookie('pub_key').split('\\').join('\n'), + // getCookie('enc_priv_key').split('\\').join('\n'), + // getCookie('robot_token')) + // )) + } + + componentDidUpdate() { + this.scrollToBottom(); + } + + scrollToBottom = () => { + this.messagesEnd.scrollIntoView({ behavior: "smooth" }); + } + + onButtonClicked = (e) => { + if(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.rws.send(JSON.stringify({ + type: "message", + message: encryptedMessage.split('\n').join('\\'), + nick: this.props.ur_nick, + }) + ) & this.setState({value: ""}) + ); + } + e.preventDefault(); + } + + 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) => +
  • + + {/* If message sender is not our nick, gray color, if it is our nick, green color */} + {message.userNick == this.props.ur_nick ? + + + + } + style={{backgroundColor: '#eeeeee'}} + title={message.userNick} + subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} + subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} + /> + : + + + + } + style={{backgroundColor: '#fafafa'}} + title={message.userNick} + subheader={this.state.audit ? message.plaintTextEncrypted : message.plainTextMessage} + subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} + />} + +
  • )} +
    { this.messagesEnd = el; }}>
    +
    +
    + + + { + this.setState({ value: e.target.value }); + this.value = this.state.value; + }} + sx={{width: 214}} + /> + + + + + +
    + + + + + + + + + +
    + ) + } +} + +export default withTranslation()(Chat); diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 0ecc4a36..127d9cdc 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -3,7 +3,7 @@ import { withTranslation, Trans} from "react-i18next"; import { IconButton, Box, Link, Paper, Rating, Button, Tooltip, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material" import QRCode from "react-qr-code"; import Countdown, { zeroPad} from 'react-countdown'; -import Chat from "./Chat" +import Chat from "./EncryptedChat" import MediaQuery from 'react-responsive' import QrReader from 'react-qr-reader' diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 3ff2bbba..18dc2bda 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -13,7 +13,7 @@ import { RoboSatsNoTextIcon } from "./Icons"; import { sha256 } from 'js-sha256'; import { genBase62Token, tokenStrength } from "../utils/token"; -import { genKey , encryptMessage , decryptMessage} from "../utils/pgp"; +import { genKey } from "../utils/pgp"; import { getCookie, writeCookie } from "../utils/cookies"; @@ -122,18 +122,6 @@ class UserGenPage extends Component { tokenHasChanged: true, }); this.props.setAppState({copiedToken: true}) - - // Encryption decryption test - console.log(encryptMessage('Example text to encrypt!', - getCookie('pub_key').split('\\').join('\n'), - getCookie('enc_priv_key').split('\\').join('\n'), - getCookie('robot_token')) - .then((encryptedMessage)=> decryptMessage( - encryptedMessage, - getCookie('pub_key').split('\\').join('\n'), - getCookie('enc_priv_key').split('\\').join('\n'), - getCookie('robot_token')) - )) } handleChangeToken=(e)=>{ diff --git a/frontend/src/utils/pgp.js b/frontend/src/utils/pgp.js index 3d42894e..e75f6a71 100644 --- a/frontend/src/utils/pgp.js +++ b/frontend/src/utils/pgp.js @@ -15,17 +15,18 @@ export async function genKey(highEntropyToken) { }; // Encrypt and sign a message -export async function encryptMessage(plainMessage, publicKeyArmored, privateKeyArmored, passphrase) { +export async function encryptMessage(plaintextMessage, ownPublicKeyArmored, peerPublicKeyArmored, privateKeyArmored, passphrase) { - const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + const ownPublicKey = await openpgp.readKey({ armoredKey: ownPublicKeyArmored }); + const peerPublicKey = await openpgp.readKey({ armoredKey: peerPublicKeyArmored }); const privateKey = await openpgp.decryptKey({ privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), passphrase }); const encryptedMessage = await openpgp.encrypt({ - message: await openpgp.createMessage({ text: plainMessage }), // input as Message object, message must be string - encryptionKeys: publicKey, + message: await openpgp.createMessage({ text: plaintextMessage }), // input as Message object, message must be string + encryptionKeys: [ ownPublicKey, peerPublicKey ], signingKeys: privateKey // optional }); diff --git a/frontend/src/utils/saveFile.js b/frontend/src/utils/saveFile.js new file mode 100644 index 00000000..00eaddce --- /dev/null +++ b/frontend/src/utils/saveFile.js @@ -0,0 +1,22 @@ +/* function to save DATA as text from browser +* @param {String} file -- file name to save to +* @param {filename} data -- object to save +*/ + +export const saveAsTxt = (filename, dataObjToWrite) => { + const blob = new Blob([JSON.stringify(dataObjToWrite, null, 2)], { type: "text/plain;charset=utf8" }); + const link = document.createElement("a"); + + link.download = filename; + link.href = window.URL.createObjectURL(blob); + link.dataset.downloadurl = ["text/plain;charset=utf8", link.download, link.href].join(":"); + + const evt = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }); + + link.dispatchEvent(evt); + link.remove() +}; \ No newline at end of file