Coverage for gws-app/gws/lib/password/__init__.py: 0%
55 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Password tools."""
3import base64
4import hashlib
5import hmac
6import random
7import string
10def compare(a: str, b: str) -> bool:
11 """Compares two Strings in a safe way to prevent timing attacks.
13 Args:
14 a: 1st String.
15 b: 2nd String.
17 Returns:
18 ``True`` if a equals b, ``False`` otherwise.
19 """
21 return hmac.compare_digest(a.encode('utf8'), b.encode('utf8'))
24def encode(password: str, algo: str = 'sha512') -> str:
25 """Encode a password into a hash.
27 Args:
28 password: String password.
29 algo: Hashing algorithm. Default is SHA512.
31 Returns:
32 Respective hash value in the format ``$algorithm$salt$hash``.
33 """
35 salt = _random_string(8)
36 h = _pbkdf2(password, salt, algo)
37 return '$'.join(['', algo, salt, base64.urlsafe_b64encode(h).decode('utf8')])
40def check(password: str, encoded: str) -> bool:
41 """Check if a password matches a hash.
43 Args:
44 password: Password as a string.
45 encoded: Hash of the input password as a string.
47 Returns:
48 ``True`` if password matches the hash, else ``False``.
49 """
51 try:
52 _, algo, salt, hs = str(encoded).split('$')
53 h1 = base64.urlsafe_b64decode(hs)
54 h2 = _pbkdf2(password, salt, algo)
55 except (TypeError, ValueError):
56 return False
58 return hmac.compare_digest(h1, h2)
61class SymbolGroup:
62 def __init__(self, s, min_len, max_len):
63 self.chars = s
64 self.max = max_len
65 self.min = min_len
66 self.count = 0
69def generate(
70 min_len: int = 16,
71 max_len: int = 16,
72 min_lower: int = 0,
73 max_lower: int = 255,
74 min_upper: int = 0,
75 max_upper: int = 255,
76 min_digit: int = 0,
77 max_digit: int = 255,
78 min_punct: int = 0,
79 max_punct: int = 255,
80) -> str:
81 """Generate a random password."""
83 groups = [
84 SymbolGroup(string.ascii_lowercase, min_lower, max_lower),
85 SymbolGroup(string.ascii_uppercase, min_upper, max_upper),
86 SymbolGroup(string.digits, min_digit, max_digit),
87 SymbolGroup(string.punctuation, min_punct, max_punct),
88 ]
89 return generate_with_groups(groups, min_len, max_len)
92def generate_with_groups(
93 groups: list[SymbolGroup],
94 min_len: int = 16,
95 max_len: int = 16,
96) -> str:
97 """Generate a random password from a list of `SymbolGroup` objects."""
99 r = random.SystemRandom()
100 p = []
102 for g in groups:
103 p.extend(r.choices(g.chars, k=g.min))
104 g.count = g.min
106 if len(p) > max_len:
107 raise ValueError('invalid parameters')
109 size = r.randint(max(min_len, len(p)), max_len)
111 while len(p) < size:
112 sel = ''.join(g.chars for g in groups if g.count < g.max)
113 if not sel:
114 raise ValueError('invalid parameters')
115 c = r.choice(sel)
116 for g in groups:
117 if c in g.chars:
118 g.count += 1
119 break
120 p.append(c)
122 r.shuffle(p)
124 return ''.join(p)
127##
130def _pbkdf2(password, salt, algo):
131 return hashlib.pbkdf2_hmac(algo, password.encode('utf8'), salt.encode('utf8'), 100000)
134def _random_string(length):
135 a = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
136 r = random.SystemRandom()
137 return ''.join(r.choice(a) for _ in range(length))