mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-05 22:10:10 +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 ./
|
COPY requirements.txt ./
|
||||||
RUN pip install --no-cache-dir -r 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 current dir's content to container's WORKDIR root i.e. all the contents of the robosats app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
@ -426,13 +426,13 @@ class Profile(models.Model):
|
|||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
# PGP keys, used for E2E chat encrytion. Priv key is encrypted with user's passphrase (highEntropyToken)
|
# 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,
|
max_length=999,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
encrypted_private_key = models.CharField(
|
encrypted_private_key = models.TextField(
|
||||||
max_length=999,
|
max_length=999,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
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)
|
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):
|
class ClaimRewardSerializer(serializers.Serializer):
|
||||||
invoice = serializers.CharField(max_length=2000,
|
invoice = serializers.CharField(max_length=2000,
|
||||||
allow_null=True,
|
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 rest_framework.response import Response
|
||||||
|
|
||||||
from django.contrib.auth import authenticate, login, logout
|
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 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 api.models import LNPayment, MarketTick, Order, Currency, Profile
|
||||||
from control.models import AccountingDay
|
from control.models import AccountingDay
|
||||||
from api.logics import Logics
|
from api.logics import Logics
|
||||||
@ -520,7 +522,6 @@ class OrderView(viewsets.ViewSet):
|
|||||||
|
|
||||||
return self.get(request)
|
return self.get(request)
|
||||||
|
|
||||||
|
|
||||||
class UserView(APIView):
|
class UserView(APIView):
|
||||||
NickGen = NickGenerator(lang="English",
|
NickGen = NickGenerator(lang="English",
|
||||||
use_adv=False,
|
use_adv=False,
|
||||||
@ -528,9 +529,15 @@ class UserView(APIView):
|
|||||||
use_noun=True,
|
use_noun=True,
|
||||||
max_num=999)
|
max_num=999)
|
||||||
|
|
||||||
# Probably should be turned into a post method
|
serializer_class = UserGenSerializer
|
||||||
|
|
||||||
def get(self, request, format=None):
|
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
|
Get a new user derived from a high entropy token
|
||||||
|
|
||||||
- Request has a high-entropy token,
|
- Request has a high-entropy token,
|
||||||
@ -538,7 +545,7 @@ class UserView(APIView):
|
|||||||
- Creates login credentials (new User object)
|
- Creates login credentials (new User object)
|
||||||
Response with Avatar and Nickname.
|
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 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:
|
if request.user.is_authenticated:
|
||||||
context = {"nickname": request.user.username}
|
context = {"nickname": request.user.username}
|
||||||
@ -554,52 +561,79 @@ class UserView(APIView):
|
|||||||
# Deprecated, kept temporarily for legacy reasons
|
# Deprecated, kept temporarily for legacy reasons
|
||||||
token = request.GET.get("token")
|
token = request.GET.get("token")
|
||||||
|
|
||||||
# The old way to generate a robot and login. Soon deprecated
|
value, counts = np.unique(list(token), return_counts=True)
|
||||||
# Only for login. No new users allowed. Only using API endpoint.
|
shannon_entropy = entropy(counts, base=62)
|
||||||
# Frontend does not support it anymore.
|
bits_entropy = log2(len(value)**len(token))
|
||||||
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 the token, only 1 iteration.
|
||||||
hash = hashlib.sha256(str.encode(token)).hexdigest()
|
hash = hashlib.sha256(str.encode(token)).hexdigest()
|
||||||
|
|
||||||
# Generate nickname deterministically
|
# Generate nickname deterministically
|
||||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||||
context["nickname"] = nickname
|
context["nickname"] = nickname
|
||||||
|
|
||||||
# Payload
|
# Payload
|
||||||
context = {
|
context = {
|
||||||
"token_shannon_entropy": shannon_entropy,
|
"token_shannon_entropy": shannon_entropy,
|
||||||
"token_bits_entropy": bits_entropy,
|
"token_bits_entropy": bits_entropy,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Do not generate a new user for the old method! Only allow login.
|
# Do not generate a new user for the old method! Only allow login.
|
||||||
if len(User.objects.filter(username=nickname)) == 1:
|
if len(User.objects.filter(username=nickname)) == 1:
|
||||||
user = authenticate(request, username=nickname, password=token)
|
user = authenticate(request, username=nickname, password=token)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
login(request, user)
|
login(request, user)
|
||||||
# Sends the welcome back message, only if created +3 mins ago
|
# Sends the welcome back message, only if created +3 mins ago
|
||||||
if request.user.date_joined < (timezone.now() -
|
if request.user.date_joined < (timezone.now() -
|
||||||
timedelta(minutes=3)):
|
timedelta(minutes=3)):
|
||||||
context["found"] = "We found your Robot avatar. Welcome back!"
|
context["found"] = "We found your Robot avatar. Welcome back!"
|
||||||
return Response(context, status=status.HTTP_202_ACCEPTED)
|
return Response(context, status=status.HTTP_202_ACCEPTED)
|
||||||
else:
|
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 change)
|
||||||
context["found"] = "Bad luck, this nickname is taken"
|
context["found"] = "Bad luck, this nickname is taken"
|
||||||
context["bad_request"] = "Enter a different token"
|
context["bad_request"] = "Enter a different token"
|
||||||
return Response(context, status.HTTP_403_FORBIDDEN)
|
return Response(context, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
elif len(User.objects.filter(username=nickname)) == 0:
|
elif len(User.objects.filter(username=nickname)) == 0:
|
||||||
context["bad_request"] = "User Generation with explicit token deprecated. Only token_sha256 allowed."
|
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)
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# The new way. The token is never sent. Only its SHA256
|
# 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
|
token_sha256 = serializer.data.get("token_sha256")
|
||||||
public_key = request.GET.get("pub")
|
public_key = serializer.data.get("public_key")
|
||||||
encrypted_private_key = request.GET.get("enc_priv")
|
encrypted_private_key = serializer.data.get("encrypted_private_key")
|
||||||
ref_code = request.GET.get("ref_code")
|
ref_code = serializer.data.get("ref_code")
|
||||||
|
|
||||||
if not public_key or not encrypted_private_key:
|
if not public_key or not encrypted_private_key:
|
||||||
context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
|
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
|
# 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.
|
# 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
|
# Submitting the three params needed to compute token entropy is not mandatory
|
||||||
# If not supply, users can be created with garbage entropy token. Frontend will always supply.
|
# If not submitted, avatars can be created with garbage entropy token. Frontend will always submit them.
|
||||||
try:
|
try:
|
||||||
unique_values = int(request.GET.get("unique_values"))
|
unique_values = serializer.data.get("unique_values")
|
||||||
counts = request.GET.get("counts").split(",")
|
counts = serializer.data.get("counts")
|
||||||
counts = [int(x) for x in counts]
|
length = serializer.data.get("length")
|
||||||
length = int(request.GET.get("length"))
|
|
||||||
|
|
||||||
shannon_entropy = entropy(counts, base=62)
|
shannon_entropy = entropy(counts, base=62)
|
||||||
bits_entropy = log2(unique_values**length)
|
bits_entropy = log2(unique_values**length)
|
||||||
@ -671,11 +704,12 @@ class UserView(APIView):
|
|||||||
user.profile.referred_by = queryset[0]
|
user.profile.referred_by = queryset[0]
|
||||||
|
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
|
|
||||||
context["public_key"] = public_key
|
context["public_key"] = user.profile.public_key
|
||||||
context["encrypted_private_key"] = encrypted_private_key
|
context["encrypted_private_key"] = user.profile.encrypted_private_key
|
||||||
return Response(context, status=status.HTTP_201_CREATED)
|
return Response(context, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# log in user and return pub/priv keys if existing
|
||||||
else:
|
else:
|
||||||
user = authenticate(request, username=nickname, password=token_sha256)
|
user = authenticate(request, username=nickname, password=token_sha256)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
@ -684,11 +718,11 @@ class UserView(APIView):
|
|||||||
if request.user.date_joined < (timezone.now() -
|
if request.user.date_joined < (timezone.now() -
|
||||||
timedelta(minutes=3)):
|
timedelta(minutes=3)):
|
||||||
context["found"] = "We found your Robot avatar. Welcome back!"
|
context["found"] = "We found your Robot avatar. Welcome back!"
|
||||||
context["public_key"] = request.user.profile.public_key
|
context["public_key"] = user.profile.public_key
|
||||||
context["encrypted_private_key"] = request.user.profile.encrypted_private_key
|
context["encrypted_private_key"] = user.profile.encrypted_private_key
|
||||||
return Response(context, status=status.HTTP_202_ACCEPTED)
|
return Response(context, status=status.HTTP_202_ACCEPTED)
|
||||||
else:
|
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["found"] = "Bad luck, this nickname is taken"
|
||||||
context["bad_request"] = "Enter a different token"
|
context["bad_request"] = "Enter a different token"
|
||||||
return Response(context, status.HTTP_403_FORBIDDEN)
|
return Response(context, status.HTTP_403_FORBIDDEN)
|
||||||
|
@ -34,6 +34,7 @@ services:
|
|||||||
- .:/usr/src/robosats
|
- .:/usr/src/robosats
|
||||||
- /mnt/development/lnd:/lnd
|
- /mnt/development/lnd:/lnd
|
||||||
network_mode: service:tor
|
network_mode: service:tor
|
||||||
|
command: python3 -u manage.py runserver 0.0.0.0:8000
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build: ./frontend
|
||||||
|
@ -13,7 +13,7 @@ import { RoboSatsNoTextIcon } from "./Icons";
|
|||||||
|
|
||||||
import { sha256 } from 'js-sha256';
|
import { sha256 } from 'js-sha256';
|
||||||
import { genBase62Token, tokenStrength } from "../utils/token";
|
import { genBase62Token, tokenStrength } from "../utils/token";
|
||||||
import { genKey } from "../utils/pgp";
|
import { genKey , encryptMessage , decryptMessage} from "../utils/pgp";
|
||||||
import { getCookie, writeCookie } from "../utils/cookies";
|
import { getCookie, writeCookie } from "../utils/cookies";
|
||||||
|
|
||||||
|
|
||||||
@ -51,19 +51,31 @@ class UserGenPage extends Component {
|
|||||||
|
|
||||||
getGeneratedUser=(token)=>{
|
getGeneratedUser=(token)=>{
|
||||||
|
|
||||||
var strength = tokenStrength(token);
|
const strength = tokenStrength(token);
|
||||||
|
const refCode = this.refCode
|
||||||
|
|
||||||
genKey(token).then((key) =>
|
const requestOptions = genKey(token).then(function(key) {
|
||||||
fetch('/api/user' +
|
return {
|
||||||
'?token_sha256=' + sha256(token) +
|
method: 'POST',
|
||||||
'&pub=' + key.publicKeyArmored +
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||||
'&enc_priv=' + key.encryptedPrivateKeyArmored +
|
body: JSON.stringify({
|
||||||
'&unique_values=' + strength.uniqueValues +
|
token_sha256: sha256(token),
|
||||||
'&counts=' + strength.counts +
|
public_key: key.publicKeyArmored,
|
||||||
'&length=' + token.length +
|
encrypted_private_key: key.encryptedPrivateKeyArmored,
|
||||||
'&ref_code=' + this.refCode)
|
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((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => { console.log(data) &
|
||||||
this.setState({
|
this.setState({
|
||||||
nickname: data.nickname,
|
nickname: data.nickname,
|
||||||
bit_entropy: data.token_bits_entropy,
|
bit_entropy: data.token_bits_entropy,
|
||||||
@ -84,11 +96,13 @@ class UserGenPage extends Component {
|
|||||||
nickname: data.nickname,
|
nickname: data.nickname,
|
||||||
token: token,
|
token: token,
|
||||||
avatarLoaded: false,
|
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
|
// If the robot has been found (recovered) we assume the token is backed up
|
||||||
(data.found ? this.props.setAppState({copiedToken:true}) : null)
|
(data.found ? this.props.setAppState({copiedToken:true}) : null)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +122,18 @@ class UserGenPage extends Component {
|
|||||||
tokenHasChanged: true,
|
tokenHasChanged: true,
|
||||||
});
|
});
|
||||||
this.props.setAppState({copiedToken: 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)=>{
|
handleChangeToken=(e)=>{
|
||||||
@ -169,7 +195,7 @@ class UserGenPage extends Component {
|
|||||||
{
|
{
|
||||||
this.state.found ?
|
this.state.found ?
|
||||||
<Grid item xs={12} align="center">
|
<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/>
|
{this.state.found ? t("A robot avatar was found, welcome back!"):null}<br/>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -179,7 +205,7 @@ class UserGenPage extends Component {
|
|||||||
<Grid container align="center">
|
<Grid container align="center">
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<TextField sx={{maxWidth: 280}}
|
<TextField sx={{maxWidth: 280}}
|
||||||
error={this.state.bad_request}
|
error={this.state.bad_request ? true : false}
|
||||||
label={t("Store your token safely")}
|
label={t("Store your token safely")}
|
||||||
required={true}
|
required={true}
|
||||||
value={this.state.token}
|
value={this.state.token}
|
||||||
|
Loading…
Reference in New Issue
Block a user