Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions quantara/web_app/api/referal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- 404: If the user with the provided wallet ID does not exist.
"""

import random
import secrets
import string

from fastapi import APIRouter, Depends, FastAPI, HTTPException, Query, Request
Expand All @@ -30,6 +30,8 @@
responses={404: {"description": "Not found"}},
)

_ALPHABET = string.ascii_letters + string.digits


class ReferralResponse(BaseModel):
"""
Expand All @@ -40,9 +42,12 @@ class ReferralResponse(BaseModel):
referral_code: str


def generate_random_string(length=16):
def generate_random_string(length: int = 16) -> str:
"""
Generate a random string of letters and digits with the given length.
Generate a cryptographically secure random alphanumeric string.

Uses the ``secrets`` module (CSPRNG) instead of ``random`` so referral
codes cannot be predicted from prior outputs (see issue #196).

Args:
length (int): Length of the string (default is 16).
Expand All @@ -51,7 +56,7 @@ def generate_random_string(length=16):
str: Randomly generated string.
"""

return "".join(random.choices(string.ascii_letters + string.digits, k=length))
return "".join(secrets.choice(_ALPHABET) for _ in range(length))


@router.get("/create_referal_link")
Expand Down
52 changes: 52 additions & 0 deletions quantara/web_app/tests/test_create_referal_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,55 @@
assert referral_code1 != referral_code2


def test_generate_random_string_uses_secrets_not_random():
"""Referral codes must come from secrets (CSPRNG), not random."""
import inspect
import random
import string

from web_app.api import referal

source = inspect.getsource(referal.generate_random_string)
assert "secrets" in source

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert "random.choices" not in source

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert "random.random" not in source

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

alphabet = string.ascii_letters + string.digits
code = referal.generate_random_string(16)
assert len(code) == 16

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert all(ch in alphabet for ch in code)

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# Sanity: many draws stay unique enough that collision is rare.
codes = {referal.generate_random_string(16) for _ in range(200)}
assert len(codes) == 200

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_generate_random_string_character_distribution():
"""Rough uniform distribution over the 62-symbol alphabet (10k draws)."""
import string
from collections import Counter

from web_app.api.referal import generate_random_string

alphabet = string.ascii_letters + string.digits
draws = 10_000
length = 16
counts = Counter()
for _ in range(draws):
counts.update(generate_random_string(length))

total = draws * length
expected = total / len(alphabet)

Check notice

Code scanning / CodeQL

Unused local variable Note test

Variable expected is not used.
# ±0.5% absolute frequency band around 1/62 as specified in #196.
for ch in alphabet:
freq = counts[ch] / total
assert abs(freq - (1 / len(alphabet))) <= 0.005, (

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
f"char {ch!r} frequency {freq:.6f} outside ±0.5% of 1/62"
)
# Entropy floor for 16 alnum chars: log2(62^16) ≈ 95.3 bits.
import math

entropy_bits = length * math.log2(len(alphabet))
assert entropy_bits >= 95

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


Loading