python: Use standard secrets module to generate random tokens.

There are three functional side effects:

• Correct an insignificant but mathematically offensive bias toward
repeated characters in generate_api_key introduced in commit
47b4283c4b4c70ecde4d3c8de871c90ee2506d87; its entropy is increased
from 190.52864 bits to 190.53428 bits.

• Use the base32 alphabet in confirmation.models.generate_key; its
entropy is reduced from 124.07820 bits to the documented 120 bits, but
now it uses 1 syscall instead of 24.

• Use the base32 alphabet in get_bigbluebutton_url; its entropy is
reduced from 51.69925 bits to 50 bits, but now it uses 1 syscall
instead of 10.

(The base32 alphabet is A-Z 2-7.  We could probably replace all of
these with plain secrets.token_urlsafe, since I expect most callers
can handle the full urlsafe_b64 alphabet A-Z a-z 0-9 - _ without
problems.)

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2020-09-04 19:02:13 -07:00
committed by Tim Abbott
parent 56546170cf
commit b7b7475672
20 changed files with 51 additions and 57 deletions

View File

@@ -1,10 +1,8 @@
import base64
import hashlib
import heapq
import itertools
import os
import re
import string
import secrets
from itertools import zip_longest
from time import sleep
from typing import Any, Callable, Iterator, List, Optional, Sequence, Set, Tuple, TypeVar
@@ -107,14 +105,12 @@ def log_statsd_event(name: str) -> None:
event_name = f"events.{name}"
statsd.incr(event_name)
def generate_random_token(length: int) -> str:
return str(base64.b16encode(os.urandom(length // 2)).decode('utf-8').lower())
def generate_api_key() -> str:
choices = string.ascii_letters + string.digits
altchars = ''.join(choices[ord(os.urandom(1)) % 62] for _ in range(2)).encode("utf-8")
api_key = base64.b64encode(os.urandom(24), altchars=altchars).decode("utf-8")
return api_key
api_key = ""
while len(api_key) < 32:
# One iteration suffices 99.4992% of the time.
api_key += secrets.token_urlsafe(3 * 9).replace("_", "").replace("-", "")
return api_key[:32]
def has_api_key_format(key: str) -> bool:
return bool(re.fullmatch(r"([A-Za-z0-9]){32}", key))