From 59d8d325b2bad9ce1f965c440e804e1f987fe233 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sun, 22 May 2022 10:46:04 -0700 Subject: [PATCH 01/10] Add utility functions for keypair generation, encrypt and decrypt messages --- frontend/package-lock.json | 5 +++ frontend/package.json | 1 + frontend/src/utils/pgp.js | 66 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 frontend/src/utils/pgp.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 951b8a70..9c7253ad 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9248,6 +9248,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0e41091f..040d304a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,6 +52,7 @@ "i18next-browser-languagedetector": "^6.1.4", "i18next-http-backend": "^1.4.0", "i18next-xhr-backend": "^3.2.2", + "js-sha256": "^0.9.0", "material-ui-image": "^3.3.2", "openpgp": "^5.2.1", "react": "^18.0.0", diff --git a/frontend/src/utils/pgp.js b/frontend/src/utils/pgp.js new file mode 100644 index 00000000..889d7763 --- /dev/null +++ b/frontend/src/utils/pgp.js @@ -0,0 +1,66 @@ +import * as openpgp from 'openpgp/lightweight'; + +// Generate KeyPair. Private Key is encrypted with the highEntropyToken +export async function genKeys(highEntropyToken) { + + const keyPair = await openpgp.generateKey({ + type: 'ecc', // Type of the key, defaults to ECC + curve: 'curve25519', // ECC curve name, defaults to curve25519 + userIDs: [{name: 'RoboSats Avatar'}], + passphrase: highEntropyToken, + format: 'armored' + }) + + console.log(keyPair) + + const publicKeyArmored = keyPair.publicKey; + const privateKeyArmored = keyPair.privateKey; // encrypted private key + + return {publicKeyArmored: publicKeyArmored, privateKeyArmored: privateKeyArmored} +}; + +// Encrypt and sign a message +export async function encryptMessage(plainMessage, publicKeyArmored, privateKeyArmored, passphrase) { + + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + 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, + signingKeys: privateKey // optional + }); + + return encryptedMessage; // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----' +}; + +// Decrypt and check signature of a message +export async function decryptMessage(encryptedMessage, publicKeyArmored, privateKeyArmored, passphrase) { + + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const message = await openpgp.readMessage({ + armoredMessage: encryptedMessage // parse armored message + }); + const { data: decrypted, signatures } = await openpgp.decrypt({ + message, + verificationKeys: publicKey, // optional + decryptionKeys: privateKey + }); + + // check signature validity (signed messages only) + try { + await signatures[0].verified; // throws on invalid signature + console.log('Signature is valid'); + return {decryptedMessage: decrypted, validSignature: true} + } catch (e) { + return {decryptedMessage: decrypted, validSignature: false}; + } +}; \ No newline at end of file From 191dfe0d3b04a7d90ff3e09a8633b9ca76db5643 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sun, 22 May 2022 12:30:12 -0700 Subject: [PATCH 02/10] Upgrade userGen, only token_sha256 used. Deprecate token user generation. --- api/views.py | 98 ++++++++++++++++++++------ frontend/src/components/UserGenPage.js | 22 +++--- frontend/src/utils/token.js | 17 +++++ 3 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 frontend/src/utils/token.js diff --git a/api/views.py b/api/views.py index 7b0a05cc..c45aa518 100644 --- a/api/views.py +++ b/api/views.py @@ -551,26 +551,84 @@ class UserView(APIView): "bad_request"] = f"You are already logged in as {request.user} and have an active order" return Response(context, status.HTTP_400_BAD_REQUEST) - token = request.GET.get("token") + # Deprecated, kept temporarily for legacy reasons + token = request.GET.get("token") + + # The old way to generate a robot and login. Soon deprecated + # Only for login. No new users allowed. Only using API endpoint. + # Frontend does not support it anymore. + if token: + value, counts = np.unique(list(token), return_counts=True) + shannon_entropy = entropy(counts, base=62) + bits_entropy = log2(len(value)**len(token)) + + # Hash the token, only 1 iteration. + hash = hashlib.sha256(str.encode(token)).hexdigest() + + # Generate nickname deterministically + nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] + context["nickname"] = nickname + + # Payload + context = { + "token_shannon_entropy": shannon_entropy, + "token_bits_entropy": bits_entropy, + } + + # Do not generate a new user for the old method! Only allow login. + if len(User.objects.filter(username=nickname)) == 1: + user = authenticate(request, username=nickname, password=token) + if user is not None: + login(request, user) + # Sends the welcome back message, only if created +3 mins ago + if request.user.date_joined < (timezone.now() - + timedelta(minutes=3)): + context["found"] = "We found your Robot avatar. Welcome back!" + return Response(context, status=status.HTTP_202_ACCEPTED) + else: + # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) + context["found"] = "Bad luck, this nickname is taken" + context["bad_request"] = "Enter a different token" + return Response(context, status.HTTP_403_FORBIDDEN) + + elif len(User.objects.filter(username=nickname)) == 0: + context["bad_request"] = "User Generation with explicit token deprecated. Only token_sha256 allowed." + return Response(context, status.HTTP_400_BAD_REQUEST) + + # The new way. The token is never sent. Only its SHA256 + token_sha256 = request.GET.get("token_sha256") # New way to gen users and get credentials ref_code = request.GET.get("ref_code") - # Compute token entropy - value, counts = np.unique(list(token), return_counts=True) - shannon_entropy = entropy(counts, base=62) - bits_entropy = log2(len(value)**len(token)) - # Payload - context = { - "token_shannon_entropy": shannon_entropy, - "token_bits_entropy": bits_entropy, - } + # Now the server only receives a hash of the token. So server trusts the client + # with computing length, counts and unique_values to confirm the high entropy of the token + # In any case, it is up to the client if they want to create a bad high entropy token. - # Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity - if bits_entropy < 128 or shannon_entropy < 0.7: - context["bad_request"] = "The token does not have enough entropy" - return Response(context, status=status.HTTP_400_BAD_REQUEST) + # Supplying the pieces of info about the token to compute entropy is not mandatory + # If not supply, users can be created with garbage entropy token. Frontend will always supply. + try: + unique_values = int(request.GET.get("unique_values")) + counts = request.GET.get("counts").split(",") + counts = [int(x) for x in counts] + length = int(request.GET.get("length")) - # Hash the token, only 1 iteration. - hash = hashlib.sha256(str.encode(token)).hexdigest() + shannon_entropy = entropy(counts, base=62) + bits_entropy = log2(unique_values**length) + + # Payload + context = { + "token_shannon_entropy": shannon_entropy, + "token_bits_entropy": bits_entropy, + } + + # Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity + if bits_entropy < 128 or shannon_entropy < 0.7: + context["bad_request"] = "The token does not have enough entropy" + return Response(context, status=status.HTTP_400_BAD_REQUEST) + except: + pass + + # Hash the token_sha256, only 1 iteration. (this is the second SHA256 of the user token) + hash = hashlib.sha256(str.encode(token_sha256)).hexdigest() # Generate nickname deterministically nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] @@ -586,14 +644,12 @@ class UserView(APIView): with open(image_path, "wb") as f: rh.img.save(f, format="png") - - # Create new credentials and login if nickname is new if len(User.objects.filter(username=nickname)) == 0: User.objects.create_user(username=nickname, - password=token, + password=token_sha256, is_staff=False) - user = authenticate(request, username=nickname, password=token) + user = authenticate(request, username=nickname, password=token_sha256) login(request, user) context['referral_code'] = token_urlsafe(8) @@ -610,7 +666,7 @@ class UserView(APIView): return Response(context, status=status.HTTP_201_CREATED) else: - user = authenticate(request, username=nickname, password=token) + user = authenticate(request, username=nickname, password=token_sha256) if user is not None: login(request, user) # Sends the welcome back message, only if created +3 mins ago diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 4acfdd0b..bbdeb9c1 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -11,8 +11,11 @@ import ContentCopy from "@mui/icons-material/ContentCopy"; import BoltIcon from '@mui/icons-material/Bolt'; import { RoboSatsNoTextIcon } from "./Icons"; +import { sha256 } from 'js-sha256'; +import { genBase62Token, tokenStrength } from "../utils/token"; import { getCookie, writeCookie } from "../utils/cookies"; + class UserGenPage extends Component { constructor(props) { super(props); @@ -37,7 +40,7 @@ class UserGenPage extends Component { }); } else{ - var newToken = this.genBase62Token(36) + var newToken = genBase62Token(36) this.setState({ token: newToken }); @@ -45,19 +48,10 @@ class UserGenPage extends Component { } } - // sort of cryptographically strong function to generate Base62 token client-side - genBase62Token(length) - { - return window.btoa(Array.from( - window.crypto.getRandomValues( - new Uint8Array(length * 2))) - .map((b) => String.fromCharCode(b)) - .join("")).replace(/[+/]/g, "") - .substring(0, length); - } - getGeneratedUser=(token)=>{ - fetch('/api/user' + '?token=' + token + '&ref_code=' + this.refCode) + var strength = tokenStrength(token) + + fetch('/api/user' + '?token_sha256=' + sha256(token) + '&unique_values=' + strength.uniqueValues +'&counts=' + strength.counts +'&length=' + token.length + '&ref_code=' + this.refCode) .then((response) => response.json()) .then((data) => { this.setState({ @@ -97,7 +91,7 @@ class UserGenPage extends Component { } handleClickNewRandomToken=()=>{ - var token = this.genBase62Token(36); + var token = genBase62Token(36); this.setState({ token: token, tokenHasChanged: true, diff --git a/frontend/src/utils/token.js b/frontend/src/utils/token.js new file mode 100644 index 00000000..181f99c5 --- /dev/null +++ b/frontend/src/utils/token.js @@ -0,0 +1,17 @@ +// sort of cryptographically strong function to generate Base62 token client-side +export function genBase62Token(length){ + return window.btoa(Array.from( + window.crypto.getRandomValues( + new Uint8Array(length * 2))) + .map((b) => String.fromCharCode(b)) + .join("")).replace(/[+/]/g, "") + .substring(0, length); +} + +export function tokenStrength(token) { + const characters = token.split("").reduce(function(obj, s){ + obj[s] = (obj[s] || 0) + 1; + return obj; + }, {}); + return {uniqueValues:Object.keys(characters).length,counts:Object.values(characters)} +} \ No newline at end of file From 789f9fbdb14d6ca7cddfb0fb7c6589207866a24d Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sun, 22 May 2022 15:12:25 -0700 Subject: [PATCH 03/10] Add pub_key and enc_priv_key fields. Store in cookies. Bug: misformed --- api/models.py | 14 ++++++ api/views.py | 15 +++++- frontend/src/components/UserGenPage.js | 65 +++++++++++++++----------- frontend/src/utils/pgp.js | 9 +--- 4 files changed, 68 insertions(+), 35 deletions(-) diff --git a/api/models.py b/api/models.py index 7115ec9b..f2ec1abe 100644 --- a/api/models.py +++ b/api/models.py @@ -425,6 +425,20 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs): class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) + # PGP keys, used for E2E chat encrytion. Priv key is encrypted with user's passphrase (highEntropyToken) + public_key = models.CharField( + max_length=999, + null=True, + default=None, + blank=True, + ) + encrypted_private_key = models.CharField( + max_length=999, + null=True, + default=None, + blank=True, + ) + # Total trades total_contracts = models.PositiveIntegerField(null=False, default=0) diff --git a/api/views.py b/api/views.py index c45aa518..3a03a38d 100644 --- a/api/views.py +++ b/api/views.py @@ -597,7 +597,13 @@ class UserView(APIView): # The new way. The token is never sent. Only its SHA256 token_sha256 = request.GET.get("token_sha256") # New way to gen users and get credentials + public_key = request.GET.get("pub") + encrypted_private_key = request.GET.get("enc_priv") ref_code = request.GET.get("ref_code") + + if not public_key or not encrypted_private_key: + context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys" + return Response(context, status.HTTP_400_BAD_REQUEST) # Now the server only receives a hash of the token. So server trusts the client # with computing length, counts and unique_values to confirm the high entropy of the token @@ -655,14 +661,19 @@ class UserView(APIView): context['referral_code'] = token_urlsafe(8) user.profile.referral_code = context['referral_code'] user.profile.avatar = "static/assets/avatars/" + nickname + ".png" + user.profile.public_key = public_key + user.profile.encrypted_private_key = encrypted_private_key # If the ref_code was created by another robot, this robot was referred. queryset = Profile.objects.filter(referral_code=ref_code) if len(queryset) == 1: user.profile.is_referred = True user.profile.referred_by = queryset[0] - + user.profile.save() + + context["public_key"] = public_key + context["encrypted_private_key"] = encrypted_private_key return Response(context, status=status.HTTP_201_CREATED) else: @@ -673,6 +684,8 @@ class UserView(APIView): if request.user.date_joined < (timezone.now() - timedelta(minutes=3)): context["found"] = "We found your Robot avatar. Welcome back!" + context["public_key"] = request.user.profile.public_key + context["encrypted_private_key"] = request.user.profile.encrypted_private_key return Response(context, status=status.HTTP_202_ACCEPTED) else: # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index bbdeb9c1..a88b6400 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -13,6 +13,7 @@ import { RoboSatsNoTextIcon } from "./Icons"; import { sha256 } from 'js-sha256'; import { genBase62Token, tokenStrength } from "../utils/token"; +import { genKey } from "../utils/pgp"; import { getCookie, writeCookie } from "../utils/cookies"; @@ -49,36 +50,46 @@ class UserGenPage extends Component { } getGeneratedUser=(token)=>{ - var strength = tokenStrength(token) - fetch('/api/user' + '?token_sha256=' + sha256(token) + '&unique_values=' + strength.uniqueValues +'&counts=' + strength.counts +'&length=' + token.length + '&ref_code=' + this.refCode) - .then((response) => response.json()) - .then((data) => { - this.setState({ + var strength = tokenStrength(token); + + genKey(token).then((key) => + fetch('/api/user' + + '?token_sha256=' + sha256(token) + + '&pub=' + key.publicKeyArmored + + '&enc_priv=' + key.encryptedPrivateKeyArmored + + '&unique_values=' + strength.uniqueValues + + '&counts=' + strength.counts + + '&length=' + token.length + + '&ref_code=' + this.refCode) + .then((response) => response.json()) + .then((data) => { + this.setState({ + nickname: data.nickname, + bit_entropy: data.token_bits_entropy, + avatar_url: '/static/assets/avatars/' + data.nickname + '.png', + shannon_entropy: data.token_shannon_entropy, + bad_request: data.bad_request, + found: data.found, + loadingRobot:false, + }) + & + // Add nick and token to App state (token only if not a bad request) + (data.bad_request ? this.props.setAppState({ nickname: data.nickname, - bit_entropy: data.token_bits_entropy, - avatar_url: '/static/assets/avatars/' + data.nickname + '.png', - shannon_entropy: data.token_shannon_entropy, - bad_request: data.bad_request, - found: data.found, - loadingRobot:false, + avatarLoaded: false, + }) + : + (this.props.setAppState({ + nickname: data.nickname, + token: token, + avatarLoaded: false, + })) & writeCookie("robot_token",token) & writeCookie("pub_key",data.public_key) & writeCookie("enc_priv_key",data.encrypted_private_key)) + & + // If the robot has been found (recovered) we assume the token is backed up + (data.found ? this.props.setAppState({copiedToken:true}) : null) }) - & - // Add nick and token to App state (token only if not a bad request) - (data.bad_request ? this.props.setAppState({ - nickname: data.nickname, - avatarLoaded: false, - }) - : - (this.props.setAppState({ - nickname: data.nickname, - token: token, - avatarLoaded: false, - })) & writeCookie("robot_token",token)) - & - // If the robot has been found (recovered) we assume the token is backed up - (data.found ? this.props.setAppState({copiedToken:true}) : null) - }); + ); } delGeneratedUser() { diff --git a/frontend/src/utils/pgp.js b/frontend/src/utils/pgp.js index 889d7763..3d42894e 100644 --- a/frontend/src/utils/pgp.js +++ b/frontend/src/utils/pgp.js @@ -1,7 +1,7 @@ import * as openpgp from 'openpgp/lightweight'; // Generate KeyPair. Private Key is encrypted with the highEntropyToken -export async function genKeys(highEntropyToken) { +export async function genKey(highEntropyToken) { const keyPair = await openpgp.generateKey({ type: 'ecc', // Type of the key, defaults to ECC @@ -10,13 +10,8 @@ export async function genKeys(highEntropyToken) { passphrase: highEntropyToken, format: 'armored' }) - - console.log(keyPair) - - const publicKeyArmored = keyPair.publicKey; - const privateKeyArmored = keyPair.privateKey; // encrypted private key - return {publicKeyArmored: publicKeyArmored, privateKeyArmored: privateKeyArmored} + return {publicKeyArmored: keyPair.publicKey, encryptedPrivateKeyArmored: keyPair.privateKey} }; // Encrypt and sign a message From ac0969baf6049cf52a2a8b7ed6351943d8d5a2e1 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Mon, 23 May 2022 04:21:01 -0700 Subject: [PATCH 04/10] Convert new UserGen into POST. Fix misformed armored keys. Example encrypt/decrypt. --- Dockerfile | 1 + api/models.py | 4 +- api/serializers.py | 37 +++++++ api/views.py | 144 +++++++++++++++---------- docker-compose.yml | 1 + frontend/src/components/UserGenPage.js | 58 +++++++--- 6 files changed, 172 insertions(+), 73 deletions(-) diff --git a/Dockerfile b/Dockerfile index 52c08374..fe04d2a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN python -m pip install --upgrade pip COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt +RUN pip install git+git://github.com/django/django.git # copy current dir's content to container's WORKDIR root i.e. all the contents of the robosats app COPY . . diff --git a/api/models.py b/api/models.py index f2ec1abe..53b8b8f7 100644 --- a/api/models.py +++ b/api/models.py @@ -426,13 +426,13 @@ class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) # PGP keys, used for E2E chat encrytion. Priv key is encrypted with user's passphrase (highEntropyToken) - public_key = models.CharField( + public_key = models.TextField( max_length=999, null=True, default=None, blank=True, ) - encrypted_private_key = models.CharField( + encrypted_private_key = models.TextField( max_length=999, null=True, default=None, diff --git a/api/serializers.py b/api/serializers.py index 01c4b6a7..196d1986 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -80,6 +80,43 @@ class UpdateOrderSerializer(serializers.Serializer): ) amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None) +class UserGenSerializer(serializers.Serializer): + # Mandatory fields + token_sha256 = serializers.CharField(max_length=64, + allow_null=False, + allow_blank=False, + required=True, + help_text="SHA256 of user secret") + public_key = serializers.CharField(max_length=999, + allow_null=False, + allow_blank=False, + required=True, + help_text="Armored ASCII PGP public key block") + encrypted_private_key = serializers.CharField(max_length=999, + allow_null=False, + allow_blank=False, + required=True, + help_text="Armored ASCII PGP encrypted private key block") + + # Optional fields + ref_code = serializers.CharField(max_length=30, + allow_null=True, + allow_blank=True, + required=False, + default=None) + counts = serializers.ListField(child=serializers.IntegerField(), + allow_null=True, + required=False, + default=None) + length = serializers.IntegerField(allow_null=True, + default=None, + required=False, + min_value=1) + unique_values = serializers.IntegerField(allow_null=True, + default=None, + required=False, + min_value=1) + class ClaimRewardSerializer(serializers.Serializer): invoice = serializers.CharField(max_length=2000, allow_null=True, diff --git a/api/views.py b/api/views.py index 3a03a38d..e6420193 100644 --- a/api/views.py +++ b/api/views.py @@ -7,9 +7,11 @@ from rest_framework.views import APIView from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.models import User -from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer +from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer from api.models import LNPayment, MarketTick, Order, Currency, Profile from control.models import AccountingDay from api.logics import Logics @@ -520,7 +522,6 @@ class OrderView(viewsets.ViewSet): return self.get(request) - class UserView(APIView): NickGen = NickGenerator(lang="English", use_adv=False, @@ -528,9 +529,15 @@ class UserView(APIView): use_noun=True, max_num=999) - # Probably should be turned into a post method + serializer_class = UserGenSerializer + def get(self, request, format=None): """ + DEPRECATED + The old way to generate a robot and login. + Only for login. No new users allowed. Only available using API endpoint. + Frontend does not support it anymore. + Get a new user derived from a high entropy token - Request has a high-entropy token, @@ -538,7 +545,7 @@ class UserView(APIView): - Creates login credentials (new User object) Response with Avatar and Nickname. """ - + context = {} # If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him if request.user.is_authenticated: context = {"nickname": request.user.username} @@ -554,52 +561,79 @@ class UserView(APIView): # Deprecated, kept temporarily for legacy reasons token = request.GET.get("token") - # The old way to generate a robot and login. Soon deprecated - # Only for login. No new users allowed. Only using API endpoint. - # Frontend does not support it anymore. - if token: - value, counts = np.unique(list(token), return_counts=True) - shannon_entropy = entropy(counts, base=62) - bits_entropy = log2(len(value)**len(token)) + value, counts = np.unique(list(token), return_counts=True) + shannon_entropy = entropy(counts, base=62) + bits_entropy = log2(len(value)**len(token)) - # Hash the token, only 1 iteration. - hash = hashlib.sha256(str.encode(token)).hexdigest() + # Hash the token, only 1 iteration. + hash = hashlib.sha256(str.encode(token)).hexdigest() - # Generate nickname deterministically - nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] - context["nickname"] = nickname - - # Payload - context = { - "token_shannon_entropy": shannon_entropy, - "token_bits_entropy": bits_entropy, - } + # Generate nickname deterministically + nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] + context["nickname"] = nickname + + # Payload + context = { + "token_shannon_entropy": shannon_entropy, + "token_bits_entropy": bits_entropy, + } - # Do not generate a new user for the old method! Only allow login. - if len(User.objects.filter(username=nickname)) == 1: - user = authenticate(request, username=nickname, password=token) - if user is not None: - login(request, user) - # Sends the welcome back message, only if created +3 mins ago - if request.user.date_joined < (timezone.now() - - timedelta(minutes=3)): - context["found"] = "We found your Robot avatar. Welcome back!" - return Response(context, status=status.HTTP_202_ACCEPTED) - else: - # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) - context["found"] = "Bad luck, this nickname is taken" - context["bad_request"] = "Enter a different token" - return Response(context, status.HTTP_403_FORBIDDEN) + # Do not generate a new user for the old method! Only allow login. + if len(User.objects.filter(username=nickname)) == 1: + user = authenticate(request, username=nickname, password=token) + if user is not None: + login(request, user) + # Sends the welcome back message, only if created +3 mins ago + if request.user.date_joined < (timezone.now() - + timedelta(minutes=3)): + context["found"] = "We found your Robot avatar. Welcome back!" + return Response(context, status=status.HTTP_202_ACCEPTED) + else: + # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) + context["found"] = "Bad luck, this nickname is taken" + context["bad_request"] = "Enter a different token" + return Response(context, status.HTTP_403_FORBIDDEN) - elif len(User.objects.filter(username=nickname)) == 0: - context["bad_request"] = "User Generation with explicit token deprecated. Only token_sha256 allowed." + elif len(User.objects.filter(username=nickname)) == 0: + context["bad_request"] = "User Generation with explicit token deprecated. Only token_sha256 allowed." + return Response(context, status.HTTP_400_BAD_REQUEST) + + def post(self, request, format=None): + """ + Get a new user derived from a high entropy token + + - Request has a hash of a high-entropy token + - Request includes pubKey and encrypted privKey + - Generates new nickname and avatar. + - Creates login credentials (new User object) + + Response with Avatar, Nickname, pubKey, privKey. + """ + context = {} + serializer = self.serializer_class(data=request.data) + + # Return bad request if serializer is not valid + if not serializer.is_valid(): + context = {"bad_request": "Invalid serializer"} + return Response(context, status=status.HTTP_400_BAD_REQUEST) + + # If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him + if request.user.is_authenticated: + context = {"nickname": request.user.username} + not_participant, _, _ = Logics.validate_already_maker_or_taker( + request.user) + + # Does not allow this 'mistake' if an active order + if not not_participant: + context[ + "bad_request"] = f"You are already logged in as {request.user} and have an active order" return Response(context, status.HTTP_400_BAD_REQUEST) # The new way. The token is never sent. Only its SHA256 - token_sha256 = request.GET.get("token_sha256") # New way to gen users and get credentials - public_key = request.GET.get("pub") - encrypted_private_key = request.GET.get("enc_priv") - ref_code = request.GET.get("ref_code") + token_sha256 = serializer.data.get("token_sha256") + public_key = serializer.data.get("public_key") + encrypted_private_key = serializer.data.get("encrypted_private_key") + ref_code = serializer.data.get("ref_code") if not public_key or not encrypted_private_key: context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys" @@ -609,13 +643,12 @@ class UserView(APIView): # with computing length, counts and unique_values to confirm the high entropy of the token # In any case, it is up to the client if they want to create a bad high entropy token. - # Supplying the pieces of info about the token to compute entropy is not mandatory - # If not supply, users can be created with garbage entropy token. Frontend will always supply. + # Submitting the three params needed to compute token entropy is not mandatory + # If not submitted, avatars can be created with garbage entropy token. Frontend will always submit them. try: - unique_values = int(request.GET.get("unique_values")) - counts = request.GET.get("counts").split(",") - counts = [int(x) for x in counts] - length = int(request.GET.get("length")) + unique_values = serializer.data.get("unique_values") + counts = serializer.data.get("counts") + length = serializer.data.get("length") shannon_entropy = entropy(counts, base=62) bits_entropy = log2(unique_values**length) @@ -671,11 +704,12 @@ class UserView(APIView): user.profile.referred_by = queryset[0] user.profile.save() - - context["public_key"] = public_key - context["encrypted_private_key"] = encrypted_private_key + + context["public_key"] = user.profile.public_key + context["encrypted_private_key"] = user.profile.encrypted_private_key return Response(context, status=status.HTTP_201_CREATED) + # log in user and return pub/priv keys if existing else: user = authenticate(request, username=nickname, password=token_sha256) if user is not None: @@ -684,11 +718,11 @@ class UserView(APIView): if request.user.date_joined < (timezone.now() - timedelta(minutes=3)): context["found"] = "We found your Robot avatar. Welcome back!" - context["public_key"] = request.user.profile.public_key - context["encrypted_private_key"] = request.user.profile.encrypted_private_key + context["public_key"] = user.profile.public_key + context["encrypted_private_key"] = user.profile.encrypted_private_key return Response(context, status=status.HTTP_202_ACCEPTED) else: - # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) + # It is unlikely, but maybe the nickname is taken (1 in 20 Billion chance) context["found"] = "Bad luck, this nickname is taken" context["bad_request"] = "Enter a different token" return Response(context, status.HTTP_403_FORBIDDEN) diff --git a/docker-compose.yml b/docker-compose.yml index bcb3e10f..d15aa09e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,7 @@ services: - .:/usr/src/robosats - /mnt/development/lnd:/lnd network_mode: service:tor + command: python3 -u manage.py runserver 0.0.0.0:8000 frontend: build: ./frontend diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index a88b6400..3ff2bbba 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 } from "../utils/pgp"; +import { genKey , encryptMessage , decryptMessage} from "../utils/pgp"; import { getCookie, writeCookie } from "../utils/cookies"; @@ -51,19 +51,31 @@ class UserGenPage extends Component { getGeneratedUser=(token)=>{ - var strength = tokenStrength(token); + const strength = tokenStrength(token); + const refCode = this.refCode - genKey(token).then((key) => - fetch('/api/user' + - '?token_sha256=' + sha256(token) + - '&pub=' + key.publicKeyArmored + - '&enc_priv=' + key.encryptedPrivateKeyArmored + - '&unique_values=' + strength.uniqueValues + - '&counts=' + strength.counts + - '&length=' + token.length + - '&ref_code=' + this.refCode) + const requestOptions = genKey(token).then(function(key) { + return { + method: 'POST', + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, + body: JSON.stringify({ + token_sha256: sha256(token), + public_key: key.publicKeyArmored, + encrypted_private_key: key.encryptedPrivateKeyArmored, + unique_values: strength.uniqueValues, + counts: strength.counts, + length: token.length, + ref_code: refCode, + }) + }} + ); + + console.log(requestOptions) + + requestOptions.then((options) => + fetch("/api/user/",options) .then((response) => response.json()) - .then((data) => { + .then((data) => { console.log(data) & this.setState({ nickname: data.nickname, bit_entropy: data.token_bits_entropy, @@ -84,11 +96,13 @@ class UserGenPage extends Component { nickname: data.nickname, token: token, avatarLoaded: false, - })) & writeCookie("robot_token",token) & writeCookie("pub_key",data.public_key) & writeCookie("enc_priv_key",data.encrypted_private_key)) + })) & writeCookie("robot_token",token) + & writeCookie("pub_key",data.public_key.split('\n').join('\\')) + & writeCookie("enc_priv_key",data.encrypted_private_key.split('\n').join('\\'))) & // If the robot has been found (recovered) we assume the token is backed up (data.found ? this.props.setAppState({copiedToken:true}) : null) - }) + }) ); } @@ -108,6 +122,18 @@ 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)=>{ @@ -169,7 +195,7 @@ class UserGenPage extends Component { { this.state.found ? - + {this.state.found ? t("A robot avatar was found, welcome back!"):null}
@@ -179,7 +205,7 @@ class UserGenPage extends Component { Date: Mon, 23 May 2022 17:31:34 -0700 Subject: [PATCH 05/10] Create EncryptedChat, add saveKeys & saveMessages, add audit button. WIP --- chat/consumers.py | 3 + frontend/src/components/Chat.js | 4 +- frontend/src/components/EncryptedChat.js | 240 +++++++++++++++++++++++ frontend/src/components/TradeBox.js | 2 +- frontend/src/components/UserGenPage.js | 14 +- frontend/src/utils/pgp.js | 9 +- frontend/src/utils/saveFile.js | 22 +++ 7 files changed, 275 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/EncryptedChat.js create mode 100644 frontend/src/utils/saveFile.js 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 From af001d31d231f4263f85076d89226094e47acd9b Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 24 May 2022 05:16:50 -0700 Subject: [PATCH 06/10] Add Audit Dialog --- frontend/src/components/Dialogs/AuditPGP.tsx | 200 +++++++++++++++++++ frontend/src/components/Dialogs/index.ts | 1 + frontend/src/components/EncryptedChat.js | 77 ++++--- frontend/src/components/Icons/Export.tsx | 10 + frontend/src/components/Icons/index.ts | 2 + frontend/src/components/MakerPage.js | 2 +- frontend/src/utils/pgp.js | 2 +- frontend/src/utils/saveFile.js | 18 ++ 8 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/Dialogs/AuditPGP.tsx create mode 100644 frontend/src/components/Icons/Export.tsx diff --git a/frontend/src/components/Dialogs/AuditPGP.tsx b/frontend/src/components/Dialogs/AuditPGP.tsx new file mode 100644 index 00000000..51b43d9b --- /dev/null +++ b/frontend/src/components/Dialogs/AuditPGP.tsx @@ -0,0 +1,200 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { + Dialog, + DialogTitle, + Tooltip, + IconButton, + TextField, + DialogActions, + DialogContent, + DialogContentText, + Button, + Grid, + Link, +} from "@mui/material" + +import { saveAsJson, saveAsTxt } from "../../utils/saveFile"; + +// Icons +import KeyIcon from '@mui/icons-material/Key'; +import ContentCopy from "@mui/icons-material/ContentCopy"; +import ForumIcon from '@mui/icons-material/Forum'; +import { ExportIcon } from '../Icons'; + +type Props = { + open: boolean; + onClose: () => void; + orderId: number; + messages: array; + own_pub_key: string; + own_enc_priv_key: string; + peer_pub_key: string; + passphrase: string; + onClickBack: () => void; +} + +const AuditPGPDialog = ({ + open, + onClose, + orderId, + messages, + own_pub_key, + own_enc_priv_key, + peer_pub_key, + passphrase, + onClickBack, +}: Props): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + {t("This chat is PGP Encrypted")} + + + + + {t("Your communication is end-to-end encrypted with OpenPGP. You can verify the privacy of this chat using any third party tool based on the OpenPGP standard.")} + + + + + + + + + {t("Your public key")}} + value={own_pub_key} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(own_pub_key)}> + + + , + }} + /> + + + + {t("Peer public key")}} + value={peer_pub_key} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(peer_pub_key)}> + + + , + }} + /> + + + + {t("Your encrypted private key")}} + value={own_enc_priv_key} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(own_enc_priv_key)}> + + + , + }} + /> + + + + {t("Your private key passphrase (keep secure!)")}} + value={passphrase} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(passphrase)}> + + + , + }} + /> + + +
    + + + + + + {/* */} + + {/* */} + + + +
    +
    + + + + + +
    + ) +} + +export default AuditPGPDialog; diff --git a/frontend/src/components/Dialogs/index.ts b/frontend/src/components/Dialogs/index.ts index 518f5c9e..cad8b05c 100644 --- a/frontend/src/components/Dialogs/index.ts +++ b/frontend/src/components/Dialogs/index.ts @@ -1,3 +1,4 @@ +export { default as AuditPGPDialog } from "./AuditPGP"; export { default as CommunityDialog } from "./Community"; export { default as InfoDialog } from "./Info"; export { default as LearnDialog } from "./Learn"; diff --git a/frontend/src/components/EncryptedChat.js b/frontend/src/components/EncryptedChat.js index 59db5365..4b4e43e8 100644 --- a/frontend/src/components/EncryptedChat.js +++ b/frontend/src/components/EncryptedChat.js @@ -1,10 +1,19 @@ 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 {Button, 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 { getCookie } from "../utils/cookies"; -import { saveAsTxt } from "../utils/saveFile"; +import { saveAsJson, saveAsTxt } from "../utils/saveFile"; +import { AuditPGPDialog } from "./Dialogs" + +// Icons +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import KeyIcon from '@mui/icons-material/Key'; +import { ExportIcon } from './Icons'; class Chat extends Component { constructor(props) { @@ -65,7 +74,12 @@ class Chat extends Component { // 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!!") + 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 KEY PUBKEY RECEIVED!!") this.setState({peer_pub_key:dataFromServer.message}) } else @@ -85,6 +99,7 @@ class Chat extends Component { plainTextMessage: decryptedData.decryptedMessage, validSignature: decryptedData.validSignature, userNick: dataFromServer.user_nick, + showPGP: false, time: dataFromServer.time }], }) @@ -102,18 +117,6 @@ class Chat extends Component { 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() { @@ -140,6 +143,17 @@ class Chat extends Component { 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, + }) + } + render() { const { t } = this.props; return ( @@ -195,7 +209,7 @@ class Chat extends Component { } style={{backgroundColor: '#fafafa'}} title={message.userNick} - subheader={this.state.audit ? message.plaintTextEncrypted : message.plainTextMessage} + subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} />} @@ -223,15 +237,30 @@ class Chat extends Component {
    - - - - - - - - +
    + + 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})} + /> + + + + + + {/* */} + + {/* */} + + ) } diff --git a/frontend/src/components/Icons/Export.tsx b/frontend/src/components/Icons/Export.tsx new file mode 100644 index 00000000..038ee27f --- /dev/null +++ b/frontend/src/components/Icons/Export.tsx @@ -0,0 +1,10 @@ +import React, { Component } from "react"; +import { SvgIcon } from "@mui/material" + +export default function ExportIcon(props) { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Icons/index.ts b/frontend/src/components/Icons/index.ts index e63c964e..dce24d8f 100644 --- a/frontend/src/components/Icons/index.ts +++ b/frontend/src/components/Icons/index.ts @@ -12,3 +12,5 @@ export { default as RoboSatsTextIcon } from "./RoboSatsText"; export { default as SellSatsCheckedIcon } from "./SellSatsChecked"; export { default as SellSatsIcon } from "./SellSats"; export { default as SendReceiveIcon } from "./SendReceive"; +export { default as ExportIcon } from "./Export"; + diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index d1c4c64a..8adb9a2f 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -577,7 +577,7 @@ class MakerPage extends Component { - + }> {t("Expiry Timers")} diff --git a/frontend/src/utils/pgp.js b/frontend/src/utils/pgp.js index e75f6a71..5e2a01dc 100644 --- a/frontend/src/utils/pgp.js +++ b/frontend/src/utils/pgp.js @@ -6,7 +6,7 @@ export async function genKey(highEntropyToken) { const keyPair = await openpgp.generateKey({ type: 'ecc', // Type of the key, defaults to ECC curve: 'curve25519', // ECC curve name, defaults to curve25519 - userIDs: [{name: 'RoboSats Avatar'}], + userIDs: [{name: 'RoboSats Avatar ID'+ parseInt(Math.random() * 1000000)}], //Just for identification. Ideally it would be the avatar nickname, but the nickname is generated only after submission passphrase: highEntropyToken, format: 'armored' }) diff --git a/frontend/src/utils/saveFile.js b/frontend/src/utils/saveFile.js index 00eaddce..02423f37 100644 --- a/frontend/src/utils/saveFile.js +++ b/frontend/src/utils/saveFile.js @@ -17,6 +17,24 @@ export const saveAsTxt = (filename, dataObjToWrite) => { cancelable: true, }); + link.dispatchEvent(evt); + link.remove() +}; + +export const saveAsJson = (filename, dataObjToWrite) => { + const blob = new Blob([JSON.stringify(dataObjToWrite, null, 2)], { type: "text/json" }); + const link = document.createElement("a"); + + link.download = filename; + link.href = window.URL.createObjectURL(blob); + link.dataset.downloadurl = ["text/json", 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 From d492475eec5865a0013fc3c745541ce6fb612a66 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 24 May 2022 05:52:33 -0700 Subject: [PATCH 07/10] Add show signature verification on chat messages --- frontend/src/components/Dialogs/AuditPGP.tsx | 4 +-- frontend/src/components/EncryptedChat.js | 27 +++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/Dialogs/AuditPGP.tsx b/frontend/src/components/Dialogs/AuditPGP.tsx index 51b43d9b..6c7a3448 100644 --- a/frontend/src/components/Dialogs/AuditPGP.tsx +++ b/frontend/src/components/Dialogs/AuditPGP.tsx @@ -158,7 +158,7 @@ const AuditPGPDialog = ({
    - {t("Export Keys")} + {t("Keys")}
    @@ -177,7 +177,7 @@ const AuditPGPDialog = ({
    - {t("Export Chat")} + {t("Messages")}
    diff --git a/frontend/src/components/EncryptedChat.js b/frontend/src/components/EncryptedChat.js index 4b4e43e8..1c139c44 100644 --- a/frontend/src/components/EncryptedChat.js +++ b/frontend/src/components/EncryptedChat.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { withTranslation } from "react-i18next"; -import {Button, Badge, ToolTip, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, Typography} from "@mui/material"; +import {Button, 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 { getCookie } from "../utils/cookies"; @@ -193,7 +193,7 @@ class Chat extends Component { } style={{backgroundColor: '#eeeeee'}} - title={message.userNick} + title={
    {message.userNick}{message.validSignature ? : }
    } subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} /> @@ -208,7 +208,17 @@ class Chat extends Component { } style={{backgroundColor: '#fafafa'}} - title={message.userNick} + title={ + +
    + {message.userNick} + {message.validSignature ? + + : + + } +
    +
    } subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} />} @@ -250,14 +260,17 @@ class Chat extends Component { passphrase={this.state.token} onClickBack={() => this.setState({audit:false})} /> + - + + + - {/* */} - - {/* */} + + +
    From 83564df25a03f8b26403ced54061aa3991818bb4 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 24 May 2022 06:33:55 -0700 Subject: [PATCH 08/10] Add tooltips and helper buttons --- frontend/src/components/Dialogs/AuditPGP.tsx | 188 ++++++++++--------- frontend/src/components/EncryptedChat.js | 51 +++-- frontend/src/utils/saveFile.js | 18 -- 3 files changed, 136 insertions(+), 121 deletions(-) diff --git a/frontend/src/components/Dialogs/AuditPGP.tsx b/frontend/src/components/Dialogs/AuditPGP.tsx index 6c7a3448..38e82bfe 100644 --- a/frontend/src/components/Dialogs/AuditPGP.tsx +++ b/frontend/src/components/Dialogs/AuditPGP.tsx @@ -14,7 +14,7 @@ import { Link, } from "@mui/material" -import { saveAsJson, saveAsTxt } from "../../utils/saveFile"; +import { saveAsJson } from "../../utils/saveFile"; // Icons import KeyIcon from '@mui/icons-material/Key'; @@ -53,120 +53,130 @@ const AuditPGPDialog = ({ onClose={onClose} > - {t("This chat is PGP Encrypted")} + {t("Do not trust, verify!")} - {t("Your communication is end-to-end encrypted with OpenPGP. You can verify the privacy of this chat using any third party tool based on the OpenPGP standard.")} + {t("Your communication is end-to-end encrypted with OpenPGP. You can verify the privacy of this chat using any tool based on the OpenPGP standard.")} - + - {t("Your public key")}} - value={own_pub_key} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(own_pub_key)}> - - - , - }} - /> + + {t("Your public key")}} + value={own_pub_key} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(own_pub_key)}> + + + , + }} + /> + - {t("Peer public key")}} - value={peer_pub_key} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(peer_pub_key)}> - - - , - }} - /> + + {t("Peer public key")}} + value={peer_pub_key} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(peer_pub_key)}> + + + , + }} + /> + - {t("Your encrypted private key")}} - value={own_enc_priv_key} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(own_enc_priv_key)}> - - - , - }} - /> + + {t("Your encrypted private key")}} + value={own_enc_priv_key} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(own_enc_priv_key)}> + + + , + }} + /> + - {t("Your private key passphrase (keep secure!)")}} - value={passphrase} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(passphrase)}> - - - , - }} - /> + + {t("Your private key passphrase (keep secure!)")}} + value={passphrase} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(passphrase)}> + + + , + }} + /> +
    - + + + - {/* */} +
    - {/* */} +
    diff --git a/frontend/src/components/EncryptedChat.js b/frontend/src/components/EncryptedChat.js index 1c139c44..91389ab9 100644 --- a/frontend/src/components/EncryptedChat.js +++ b/frontend/src/components/EncryptedChat.js @@ -1,16 +1,16 @@ import React, { Component } from 'react'; import { withTranslation } from "react-i18next"; -import {Button, Badge, Tooltip, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, Typography} from "@mui/material"; +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 { getCookie } from "../utils/cookies"; -import { saveAsJson, saveAsTxt } from "../utils/saveFile"; +import { saveAsJson } from "../utils/saveFile"; import { AuditPGPDialog } from "./Dialogs" // Icons import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; -import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import ContentCopy from "@mui/icons-material/ContentCopy"; import VisibilityIcon from '@mui/icons-material/Visibility'; import KeyIcon from '@mui/icons-material/Key'; import { ExportIcon } from './Icons'; @@ -193,7 +193,24 @@ class Chat extends Component { } style={{backgroundColor: '#eeeeee'}} - title={
    {message.userNick}{message.validSignature ? : }
    } + title={ + +
    + {message.userNick} + {message.validSignature ? + + : + + } +
    + +
    +
    + +
    +
    +
    + } subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} /> @@ -209,15 +226,21 @@ class Chat extends Component { } style={{backgroundColor: '#fafafa'}} title={ - -
    - {message.userNick} - {message.validSignature ? - - : - - } -
    + +
    + {message.userNick} + {message.validSignature ? + + : + + } +
    + +
    +
    + +
    +
    } subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} @@ -268,7 +291,7 @@ class Chat extends Component { - + diff --git a/frontend/src/utils/saveFile.js b/frontend/src/utils/saveFile.js index 02423f37..40f76763 100644 --- a/frontend/src/utils/saveFile.js +++ b/frontend/src/utils/saveFile.js @@ -3,24 +3,6 @@ * @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() -}; - export const saveAsJson = (filename, dataObjToWrite) => { const blob = new Blob([JSON.stringify(dataObjToWrite, null, 2)], { type: "text/json" }); const link = document.createElement("a"); From 508151eae54afe86b5e5d2fdbb6f1536c8c39c1d Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 24 May 2022 14:36:21 -0700 Subject: [PATCH 09/10] Add showPGP per message, copy per message, create messageCard --- chat/consumers.py | 1 - frontend/src/components/Dialogs/AuditPGP.tsx | 6 +- frontend/src/components/EncryptedChat.js | 235 +++++++++++++------ frontend/src/locales/en.json | 2 +- 4 files changed, 171 insertions(+), 73 deletions(-) diff --git a/chat/consumers.py b/chat/consumers.py index 37647a30..0e462d3b 100644 --- a/chat/consumers.py +++ b/chat/consumers.py @@ -110,7 +110,6 @@ 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() diff --git a/frontend/src/components/Dialogs/AuditPGP.tsx b/frontend/src/components/Dialogs/AuditPGP.tsx index 38e82bfe..75f7a721 100644 --- a/frontend/src/components/Dialogs/AuditPGP.tsx +++ b/frontend/src/components/Dialogs/AuditPGP.tsx @@ -20,7 +20,7 @@ import { saveAsJson } from "../../utils/saveFile"; import KeyIcon from '@mui/icons-material/Key'; import ContentCopy from "@mui/icons-material/ContentCopy"; import ForumIcon from '@mui/icons-material/Forum'; -import { ExportIcon } from '../Icons'; +import { ExportIcon, NewTabIcon } from '../Icons'; type Props = { open: boolean; @@ -53,7 +53,7 @@ const AuditPGPDialog = ({ onClose={onClose} > - {t("Do not trust, verify!")} + {t("Don't trust, verify")} @@ -63,7 +63,7 @@ const AuditPGPDialog = ({ - + diff --git a/frontend/src/components/EncryptedChat.js b/frontend/src/components/EncryptedChat.js index 91389ab9..3905a274 100644 --- a/frontend/src/components/EncryptedChat.js +++ b/frontend/src/components/EncryptedChat.js @@ -12,6 +12,7 @@ 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'; @@ -30,10 +31,13 @@ class Chat extends Component { connected: false, peer_connected: false, audit: false, + showPGP: new Array, + waitingEcho: false, + lastSent: '---BLANK---', }; rws = new ReconnectingWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.orderId + '/'); - + componentDidMount() { this.rws.addEventListener('open', () => { console.log('Connected!'); @@ -93,6 +97,8 @@ class Chat extends Component { .then((decryptedData) => this.setState((state) => ({ + waitingEcho: this.state.waitingEcho == true ? (decryptedData.decryptedMessage == this.state.lastSent ? false: true ) : false, + lastSent: decryptedData.decryptedMessage == this.state.lastSent ? '----BLANK----': this.state.lastSent, messages: [...state.messages, { encryptedMessage: dataFromServer.message.split('\\').join('\n'), @@ -129,6 +135,7 @@ class Chat extends Component { onButtonClicked = (e) => { if(this.state.value!=''){ + this.setState({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) & @@ -137,7 +144,7 @@ class Chat extends Component { message: encryptedMessage.split('\n').join('\\'), nick: this.props.ur_nick, }) - ) & this.setState({value: ""}) + ) & this.setState({value: "", waitingEcho: false}) ); } e.preventDefault(); @@ -154,6 +161,60 @@ class Chat extends Component { }) } + 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}; + })}> + + +
    +
    + + navigator.clipboard.writeText(this.state.showPGP[props.index] ? props.message.encryptedMessage : props.message.plainTextMessage)}> + + + +
    +
    +
    + } + subheader={this.state.showPGP[props.index] ? 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 ( @@ -162,7 +223,7 @@ class Chat extends Component { - + {t("You")+": "}{this.state.connected ? t("connected"): t("disconnected")} @@ -170,7 +231,7 @@ class Chat extends Component { - + {t("Peer")+": "}{this.state.peer_connected ? t("connected"): t("disconnected")} @@ -180,72 +241,101 @@ class Chat extends Component { {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} - {message.validSignature ? - - : - - } -
    - -
    -
    - -
    -
    -
    - } - subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} - subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} - /> + : - - - - } - style={{backgroundColor: '#fafafa'}} - title={ - -
    - {message.userNick} - {message.validSignature ? - - : - - } -
    - -
    -
    - -
    -
    -
    } - subheader={this.state.audit ? message.encryptedMessage : message.plainTextMessage} - subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}} - />} -
    + + } + {/* // + // + // + // } + // style={{backgroundColor: '#eeeeee'}} + // title={ + // + //
    + //
    + // {message.userNick} + // {message.validSignature ? + // + // : + // + // } + //
    + //
    + // + // this.setState(prevState => { + // const newShowPGP = [...prevState.showPGP]; + // newShowPGP[index] = !newShowPGP[index]; + // return {showPGP: newShowPGP}; + // })}> + // + // + //
    + //
    + // + // navigator.clipboard.writeText(this.state.showPGP[index] ? message.encryptedMessage : message.plainTextMessage)}> + // + // + // + //
    + //
    + //
    + // } + // subheader={this.state.showPGP[index] ? message.encryptedMessage : message.plainTextMessage} + // subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444', fontSize: this.state.showPGP[index]? 11 : null }}} + // /> + // : + // + // + // + // } + // style={{backgroundColor: '#fafafa'}} + // title={ + // + //
    + //
    + // {message.userNick} + // {message.validSignature ? + // + // : + // + // } + //
    + //
    + // + // this.setState(prevState => { + // const newShowPGP = [...prevState.showPGP]; + // newShowPGP[index] = !newShowPGP[index]; + // return {showPGP: newShowPGP}; + // })}> + // + // + //
    + //
    + // + // navigator.clipboard.writeText(this.state.showPGP[index] ? message.encryptedMessage : message.plainTextMessage)}> + // + // + // + //
    + //
    + //
    */}
  • )}
    { this.messagesEnd = el; }}>
    @@ -266,7 +356,16 @@ class Chat extends Component { />
    - +
    diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 8191f3a8..2b430330 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -381,7 +381,7 @@ "You can also check the full guide in ":"You can also check the full guide in ", "How to use":"How to use", "What payment methods are accepted?":"What payment methods are accepted?", - "All of them as long as they are fast. You can write down your preferred payment method(s). You will have to match with a peer who also accepts that method. The step to exchange fiat has a expiry time of 24 hours before a dispute is automatically open. We highly recommend using instant fiat payment rails.":"All of them as long as they are fast. You can write down your preferred payment method(s). You will have to match with a peer who also accepts that method. The step to exchange fiat has a expiry time of 24 hours before a dispute is automatically open. We highly recommend using instant fiat payment rails.", + "All of them as long as they are fast. You can write down your preferred payment method(s). You will have to match with a peer who also accepts that method. The step to exchange fiat has a expiry time of 24 hours before a dispute is automatically open. We highly recommend using instant fiat payment rails.":"All of them as long as they are fast. You can write down your preferred payment method(s). You will have to match with a peer who also accepts that method. The step to exchange fiat has an expiry time of 24 hours before a dispute is automatically open. We highly recommend using instant fiat payment rails.", "Are there trade limits?":"Are there trade limits?", "Maximum single trade size is {{maxAmount}} Satoshis to minimize lightning routing failure. There is no limits to the number of trades per day. A robot can only have one order at a time. However, you can use multiple robots simultaneously in different browsers (remember to back up your robot tokens!).":"Maximum single trade size is {{maxAmount}} Satoshis to minimize lightning routing failure. There is no limits to the number of trades per day. A robot can only have one order at a time. However, you can use multiple robots simultaneously in different browsers (remember to back up your robot tokens!).", "Is RoboSats private?":"Is RoboSats private?", From 185909c50d25a6ff486cdbf2f7536973dd9e1b9e Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 24 May 2022 15:28:36 -0700 Subject: [PATCH 10/10] Localize encrypted chat --- frontend/src/components/Dialogs/AuditPGP.tsx | 125 +++++++------------ frontend/src/components/EncryptedChat.js | 92 +------------- frontend/src/locales/en.json | 29 ++++- frontend/src/locales/es.json | 22 +++- 4 files changed, 91 insertions(+), 177 deletions(-) diff --git a/frontend/src/components/Dialogs/AuditPGP.tsx b/frontend/src/components/Dialogs/AuditPGP.tsx index 75f7a721..3c5cf31c 100644 --- a/frontend/src/components/Dialogs/AuditPGP.tsx +++ b/frontend/src/components/Dialogs/AuditPGP.tsx @@ -22,6 +22,31 @@ import ContentCopy from "@mui/icons-material/ContentCopy"; import ForumIcon from '@mui/icons-material/Forum'; import { ExportIcon, NewTabIcon } from '../Icons'; +function CredentialTextfield(props){ + return( + + + {props.label}} + value={props.value} + variant='filled' + size='small' + InputProps={{ + endAdornment: + + navigator.clipboard.writeText(props.value)}> + + + , + }} + /> + + + ) +} + type Props = { open: boolean; onClose: () => void; @@ -66,89 +91,29 @@ const AuditPGPDialog = ({
    - - - {t("Your public key")}} - value={own_pub_key} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(own_pub_key)}> - - - , - }} - /> - - + - - - {t("Peer public key")}} - value={peer_pub_key} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(peer_pub_key)}> - - - , - }} - /> - - + - - - {t("Your encrypted private key")}} - value={own_enc_priv_key} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(own_enc_priv_key)}> - - - , - }} - /> - - + - - - {t("Your private key passphrase (keep secure!)")}} - value={passphrase} - variant='filled' - size='small' - InputProps={{ - endAdornment: - - navigator.clipboard.writeText(passphrase)}> - - - , - }} - /> - - +
    diff --git a/frontend/src/components/EncryptedChat.js b/frontend/src/components/EncryptedChat.js index 3905a274..c272a37b 100644 --- a/frontend/src/components/EncryptedChat.js +++ b/frontend/src/components/EncryptedChat.js @@ -241,102 +241,12 @@ class Chat extends Component { {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} - // {message.validSignature ? - // - // : - // - // } - //
    - //
    - // - // this.setState(prevState => { - // const newShowPGP = [...prevState.showPGP]; - // newShowPGP[index] = !newShowPGP[index]; - // return {showPGP: newShowPGP}; - // })}> - // - // - //
    - //
    - // - // navigator.clipboard.writeText(this.state.showPGP[index] ? message.encryptedMessage : message.plainTextMessage)}> - // - // - // - //
    - //
    - //
    - // } - // subheader={this.state.showPGP[index] ? message.encryptedMessage : message.plainTextMessage} - // subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444', fontSize: this.state.showPGP[index]? 11 : null }}} - // /> - // : - // - // - // - // } - // style={{backgroundColor: '#fafafa'}} - // title={ - // - //
    - //
    - // {message.userNick} - // {message.validSignature ? - // - // : - // - // } - //
    - //
    - // - // this.setState(prevState => { - // const newShowPGP = [...prevState.showPGP]; - // newShowPGP[index] = !newShowPGP[index]; - // return {showPGP: newShowPGP}; - // })}> - // - // - //
    - //
    - // - // navigator.clipboard.writeText(this.state.showPGP[index] ? message.encryptedMessage : message.plainTextMessage)}> - // - // - // - //
    - //
    - //
    */} -
  • )} + )}
    { this.messagesEnd = el; }}>
    diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 2b430330..bde40124 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -259,9 +259,32 @@ "Type a message":"Type a message", "Connecting...":"Connecting...", "Send":"Send", - "The chat has no memory: if you leave, messages are lost.":"The chat has no memory: if you leave, messages are lost.", - "Learn easy PGP encryption.":"Learn easy PGP encryption.", - "PGP_guide_url":"https://learn.robosats.com/docs/pgp-encryption/", + "Verify your privacy":"Verify your privacy", + "Audit PGP":"Audit PGP", + "Save full log as a JSON file (messages and credentials)":"Save full log as a JSON file (messages and credentials)", + "Export":"Export", + "Don't trust, verify":"Don't trust, verify", + "Your communication is end-to-end encrypted with OpenPGP. You can verify the privacy of this chat using any tool based on the OpenPGP standard.":"Your communication is end-to-end encrypted with OpenPGP. You can verify the privacy of this chat using any tool based on the OpenPGP standard.", + "Learn how to verify":"Learn how to verify", + "Your PGP public key. Your peer uses it to encrypt messages only you can read.":"Your PGP public key. Your peer uses it to encrypt messages only you can read.", + "Your public key":"Your public key", + "Your peer PGP public key. You use it to encrypt messages only he can read and to verify your peer signed the incoming messages.":"Your peer PGP public key. You use it to encrypt messages only he can read.and to verify your peer signed the incoming messages.", + "Peer public key":"Peer public key", + "Your encrypted private key. You use it to decrypt the messages that your peer encrypted for you. You also use it to sign the messages you send.":"Your encrypted private key. You use it to decrypt the messages that your peer encrypted for you. You also use it to sign the messages you send.", + "Your encrypted private key":"Your encrypted private key", + "The passphrase to decrypt your private key. Only you know it! Do not share. It is also your robot token.":"The passphrase to decrypt your private key. Only you know it! Do not share. It is also your robot avatar user token.", + "Your private key passphrase (keep secure!)":"Your private key passphrase (keep secure!)", + "Save credentials as a JSON file":"Save credentials as a JSON file", + "Keys":"Keys", + "Save messages as a JSON file":"Save messages as a JSON file", + "Messages":"Messages", + + + + + + + "CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline", "Contract Box":"Contract Box", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 15b53471..26bcb836 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -258,9 +258,25 @@ "Type a message": "Escribe un mensaje", "Connecting...": "Conectando...", "Send": "Enviar", - "The chat has no memory: if you leave, messages are lost.": "Chat sin memoria: si lo cierras, los mensajes se pierden.", - "Learn easy PGP encryption.": "Aprende encriptación PGP.", - "PGP_guide_url":"https://learn.robosats.com/docs/pgp-encryption/es", + "Verify your privacy":"Verifica tu privacidad", + "Audit PGP":"Auditar", + "Save full log as a JSON file (messages and credentials)":"Guardar el log completo como JSON (credenciales y mensajes)", + "Export":"Exporta", + "Don't trust, verify":"No confíes, verifica", + "Your communication is end-to-end encrypted with OpenPGP. You can verify the privacy of this chat using any tool based on the OpenPGP standard.":"Tu comunicación se encripta de punta-a-punta con OpenPGP. Puedes verificar la privacida de este chat con cualquier herramienta de tercero basada en el estandar PGP.", + "Learn how to verify":"Aprende a verificar", + "Your PGP public key. Your peer uses it to encrypt messages only you can read.":"Esta es tu llave pública PGP. Tu contraparte la usa para encriptar mensajes que sólo tú puedes leer.", + "Your public key":"Tu llave pública", + "Your peer PGP public key. You use it to encrypt messages only he can read and to verify your peer signed the incoming messages.":"La llave pública PGP de tu contraparte. La usas para encriptar mensajes que solo él puede leer y verificar que es él quíen firmó los mensajes que recibes.", + "Peer public key":"Llave pública de tu contraparte", + "Your encrypted private key. You use it to decrypt the messages that your peer encrypted for you. You also use it to sign the messages you send.":"Tu llave privada PGP encriptada. La usas para desencriptar los mensajes que tu contraparte te envia. También la usas para firmar los mensajes que le envias.", + "Your encrypted private key":"Tu llave privada encriptada", + "The passphrase to decrypt your private key. Only you know it! Do not share. It is also your robot token.":"La contraseña para desencriptar tu llave privada. ¡Solo tú la sabes! Mantenla en secreto. También es el token de tu robot.", + "Your private key passphrase (keep secure!)":"La contraseña de tu llave privada ¡Mantener segura!", + "Save credentials as a JSON file":"Guardar credenciales como achivo JSON", + "Keys":"Llaves", + "Save messages as a JSON file":"Guardar mensajes como archivo JSON", + "Messages":"Mensajes", "CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline", "Contract Box": "Contrato",