diff --git a/openvpncertupdate.py b/openvpncertupdate.py index 0c9452c..4b5b2ee 100644 --- a/openvpncertupdate.py +++ b/openvpncertupdate.py @@ -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) # ============================================================ diff --git a/tests/test_password.py b/tests/test_password.py new file mode 100644 index 0000000..3ccc553 --- /dev/null +++ b/tests/test_password.py @@ -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