From ac0969baf6049cf52a2a8b7ed6351943d8d5a2e1 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Mon, 23 May 2022 04:21:01 -0700 Subject: [PATCH] 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 {