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