2
\$\begingroup\$

Problem

Write a program to return True, if a string is a strong password.


A password is considered strong if below conditions are all met:

  • It should have at least 6 characters and at most 20 characters.
  • It must contain at least one lowercase, one uppercase and one digit.
  • It must NOT contain three repeating characters in a row ("...aaa..." is weak, but "...aa...a..." is fine, assuming other conditions are met).

Code

I've solved a similar problem to LeetCode Strong Password Checker. If you would like to review the code or add other methods or provide any change/improvement recommendations please do so, and I'd really appreciate that.

def strong_password_match(s: str) -> bool:
    """Returns a boolean if a password is correct"""
    import re
    if re.match(
            r'^(?!.*([a-z])\1\1)(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[A-Za-z0-9]{6,20}$', s) is not None:
        return True
    return False


def strong_password_search(s: str) -> bool:
    """Returns a boolean if a password is correct"""
    import re
    if re.search(
            r'^(?!.*([a-z])\1\1)(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[A-Za-z0-9]{6,20}$', s) is not None:
        return True
    return False


def strong_password_findall(s: str) -> bool:
    """Returns a boolean if a password is correct"""
    import re
    if re.findall(
            r'^(?!.*([a-z])\1\1)(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[A-Za-z0-9]{6,20}$', s) != []:
        return True

    return False


if __name__ == '__main__':
    # ---------------------------- TEST ---------------------------
    import timeit
    import cProfile

    DIVIDER_DASH_LINE = '-' * 50
    GREEN_APPLE = '\U0001F34F'
    RED_APPLE = '\U0001F34E'

    test_methods = (
        ("re.match", strong_password_match),
        ("re.search", strong_password_search),
        ("re.findall", strong_password_findall),
    )

    test_inputs = ('abcAb1', 'abcAb1abcAb1abcAb100',
                   'abcAb1abcAb1abcAb10111', 'aaaAb1abcAb1abcAb100')

    # --------------------------------- PROFILING AND BANCHMARK SETTINGS --------------------------------------
    NUMBER_OF_RUNS = 1
    CPROFILING_ON = False
    BENCHMARK_ON = True

    for description, method in test_methods:
        print((GREEN_APPLE + RED_APPLE) * 5)

        for test_input in test_inputs:
            if CPROFILING_ON:
                print(f'{description} cProfiling: ', cProfile.run("method(test_input)"))
            if BENCHMARK_ON:
                print(f'{description} Benchmark: ', timeit.Timer(
                    f'for i in range({NUMBER_OF_RUNS}): {method(test_input)}', 'gc.enable()').timeit())

            if method(test_input):
                print(f'{GREEN_APPLE} {description}: "{test_input}" is a strong password.')
            else:
                print(f'{RED_APPLE} {description}: "{test_input}" is not a strong password.')

Output

๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ
re.match Benchmark:  1.8437564770000001
๐Ÿ re.match: "abcAb1" is a strong password.
re.match Benchmark:  1.9900512190000006
๐Ÿ re.match: "abcAb1abcAb1abcAb100" is a strong password.
re.match Benchmark:  1.782858269
๐ŸŽ re.match: "abcAb1abcAb1abcAb10111" is not a strong password.
re.match Benchmark:  1.8016775740000002
๐ŸŽ re.match: "aaaAb1abcAb1abcAb100" is not a strong password.
๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ
re.search Benchmark:  1.8374795240000008
๐Ÿ re.search: "abcAb1" is a strong password.
re.search Benchmark:  1.8352807049999988
๐Ÿ re.search: "abcAb1abcAb1abcAb100" is a strong password.
re.search Benchmark:  1.8023251919999996
๐ŸŽ re.search: "abcAb1abcAb1abcAb10111" is not a strong password.
re.search Benchmark:  1.9213070860000006
๐ŸŽ re.search: "aaaAb1abcAb1abcAb100" is not a strong password.
๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ๐Ÿ๐ŸŽ
re.findall Benchmark:  1.7806425130000019
๐Ÿ re.findall: "abcAb1" is a strong password.
re.findall Benchmark:  1.836686480000001
๐Ÿ re.findall: "abcAb1abcAb1abcAb100" is a strong password.
re.findall Benchmark:  1.8053996060000017
๐ŸŽ re.findall: "abcAb1abcAb1abcAb10111" is not a strong password.
re.findall Benchmark:  1.8203372360000003
๐ŸŽ re.findall: "aaaAb1abcAb1abcAb100" is not a strong password.

If you wish to simplify/modify/explore the expression, it's been explained on the top right panel of regex101.com. If you'd like, you can also watch in this link, how it would match against some sample inputs.


RegEx Circuit

jex.im visualizes regular expressions:

enter image description here

Source

Strong Password Checker

\$\endgroup\$
  • 7
    \$\begingroup\$ Why did you write three different functions that all have the same docstring? What do apples have to do with passwords? Why does your code contain performance tests but neither correctness tests nor usefulness tests? Do you agree with the code that Password123 is a strong password, and if so, why? \$\endgroup\$ – Roland Illig Oct 23 '19 at 16:43
  • \$\begingroup\$ """Returns a boolean if a password is correct"""; if a password is correct? Bad description. And the function does always return a boolean. Also: why "if x return true else return false"? just "return x" (sorry for this short version of the review; I just reviewed the first function) \$\endgroup\$ – Kevin Meier Oct 23 '19 at 20:13
  • 1
    \$\begingroup\$ Btw: I guess to really check if a password is strong, you should use the Kolmgorov complexity ;-)! \$\endgroup\$ – Kevin Meier Oct 23 '19 at 20:17
  • \$\begingroup\$ ugh... I vehemently object to the LeetCode challenge for having a really crappy definition of "strong password". These are good guidelines.. Rules #1 and #2 actually make your passwords worse and less safe. \$\endgroup\$ – Gloweye Oct 25 '19 at 8:00
1
\$\begingroup\$

Imports

I notice you import re three times, once for each password check function. Why? Just import the library once at the top of the file, and you can use it in all of your functions.

Returning Booleans

Instead of returning True or False based on the result of the expression, simply return the expression. It results to a boolean anyway, so returning True and False is unnecessary; just return the expression.

DRY (Don't Repeat Yourself)

You have three functions that essentially perform the same task, just with very slightly different function calls. You can compress these regex checks into one method. Then you can pass what type of check you want to perform on the password. See below.

import re

def strong_password_check(password: str, type_of_check: str) -> bool:
    """
    Accepts a password and the type of check to perform on the password

    :param password: Password to check
    :param type_of_check: How to check the password. Has to be "match", "search", or "findall"

    """
    regex = r'^(?!.*([a-z])\1\1)(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[A-Za-z0-9]{6,20}$'
    if type_of_check == "match":
        return re.match(regex, password) is not None
    if type_of_check == "search":
        return re.search(regex, password) is not None
    if type_of_check == "findall":
        return re.findall(regex, password) != []

Test Cases

Not really sure the usefulness of red and green apples. Again, importing cProfile and timeit should go at the top of the program.

I see you're checking for performance, but not for the actual strength of the password. Would your program agree that Password123 is a good password? Any security professional worth his salt wouldn't agree.

I suggest that you use the Kolmgorov Complexity to check the strength of the password.

\$\endgroup\$

Your Answer

By clicking โ€œPost Your Answerโ€, you agree to our terms of service, privacy policy and cookie policy

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