Password Validation¶
Assignment¶
Build a Python application with:
- Build a password validation function that checks an input password against the following 5 rules. Continue to prompt the user for a password until it meets the validation rules. If not, notify the user to re-input with a clear message.
Rule description | Requirement | Example |
---|---|---|
Minimum password length | 8 characters | Ensures basic resistance to brute-force attacks |
Must include at least 1 special symbols | 1 character | Symbols like !, @, #, $, %, etc. can increase complexity |
Must contain both uppercase and lowercase letters | Yes | Example: "SecurePass" instead of "securepass" |
Must include at least one numeric digit | Yes | Example: "Passw0rd" includes the digit "0" |
Avoid common words or predictable patterns | Yes | Avoid "123456", "password", or keyboard patterns like "qwerty" |
-
Calculated time to execute guess and manipulation into 10k cases.
-
(Advance - Optional) Using GitHub to publish the program into a repository through a pull request (PR)
Solution¶
The engine required to build function(s) to validate string input and returned the structured output indicating whether the input meets the validation rules.
With that, the engine is interactive between the application and user based on the following flow:
sequenceDiagram
participant user as User
participant application as Application Interface
participant engine as Engine
%% Flow
user->>application: input password (type string)
application->>engine: push argument
engine->>engine: Validation process
engine->>application: return a dictionary containing the validation result of each rule
application->>user: Table formated of the dictionary
Using TypeDict
to structured output format of the engine with following metadata
Field | Type | Description | Sample |
---|---|---|---|
rule_id | int | Unique identifier for each rule | 1 |
rule_description | str | Description of each rule | Minimum password length |
status | bool | Status of the validation result | True |
context | str | Context of the validation result | Password length is 8 characters |
For the detail of the validation result, so that this can be accessed throguh the application
Using the NordPass list of most commonly used passwords as a basis, the engine should not allow any of the common rules to pass the list.
For example, if the validation is successful,
π Please input your password to validate: 9asdjnhasdj1238asdK2$
βββββββββ³ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ³ββββββββββ
β [No.] β Rule description β Status β
β‘ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ©
β 1 β Password length is required to be at least 8 characters β True π’ β
β 2 β Password must include at least 1 special symbol β True π’ β
β 3 β Must contain both uppercase and lowercase letters β True π’ β
β 4 β Must include at least 1 digit β True π’ β
β 5 β Avoid common words or predictable patterns β True π’ β
βββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ΄ββββββββββ
>>> Your password is secure
And if the password fails in any of the rules
π Please input your password to validate: notsatifypass
βββββββββ³ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ³βββββββββββ
β [No.] β Rule description β Status β
β‘βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ©
β 1 β Password length is required to be at least 8 characters β True π’ β
β 2 β Password must include at least 1 special symbol β False π΄ β
β 3 β Must contain both uppercase and lowercase letters β True π’ β
β 4 β Must include at least 1 digit β False π΄ β
β 5 β Avoid common words or predictable patterns β True π’ β
βββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββ
>>> Your password is not strong enough.
π° Please re-input again: Failagain
βββββββββ³ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ³βββββββββββ
β [No.] β Rule description β Status β
β‘βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ©
β 1 β Password length is required to be at least 8 characters β True π’ β
β 2 β Password must include at least 1 special symbol β False π΄ β
β 3 β Must contain both uppercase and lowercase letters β True π’ β
β 4 β Must include at least 1 digit β False π΄ β
β 5 β Avoid common words or predictable patterns β True π’ β
βββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββ
>>> Your password is not strong enough.
π° Please re-input again: # ------------ Will loop until success
Code:
Start to build an engine function to define
#!/bin/python3
# Global
import re
from typing import TypedDict, Callable, Sequence
import string
import itertools
class ValidationResultOnRule(TypedDict):
rule_id: str
rule_description: str
status: bool = False
contexts: dict
class ValidationResult(TypedDict):
status: bool = False
failures: list[int]
details: dict[int, ValidationResultOnRule]
class ValidationRule(object):
def __init__(self, rule_id: str, rule_description: str, func: Callable[[str], bool], context_generator: dict[str, Callable[[str], dict]] | None = None):
self.rule_id = rule_id
self.rule_description = rule_description
self.func = func
self.context_generator = context_generator
def validate(self, text: str) -> ValidationResultOnRule:
validation_result = self.func(text)
contexts = {}
if self.context_generator is not None:
for key, context_func in self.context_generator.items():
metadata = context_func(text)
contexts[key] = metadata
result = ValidationResultOnRule(
rule_id=self.rule_id,
rule_description=self.rule_description,
status=validation_result,
contexts=contexts
)
return result
def validate_rules(text: str, rules: dict[int, ValidationRule]) -> ValidationResult:
"""
Validate a string against a list of validation rules.
Args
----
text (str): The string to validate.
rules (list[ValidationRule]): A list of validation rules to check against.
Returns
-------
A ValidationResult object, which contains the results of the validation for each rule.
The object contains a dictionary with the index of the rule as the key and a ValidationResultOnRule object as the value.
The ValidationResultOnRule object contains the status of the validation, a description of the rule, and a dictionary of additional context metadata.
Usage
-----
>>> validate_rules(
text="randomStringForValid9999@#$",
rules={
1: ValidationRule(rule_id="length", rule_description="Minimum length required", func=lambda s: len(s) >= 3, context_generator={"length": len}),
}
)
"""
# Pair
rule_results: ValidationResultOnRule = {}
for index, rule in rules.items():
output = rule.validate(text=text)
rule_results[index] = output
# Chain
result = ValidationResult(
status=all([r["status"] for _, r in rule_results.items()]),
failures=[k for k, _ in itertools.filterfalse(lambda s: s[1]["status"], rule_results.items())],
details=rule_results
)
return result
def validate_contain_at_least_n_special_symbol(text: str, n: int) -> bool:
return len(re.findall(rf"[{re.escape(string.punctuation)}]", text)) >= n
def validate_contain_both_uppsercase_and_lowercase(text: str) -> bool:
return (
len(re.findall(rf"[{re.escape(string.ascii_uppercase)}]", text))
+ len(re.findall(rf"[{re.escape(string.ascii_lowercase)}]", text))
) >= 2
def validate_contain_at_least_n_digit(text: str, n: int) -> bool:
return len(re.findall(rf"[{re.escape(string.digits)}]", text)) >= n
def validate_not_match_predictable_patterns(text: str, bucket: Sequence[str]) -> bool:
for value in bucket:
if value == text:
return False
return True
# List of common password by top providers
# Included: Nordpass, Keeper, Global (Set string of all providers)
# For references, see: <https://en.wikipedia.org/wiki/List_of_the_most_common_passwords>
MOST_COMMON_PASSWORDS_NORDPASS = (
"123456",
"123456789",
"1234567890",
"password",
"qwerty123",
"qwerty1",
"111111",
"12345",
"secret",
"123123",
"1234567890",
"000000",
"abc123",
"password1",
"iloveyou",
"11111111",
"dragon",
"monkey",
)
MOST_COMMON_PASSWORDS_KEEPER = (
"123456",
"123456789",
"qwerty",
"12345678",
"111111",
"1234567890",
"1234567",
"password",
"123123",
"987654321",
"qwertyuiop",
"mynoob",
"123321",
"666666",
"18atcskd2w",
"7777777",
"1q2w3e4r",
"654321",
"555555",
"3rjs1la7qe",
"google",
"1q2w3e4r5t",
"123qwe",
"zxcvbnm",
"1q2w3e",
)
MOST_COMMON_PASSWORDS: set[str] = set(MOST_COMMON_PASSWORDS_NORDPASS + MOST_COMMON_PASSWORDS_KEEPER)
Then construct application
#!/bin/python3
# Global
import sys
import os
# Path Append
sys.path.append(os.path.abspath(os.curdir))
# External
from rich.prompt import Prompt
from rich.console import Console
from rich.table import Table
# Internal
from lab.password_validation.engine import (
ValidationRule,
validate_contain_at_least_n_special_symbol,
validate_contain_both_uppsercase_and_lowercase,
validate_contain_at_least_n_digit,
validate_not_match_predictable_patterns,
MOST_COMMON_PASSWORDS_NORDPASS,
validate_rules,
)
# Construct passowrd validation rules
PASSWORD_VALIDATION_RULES = {
1: ValidationRule(
rule_id="MINIMUM_PASSWORD_LENGTH",
rule_description="Password length is required to be at least 8 characters",
func=lambda s: len(s) >= 8,
context_generator={
"length": len
}
),
2: ValidationRule(
rule_id="INCLUDE_AT_LEAST_ONE_SPECIAL_SYMBOL",
rule_description="Password must include at least 1 special symbol",
func=lambda s: validate_contain_at_least_n_special_symbol(s, n=1),
),
3: ValidationRule(
rule_id="INCLUDE_BOTH_UPPERCASE_AND_LOWERCASE_LETTERS",
rule_description="Must contain both uppercase and lowercase letters",
func=validate_contain_both_uppsercase_and_lowercase,
),
4: ValidationRule(
rule_id="INCLUDE_AT_LEAST_ONE_DIGIT",
rule_description="Must include at least 1 digit",
func=lambda s: validate_contain_at_least_n_digit(s, n=1),
),
5: ValidationRule(
rule_id="AVOID_COMMON_PATTERNS",
rule_description="Avoid common words or predictable patterns",
func=lambda s: validate_not_match_predictable_patterns(s, bucket=MOST_COMMON_PASSWORDS_NORDPASS),
),
}
if __name__ == "__main__":
# Handle
console = Console(emoji=True)
# Get from user
# Inspired by: https://github.com/Textualize/rich/blob/master/rich/prompt.py
input_sentence = Prompt.ask(":flags: Please input your password to validate")
while True:
# Validate
validation_result = validate_rules(text=input_sentence, rules=PASSWORD_VALIDATION_RULES)
# Add the table output
table = Table()
table.add_column("[No.]")
table.add_column("Rule description")
table.add_column("Status")
for ind, detail in sorted(validation_result["details"].items()):
_mark = ":green_circle:" if detail["status"] else ":red_circle:"
table.add_row(f"{ind}", detail["rule_description"], f"{detail['status']} {_mark}")
# Handle
console.print(table)
if validation_result["status"] is True:
console.print("[green]>>> Your password is secure")
break
else:
console.print("[red]>>> Your password is not strong enough.")
input_sentence = Prompt.ask(":anxious_face_with_sweat: Please re-input again")
Next, execute the application by running the following command in your terminal:
For the performance of the validate_rules
engine:
python lab/password_validation/benchmark.py
# [Result] [Total] Execute 10000 executions take 0.546714 seconds
# [Result] [Avg.] Each execution take 0.000055 seconds
by following snippet:
#!/bin/python3
# Global
import sys
import os
import timeit
import textwrap
# Path Append
sys.path.append(os.path.abspath(os.curdir))
# Internal
stmt = textwrap.dedent("""\
from lab.password_validation.engine import validate_rules
from lab.password_validation.application import PASSWORD_VALIDATION_RULES
validate_rules(text="StrongPasswordSince2026@123456789", rules=PASSWORD_VALIDATION_RULES)
""")
if __name__ == "__main__":
# Benchmark for validate_rules with 5 rules
# The result is the total time for total of number execution
number_execution = 10_000
benchmark = timeit.timeit(stmt=stmt, number=number_execution)
print(f"[Result] [Total] Execute {number_execution} executions take {benchmark:3f} seconds")
print(f"[Result] [Avg.] Each execution take {benchmark / number_execution:3f} seconds")
Futher Reading¶
-
PEP 589 - TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys
-
re
package: https://docs.python.org/3/library/re.html -
How does Python generate random numbers?
-
Using regex pattern only to search this: https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a
-
https://en.wikipedia.org/wiki/List_of_the_most_common_passwords