mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 02:46:28 +00:00
Convert new UserGen into POST. Fix misformed armored keys. Example encrypt/decrypt.
This commit is contained in:
parent
789f9fbdb1
commit
ac0969baf6
@ -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 . .
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
144
api/views.py
144
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)
|
||||
|
@ -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
|
||||
|
@ -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 ?
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle2" variant="subtitle2" color='primary'>
|
||||
<Typography variant="subtitle2" color='primary'>
|
||||
{this.state.found ? t("A robot avatar was found, welcome back!"):null}<br/>
|
||||
</Typography>
|
||||
</Grid>
|
||||
@ -179,7 +205,7 @@ class UserGenPage extends Component {
|
||||
<Grid container align="center">
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField sx={{maxWidth: 280}}
|
||||
error={this.state.bad_request}
|
||||
error={this.state.bad_request ? true : false}
|
||||
label={t("Store your token safely")}
|
||||
required={true}
|
||||
value={this.state.token}
|
||||
|
Loading…
Reference in New Issue
Block a user