Files
MicroPythonOS/internal_filesystem/lib/secrets.py
T
Thomas Farstrike e1c9c51955 work towards fixing
2025-05-19 11:21:54 +02:00

106 lines
3.6 KiB
Python

# secrets.py: Compatibility layer for CPython's secrets module in MicroPython
# Uses urandom for cryptographically secure randomness
# Implements SystemRandom, choice, randbelow, randbits, token_bytes, token_hex,
# token_urlsafe, and compare_digest
import urandom
import ubinascii
import uhashlib
import utime
class SystemRandom:
"""Emulates random.SystemRandom using MicroPython's urandom."""
def randrange(self, start, stop=None, step=1):
"""Return a random int in range(start, stop[, step])."""
if stop is None:
stop = start
start = 0
if step != 1:
raise NotImplementedError("step != 1 not supported")
if start >= stop:
raise ValueError("empty range")
range_size = stop - start
return start + self._randbelow(range_size)
def _randbelow(self, n):
"""Return a random int in [0, n)."""
if n <= 0:
raise ValueError("exclusive_upper_bound must be positive")
k = (n.bit_length() + 7) // 8 # Bytes needed for n
r = 0
while True:
r = int.from_bytes(self._getrandbytes(k), 'big')
if r < n:
return r
def _getrandbytes(self, n):
"""Return n random bytes."""
# Use bytes directly for compatibility with CPython secrets
return bytes(urandom.getrandbits(8) for _ in range(n))
def choice(self, seq):
"""Return a randomly chosen element from a non-empty sequence."""
if not seq:
raise IndexError("cannot choose from an empty sequence")
return seq[self._randbelow(len(seq))]
def randbits(self, k):
"""Return a non-negative int with k random bits."""
if k < 0:
raise ValueError("number of bits must be non-negative")
numbytes = (k + 7) // 8
return int.from_bytes(self._getrandbytes(numbytes), 'big') >> (numbytes * 8 - k)
# Instantiate SystemRandom for module-level functions
_sysrand = SystemRandom()
def choice(seq):
"""Return a randomly chosen element from a non-empty sequence."""
return _sysrand.choice(seq)
def randbelow(exclusive_upper_bound):
"""Return a random int in [0, exclusive_upper_bound)."""
return _sysrand._randbelow(exclusive_upper_bound)
def randbits(k):
"""Return a non-negative int with k random bits."""
return _sysrand.randbits(k)
def token_bytes(nbytes=None):
"""Return a random byte string of nbytes. Default is 32 bytes."""
if nbytes is None:
nbytes = 32
if nbytes < 0:
raise ValueError("number of bytes must be non-negative")
return _sysrand._getrandbytes(nbytes)
def token_hex(nbytes=None):
"""Return a random hex string of nbytes. Default is 32 bytes."""
return ubinascii.hexlify(token_bytes(nbytes)).decode()
def token_urlsafe(nbytes=None):
"""Return a random URL-safe base64 string of nbytes. Default is 32 bytes."""
if nbytes is None:
nbytes = 32
if nbytes < 0:
raise ValueError("number of bytes must be non-negative")
raw_bytes = token_bytes(nbytes)
encoded = ubinascii.b2a_base64(raw_bytes).decode().rstrip('\n=')
return encoded[:int(nbytes * 4 / 3)]
def compare_digest(a, b):
"""Return True if a and b are equal in constant time, else False."""
if isinstance(a, str):
a = a.encode()
if isinstance(b, str):
b = b.encode()
if not isinstance(a, (bytes, bytearray)) or not isinstance(b, (bytes, bytearray)):
raise TypeError("both inputs must be bytes-like or strings")
if len(a) != len(b):
return False
result = 0
for x, y in zip(a, b):
result |= x ^ y
return result == 0