feat: password generator with 28-char charset rules

Implements generate_password() function that produces 28-character passwords
following charset rules: position 1 uppercase, position 2 and last lowercase
with restricted characters, middle positions alphanumeric. Uses secrets module
for cryptographic randomness.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-23 23:49:14 +03:00
parent 3618d3a73b
commit 941b3d3f5b
2 changed files with 71 additions and 0 deletions

View File

@@ -125,6 +125,30 @@ def load_expiring_certs(
# ============================================================ # ============================================================
# === PASSWORD ===
# ============================================================
_BANNED_ALL = frozenset("oO01lI")
_BANNED_POS2_LAST = _BANNED_ALL | frozenset("j")
_UPPER = [c for c in string.ascii_uppercase if c not in _BANNED_ALL]
_LOWER_RESTRICTED = [c for c in string.ascii_lowercase if c not in _BANNED_POS2_LAST]
_LOWER_MID = [c for c in string.ascii_lowercase if c not in _BANNED_ALL]
_DIGITS_MID = [c for c in string.digits if c not in _BANNED_ALL]
_MID = _UPPER + _LOWER_MID + _DIGITS_MID # 55 chars for pos 3-27
def generate_password() -> str:
pw = [
secrets.choice(_UPPER), # pos 1: uppercase
secrets.choice(_LOWER_RESTRICTED), # pos 2: lowercase, no j
]
for _ in range(25): # pos 3-27: any safe alphanumeric
pw.append(secrets.choice(_MID))
pw.append(secrets.choice(_LOWER_RESTRICTED)) # pos 28: lowercase, no j
return "".join(pw)
# (remaining sections added in later tasks) # (remaining sections added in later tasks)
# ============================================================ # ============================================================

47
tests/test_password.py Normal file
View File

@@ -0,0 +1,47 @@
from openvpncertupdate import generate_password
BANNED_ALL = set("oO01lI")
BANNED_POS2_LAST = BANNED_ALL | {"j"}
def test_length():
assert len(generate_password()) == 28
def test_pos1_uppercase():
for _ in range(200):
pw = generate_password()
assert pw[0].isupper(), f"pos1 not uppercase: {pw}"
def test_pos2_lowercase_no_banned():
for _ in range(200):
pw = generate_password()
c = pw[1]
assert c.islower(), f"pos2 not lowercase: {pw}"
assert c not in BANNED_POS2_LAST, f"pos2 banned '{c}': {pw}"
def test_last_lowercase_no_banned():
for _ in range(200):
pw = generate_password()
c = pw[-1]
assert c.islower(), f"last not lowercase: {pw}"
assert c not in BANNED_POS2_LAST, f"last banned '{c}': {pw}"
def test_all_alphanumeric():
for _ in range(200):
pw = generate_password()
assert pw.isalnum(), f"non-alphanumeric in: {pw}"
def test_no_globally_banned():
for _ in range(500):
pw = generate_password()
for c in BANNED_ALL:
assert c not in pw, f"banned '{c}' in: {pw}"
def test_uniqueness():
assert len({generate_password() for _ in range(100)}) == 100