|
1 | 1 | import hashlib
|
2 | 2 | import hmac
|
3 | 3 | import secrets
|
| 4 | +import string |
| 5 | +import zlib |
| 6 | + |
| 7 | + |
| 8 | +def _crc32_to_base62(number: int) -> str: |
| 9 | + characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" |
| 10 | + base = len(characters) |
| 11 | + encoded = "" |
| 12 | + while number: |
| 13 | + number, remainder = divmod(number, base) |
| 14 | + encoded = characters[remainder] + encoded |
| 15 | + return encoded.zfill(6) # Ensure the checksum is 6 characters long |
4 | 16 |
|
5 | 17 |
|
6 | 18 | def get_token_hash(token: str, *, secret: str) -> str:
|
7 | 19 | hash = hmac.new(secret.encode("ascii"), token.encode("ascii"), hashlib.sha256)
|
8 | 20 | return hash.hexdigest()
|
9 | 21 |
|
10 | 22 |
|
11 |
| -def generate_token(*, prefix: str = "", nbytes: int | None = None) -> str: |
12 |
| - return f"{prefix}{secrets.token_urlsafe(nbytes)}" |
| 23 | +def generate_token(*, prefix: str = "") -> str: |
| 24 | + # Generate a high entropy random token |
| 25 | + token = "".join( |
| 26 | + secrets.choice(string.ascii_letters + string.digits) for _ in range(37) |
| 27 | + ) |
| 28 | + |
| 29 | + # Calculate a 32-bit CRC checksum |
| 30 | + checksum = zlib.crc32(token.encode("utf-8")) & 0xFFFFFFFF |
| 31 | + checksum_base62 = _crc32_to_base62(checksum) |
| 32 | + |
| 33 | + # Concatenate the prefix, token, and checksum |
| 34 | + return f"{prefix}{token}{checksum_base62}" |
13 | 35 |
|
14 | 36 |
|
15 |
| -def generate_token_hash_pair( |
16 |
| - *, secret: str, prefix: str = "", nbytes: int | None = None |
17 |
| -) -> tuple[str, str]: |
| 37 | +def generate_token_hash_pair(*, secret: str, prefix: str = "") -> tuple[str, str]: |
18 | 38 | """
|
19 | 39 | Generate a token suitable for sensitive values
|
20 | 40 | like magic link tokens.
|
21 | 41 |
|
22 | 42 | Returns both the actual value and its HMAC-SHA256 hash.
|
23 | 43 | Only the latter shall be stored in database.
|
24 | 44 | """
|
25 |
| - token = generate_token(prefix=prefix, nbytes=nbytes) |
| 45 | + token = generate_token(prefix=prefix) |
26 | 46 | return token, get_token_hash(token, secret=secret)
|
0 commit comments