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)