0
\$\begingroup\$

This code snippet shows an implementation of a password policy check. - The method composition uses regex to verify that the password meets composition criteria (at least one number, one special character, one capital letter and length between 8 and 64) - The method check_strength() uses the library zxcvbn to check a password's strength

Initial setup:

pip install zxcvbn

Code:

import re
from zxcvbn import zxcvbn

def composition(password):
    number = "(?=.*\d)"
    upper_lower = "(?=.*[a-z])(?=.*[A-Z])"
    special = "[^A-Za-z0-9]"
    length = ".{8,64}$"

    test = []
    test.append(True if re.search(re.compile(number), password) else False)
    test.append(True if re.search(re.compile(upper_lower), password) else False)
    test.append(True if re.search(re.compile(special), password) else False)
    test.append(True if re.search(re.compile(length), password) else False)
    
    return test

def check_strength(password):
    result = zxcvbn(password, user_inputs=['John', 'Smith'])
    strength = {
        0: "Worst",
        1: "Bad",
        2: "Weak",
        3: "Good",
        4: "Strong"
    }

    return [result['score'], strength[result['score']], result['feedback']]



def main():
    password = 'Abcdefghi1.'

    x = composition(password)
    print("composition: ", x)

    y = check_strength(password)
    print("check_strength: ", y)

    return None

if __name__ == '__main__':
    main()

1- Do you think the implementation of this code snippet is up-to-date and secure?

2- Would you make any other changes to this code snippet?

3- Did you use any additional resources while checking the code? If yes, please provide a link or description of your resource.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Elevate the data that drives the code to the status of named constants. Among other benefits, this simple change will support any efforts you might make to subject the code to automated testing (because your test assertions can use the constants too).

The regular expressions are overly complex and maybe naive. Too complex because they contain unnecessary non-capturing groups (I got rid of them, in addition to separating the tests for upper and lower). Maybe too naive, depending on your goals, because they assume that the universe of letters is restricted to ASCII (I made no changes in that regard).

import re
import sys
from zxcvbn import zxcvbn
from dataclasses import dataclass
from typing import Tuple

STRENGTHS = {
    0: 'Worst',
    1: 'Bad',
    2: 'Weak',
    3: 'Good',
    4: 'Strong'
}

PATTERNS = dict(
    number = re.compile('\d'),
    lower = re.compile('[a-z]'),
    upper = re.compile('[A-Z]'),
    special = re.compile('[^A-Za-z0-9]'),
    length = re.compile('.{8,64}$'),
)

Return more declarative data. Both composition() and check_strength() return fairly cryptic data bundles: a boolean list and a hodgepodge list. More useful would be declarative return values, such as a dict or dataclass. Each is illustrated here:

@dataclass(frozen = True)
class PasswordStrength:
    score: int
    label: str
    warning: str
    suggestions: Tuple[str]

def composition(password):
    return {
        label : bool(rgx.search(password))
        for label, rgx in PATTERNS.items()
    }

def check_strength(password):
    z = zxcvbn(password, user_inputs=['John', 'Smith'])
    return PasswordStrength(
        z['score'],
        STRENGTHS[z['score']],
        z['feedback']['warning'],
        tuple(z['feedback']['suggestions']),
    )

Consider unifying the checks. I don't know your larger goals, but in the abstract I would be tempted to unify the logic of both functions and return a single frozen data bundle containing all of the checks: everything currently in PasswordStrength, plus some attributes like has_number, has_upper, and so forth. The class could also include one or more summary @property methods to return a yes/no verdict based on the results from the individual checks.

Get in the habit of parameterizing the main function. This is a small issue in the grand scheme, but while working on the code, I was annoyed that I had to edit the code each time I wanted to experiment with a different password. You can avoid such hassles by setting up the main() function to accept command-line arguments. For example:

def main(args):
    password = args[0] if args else 'Abcdefghi1.' 
    ...

if __name__ == '__main__':
    main(sys.argv[1:])
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.