v0.150.0
 1"""
 2Plain's standard crypto functions and utilities.
 3"""
 4
 5from __future__ import annotations
 6
 7import hashlib
 8import hmac
 9import secrets
10from collections.abc import Callable
11from typing import Any
12
13from plain.runtime import settings
14from plain.utils.encoding import force_bytes
15
16
17class InvalidAlgorithm(ValueError):
18    """Algorithm is not supported by hashlib."""
19
20    pass
21
22
23def salted_hmac(
24    key_salt: str | bytes,
25    value: str | bytes,
26    secret: str | bytes | None = None,
27    *,
28    algorithm: str = "sha1",
29) -> hmac.HMAC:
30    """
31    Return the HMAC of 'value', using a key generated from key_salt and a
32    secret (which defaults to settings.SECRET_KEY). Default algorithm is SHA1,
33    but any algorithm name supported by hashlib can be passed.
34
35    A different key_salt should be passed in for every application of HMAC.
36    """
37    if secret is None:
38        secret = settings.SECRET_KEY
39
40    key_salt = force_bytes(key_salt)
41    secret = force_bytes(secret)
42    try:
43        hasher = getattr(hashlib, algorithm)
44    except AttributeError as e:
45        raise InvalidAlgorithm(
46            f"{algorithm!r} is not an algorithm accepted by the hashlib module."
47        ) from e
48    # We need to generate a derived key from our base key.  We can do this by
49    # passing the key_salt and our base key through a pseudo-random function.
50    key = hasher(key_salt + secret).digest()
51    # If len(key_salt + secret) > block size of the hash algorithm, the above
52    # line is redundant and could be replaced by key = key_salt + secret, since
53    # the hmac module does the same thing for keys longer than the block size.
54    # However, we need to ensure that we *always* do this.
55    return hmac.new(key, msg=force_bytes(value), digestmod=hasher)
56
57
58RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
59
60
61def get_random_string(length: int, allowed_chars: str = RANDOM_STRING_CHARS) -> str:
62    """
63    Return a securely generated random string.
64
65    The bit length of the returned value can be calculated with the formula:
66        log_2(len(allowed_chars)^length)
67
68    For example, with default `allowed_chars` (26+26+10), this gives:
69      * length: 12, bit length =~ 71 bits
70      * length: 22, bit length =~ 131 bits
71    """
72    return "".join(secrets.choice(allowed_chars) for i in range(length))
73
74
75def pbkdf2(
76    password: str | bytes,
77    salt: str | bytes,
78    iterations: int,
79    dklen: int = 0,
80    digest: Callable[[], Any] | None = None,
81) -> bytes:
82    """Return the hash of password using pbkdf2."""
83    if digest is None:
84        digest = hashlib.sha256
85    dklen_value: int | None = dklen if dklen else None
86    password = force_bytes(password)
87    salt = force_bytes(salt)
88    return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen_value)