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 {