18
\$\begingroup\$

I'm just a beginner in Python. I wrote a simple code that detects the user's password strength, but I feel that it isn't very efficient. How can I improve it?

print('''# This is Password Strength Detector.\n
-----------------------------------------------------------------''')
password = input('Enter your password here : ')
print('\n')

from re import *

lower_match = compile(r'[a-z]').findall(password)  # Finding if the password contains lowecase letters.
upper_match = compile(r'[A-Z]').findall(password)  # Finding if the password contains uppercase letters.
symbol_match = compile(r'[|\"|\'|~|!|@|#|$|%|^|&|*|(|)|_|=|+|\||,|.|/|?|:|;|[|]|{\}|<|>]').findall(
    password)  # Finding if the password contains special characters.
number_match = compile(r'[0-9]').findall(password)  # Finding if the password contains numbers.

if len(password) < 8 or len(lower_match) == 0 or len(upper_match) == 0 or len(symbol_match) == 0 or len(
        number_match) == 0:
    print('Your password is weak ! ')

elif len(password) >= 8:
    print('Your password is strong ! ')

elif len(password) >= 16:
    print('Your password is very strong ! ')
\$\endgroup\$
8
  • 2
    \$\begingroup\$ FWIW, password strength checkers are not reliable. While you are checking if the length is long that still leaves the password vulnerable to the "stupidly common password check" i.e, the password is "password". The best passwords are hard to guess but easy to remember, and don't necessarily have special characters in them. There's no real algorithmic way for you to check this other than maintaining a dictionary of known passwords yourself and checking the user isn't using one of those common words. \$\endgroup\$ – Dan Nov 6 '15 at 16:16
  • \$\begingroup\$ Not really on topic for code review, but as an aside the zxcvbn project is an attempt at a practical strength checker in Javascript. github.com/dropbox/zxcvbn \$\endgroup\$ – Alfred Armstrong Nov 6 '15 at 16:31
  • \$\begingroup\$ So you know, in English, you put a space after punctuation, unless it's a quote (' or ") and never before, unless it's an ellipsis () \$\endgroup\$ – Fund Monica's Lawsuit Nov 7 '15 at 20:44
  • \$\begingroup\$ I have rolled back the last edit. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Heslacher Nov 9 '15 at 12:14
  • 1
    \$\begingroup\$ @MahmoudNaguib please do not change the code in your question after some answers have been posted, as this could invalidate them. I have rolled back your most recent edit to the previous version. \$\endgroup\$ – Phrancis Nov 19 '15 at 16:13
32
\$\begingroup\$

The strength of a password has nothing to do with the presence of special characters! Obligatory xkcd reference:

To anyone who understands information theory and security and is in an infuriating argument with someone who does not (possibly involving mixed case), I sincerely apologize.

correct horse battery staple is a very strong password, but you would classify it as weak, simply because it doesn't contain capital letters, symbols, or numbers. Rethink the idea.

Testing

You will never indicate that a password is very strong because of this ordering:

elif len(password) >= 8:
    print('Your password is strong ! ')
elif len(password) >= 16:
    print('Your password is very strong ! ')

If len(password) is, say, 20, both branches are true. So you want to makes ure that the most restrictive one goes first:

elif len(password) >= 16:
    print('Your password is very strong ! ')
else:
    print('Your password is strong ! ')

Regex

The point of compile is if you reuse the same regex many times, you can make it more efficient. You use each one exactly once, so it's a waste of processing time. What you want to instead use is re.search(), since you don't care about all the instances you just want to know if there is such a thing:

lower_match = re.search(r'[a-z]', password)
upper_match = re.search(r'[A-Z]', password)
...

Then check against None instead of checking len(). Also your symbol check is inefficient. You have a bunch of |s, when you could simply list them all:

symbol_match = re.search(r'[\"\'~!@#$%^&\\*\(\)_=\+\|,./\?:;\[\]\{\}<>]', password)

Importing

from X import *

is highly frowned upon. Just do import re and use re.X everywhere.

\$\endgroup\$
6
  • 3
    \$\begingroup\$ I knew someone would post the xkcd comic eventually. I didn't expect it to be that fast. Nice answer btw :-) \$\endgroup\$ – SylvainD Nov 6 '15 at 16:14
  • 2
    \$\begingroup\$ The "strong password" bug shows that you don't have tested enough your code. You should have unit-tested the function with all possible cases. \$\endgroup\$ – Fabio F. Nov 6 '15 at 16:55
  • \$\begingroup\$ Wouldn't be symbol_match = not password.isalnum() better? Correct me if I'm wrong, but I think it does pretty much the same as the regexp, but is much more readable. \$\endgroup\$ – Alissa Nov 6 '15 at 17:43
  • 10
    \$\begingroup\$ "correct horse battery staple is a very strong password." Correction: was a very strong password until this comic came out :) \$\endgroup\$ – David says Reinstate Monica Nov 7 '15 at 1:25
  • 4
    \$\begingroup\$ @DavidGrinberg So you're telling me I should change my passwords? \$\endgroup\$ – Barry Nov 19 '15 at 16:04
14
\$\begingroup\$

Your password strength calculator is completely wrong. If I enter a 60 character lowercase sentence in there, your calculator will tell me it's a weak password, even though you won't be able to bruteforce it because of the sheer size.

The primary indicator of password strength is something called entropy. Basically, it indicates how random your password is. Entropy at its most basic form is "how many passwords could I make using your method?" and is measured in bits. It is calculated by the following formula:

entropy = number of bits in the binary representation of(the size of your charset)^(length of your password in chars)

the size of your charset is usually one of the following options:

  • 26 (only lowercase)
  • 36 (lowercase + digits)
  • 52 (uppercase and lowercase)
  • 62 (uppercase, lowercase and numbers)
  • 95 (uppercase, lowercase, numbers and symbols)

I say usually because some people don't use characters for their charset. Some people use a passphrase. And that's where the difficult part comes in. You see, a passphrase is basically a password where someone uses words from their native language for the charset. The English language currently has well over a million words, and many other languages have similarly exhaustive vocabularies. Even if you drop all the words longer than, say, 10 characters, you're still left with hundreds of thousands of words. If you have 500,000 words and choose 5 random words, you have 31 octillion (quadrilliard for those that use the -iard system) possibilities.

Because of this, you also need to account for passphrases when calculating your entropy. The hard part is that the algorithm that generates passphrases might be flawed. For example, if I have the password "apple aardvark acumen autumn aiming", that's 35 lowercase characters long. If you have a naive entropy calculator that doesn't account for passphrases, that's equal to an entropy a 3 followed by 49 zeroes. However, all the words start with the same letter a. there are only about 13,000 words that start with the letter a in English, and there are only 5 words. By the same entropy math, that's an entropy of 3 followed by 20 zeroes. that's a HUGE difference. If you go by "how many characters", your password is just about unbreakable for years to come. If you go by "how many words starting with the letter a", your password could probably be beaten in hours.

The big challenge with calculating the entropy of passphrases is that you can't define a dictionary size. For example, your passphrase might have 5 words with different starting letters, but they're all in the list of 1000 most common words, so it can be broken instantly. Or your passphrase has no spaces, which suddenly means your passphrase has only 4 words instead of 5 because you can interpret it differently, which has a HUGE effect on how strong your password is.

Entropy is rather hard to calculate with regards to passwords because of the above challenges. A good place to start is by calculating your entropy based on just the DULS character set: Digits, Upper, Lower, Symbols. Work out how big your character set size is based on those groups, then raise that to the power of the password length. The entropy is then the logarithm in base 2 of the number you get. You'll have to use your personal judgment on what entropy gives what grade. Here are some examples of how entropy relates to character length.:

  • 106 = 19 bits (a random number between 100,000 and 999,999, inclusive.)
  • 268 = 37 bits (8 lowercase characters, aka "password", although this is one of the first a hacker will try).
  • 958 = 52 bits (8 printable characters).
  • 2616 = 75 bits (16 lowercase characters).
  • 500,0005 = 94 bits (5 of the 500,000 most popular words, randomly chosen).
  • 9516 = 105 bits (16 printable characters).
  • 9530 = 197 bits (30 printable characters).
\$\endgroup\$
1
  • 2
    \$\begingroup\$ I just wanted to say, this is an awesome answer. \$\endgroup\$ – Kaz Nov 19 '15 at 16:25
3
\$\begingroup\$

i think measuring password strength should be done against both a pure brute force attack, and a dictionary-based attack.

so, for example, you could measure the brute-force permutations as:

character_set_size^character_count

to calculate character_set_size, i would add together the following:

  1. +26 if they use a lowercase letter
  2. +26 if they use an upper case letter besides the first letter
  3. +10 if they use a digit besides the last 2 characters
  4. +10 if they use a character other than english letters, numbers and a space in any position besides the last 2.

e.g. "Bagpipe2!" would only score 26 for characterset size, but "Bag24P!pe" would score a 72 (yeilding a permutation count of 9^72 ~= 5e68).

for a dictionary based attack, you would need to weight the word chosen by frequency (e.g. "the" has a lower score than "ewe"). so "theHouse" might score 2000^2, but "eweGrange" might score 20000^2. obviously, calculating a dictionary strength weight requires at least 1 dictionary, possibly including several languages, and some fancy huristics for splitting the word, accounting for numbers and calculating weight.

once you have both scores, you would need to take the lower score and compare it against some arbitrary threshold to determine "poor" vs "good" vs "super-duper"

\$\endgroup\$
7
  • \$\begingroup\$ as a side note, it helps to think of this problem the other way around. i remember writing a script once to run through all the variants of a passphrase i had partially forgotten. basically, i could remember the phrase, but i couldn't remember the uppercasing, punctuation and spelling. once i wrote the script it found the solution and decrypted my file after about 40k permutations. and that is assuming i knew all the words :) \$\endgroup\$ – james turner Nov 6 '15 at 19:38
  • \$\begingroup\$ Why do you ignore the last two characters when calculating character set size? \$\endgroup\$ – Fund Monica's Lawsuit Nov 7 '15 at 20:51
  • \$\begingroup\$ it is fairly common for people to add numbers or special characters to the end of their "normal" password simply to meet the rules for "valid passwords" in this system. since these users would never put those characters in any other position, it is not reasonable to pretend the character set is that wide in each postion. the opposite is also true, ie that user is unlikely to put an alphabetic character in the final 2 positions. \$\endgroup\$ – james turner Nov 9 '15 at 15:55
  • \$\begingroup\$ thinking about the rules for common passwords, perhaps you should simply lay out some guidelines on the password creation screen (e.g. must be 6-10 characters long, and contain a number). then you only score a password as "strong" if it breaks one of the guidelines :) \$\endgroup\$ – james turner Nov 9 '15 at 15:56
  • 1
    \$\begingroup\$ I fail to see how "people sometimes do something" equates to "ignore the entropy introduced by adding a random character to the end". \$\endgroup\$ – Fund Monica's Lawsuit Nov 9 '15 at 17:33

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.