vikrant892/password-strength-api
0
1import secrets
2import string
3
4# chars that look way too similar in most fonts
5AMBIGUOUS_CHARS = "0O1lI|`"
6
7
8def generate_password(
9 length: int = 16,
10 uppercase: bool = True,
11 lowercase: bool = True,
12 digits: bool = True,
13 symbols: bool = True,
14 exclude_ambiguous: bool = False,
15) -> str:
16 """
17 generate a cryptographically secure random password
18 uses secrets module (not random!) because random is not suitable for security
19 """
20 charset = ""
21
22 if lowercase:
23 charset += string.ascii_lowercase
24 if uppercase:
25 charset += string.ascii_uppercase
26 if digits:
27 charset += string.digits
28 if symbols:
29 charset += string.punctuation
30
31 if not charset:
32 # fallback if somehow everything is disabled (shouldn't happen with validation but just in case)
33 charset = string.ascii_letters + string.digits
34
35 if exclude_ambiguous:
36 charset = "".join(c for c in charset if c not in AMBIGUOUS_CHARS)
37
38 # generate and make sure we have at least one of each requested type
39 # otherwise you get complaints like "where's my number??"
40 while True:
41 password = "".join(secrets.choice(charset) for _ in range(length))
42
43 # verify at least one char from each requested category
44 checks = []
45 if lowercase:
46 checks.append(any(c in string.ascii_lowercase for c in password))
47 if uppercase:
48 checks.append(any(c in string.ascii_uppercase for c in password))
49 if digits:
50 checks.append(any(c in string.digits for c in password))
51 if symbols:
52 checks.append(any(c in string.punctuation for c in password))
53
54 if all(checks):
55 return password
56 # if we didn't get all categories, regenerate
57 # with length >= 8 this almost never loops more than once
58 