• Home
  • About Me
    • So Says Kaushik photo

      So Says Kaushik

      I am Kaushik Nishtala. I love crunching numbers and playing with data.

    • Learn More
    • Email
    • LinkedIn
    • Github
    • Pinterest
  • Posts
    • All Posts
    • All Tags
  • Projects
  • Search

Filter passwords using NIST guidelines

15 Apr 2019

Reading time ~12 minutes


Data

This project uses a dataset which is available here.


1. The NIST Special Publication 800-63B

If you – 50 years ago – needed to come up with a secret password you were probably part of a secret espionage organization or (more likely) you were pretending to be a spy when playing as a kid. Today, many of us are forced to come up with new passwords all the time when signing into sites and apps. As a password inventeur it is your responsibility to come up with good, hard-to-crack passwords. But it is also in the interest of sites and apps to make sure that you use good passwords. The problem is that it's really hard to define what makes a good password. However, the National Institute of Standards and Technology (NIST) knows what the second best thing is: To make sure you're at least not using a bad password.

In this notebook, we will go through the rules in NIST Special Publication 800-63B which details what checks a verifier (what the NIST calls a second party responsible for storing and verifying passwords) should perform to make sure users don't pick bad passwords. We will go through the passwords of users from a fictional company and use python to flag the users with bad passwords. But us being able to do this already means the fictional company is breaking one of the rules of 800-63B:

Verifiers SHALL store memorized secrets in a form that is resistant to offline attacks. Memorized secrets SHALL be salted and hashed using a suitable one-way key derivation function.

That is, never save users' passwords in plaintext, always encrypt the passwords! Keeping this in mind for the next time we're building a password management system, let's load in the data.

Warning: The list of passwords and the fictional user database both contain real passwords leaked from real websites. These passwords have not been filtered in any way and include words that are explicit, derogatory and offensive.

# Importing the pandas module
import pandas as pd
# Loading in datasets/users.csv
users = pd.read_csv('datasets/users.csv')

# Printing out how many users we've got
print(len(users))
# Taking a look at the 12 first users
users.head(12)
982
id user_name password
0 1 vance.jennings joobheco
1 2 consuelo.eaton 0869347314
2 3 mitchel.perkins fabypotter
3 4 odessa.vaughan aharney88
4 5 araceli.wilder acecdn3000
5 6 shawn.harrington 5278049
6 7 evelyn.gay master
7 8 noreen.hale murphy
8 9 gladys.ward lwsves2
9 10 brant.zimmerman 1190KAREN5572497
10 11 leanna.abbott aivlys24
11 12 milford.hubbard hubbard

2. Passwords should not be too short

If we take a look at the first 12 users above we already see some bad passwords. But let's not get ahead of ourselves and start flagging passwords manually. What is the first thing we should check according to the NIST Special Publication 800-63B?

Verifiers SHALL require subscriber-chosen memorized secrets to be at least 8 characters in length.

Ok, so the passwords of our users shouldn't be too short. Let's start by checking that!

# Calculating the lengths of users' passwords
users['length'] = users['password'].str.len()

# Flagging the users with too short passwords
users['too_short'] = users['length'] < 8

# Counting and printing the number of users with too short passwords
print(users['too_short'].sum())
# Taking a look at the 12 first rows
users.head(12)
376
id user_name password length too_short
0 1 vance.jennings joobheco 8 False
1 2 consuelo.eaton 0869347314 10 False
2 3 mitchel.perkins fabypotter 10 False
3 4 odessa.vaughan aharney88 9 False
4 5 araceli.wilder acecdn3000 10 False
5 6 shawn.harrington 5278049 7 True
6 7 evelyn.gay master 6 True
7 8 noreen.hale murphy 6 True
8 9 gladys.ward lwsves2 7 True
9 10 brant.zimmerman 1190KAREN5572497 16 False
10 11 leanna.abbott aivlys24 8 False
11 12 milford.hubbard hubbard 7 True

3. Common passwords people use

Already this simple rule flagged a couple of offenders among the first 12 users. Next up in Special Publication 800-63B is the rule that

verifiers SHALL compare the prospective secrets against a list that contains values known to be commonly-used, expected, or compromised.

  • Passwords obtained from previous breach corpuses.
  • Dictionary words.
  • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).
  • Context-specific words, such as the name of the service, the username, and derivatives thereof.

We're going to check these in order and start with Passwords obtained from previous breach corpuses, that is, websites where hackers have leaked all the users' passwords. As many websites don't follow the NIST guidelines and encrypt passwords there now exist large lists of the most popular passwords. Let's start by loading in the 10,000 most common passwords which I've taken from here.

# Reading in the top 10000 passwords
common_passwords = pd.read_csv('datasets/10_million_password_list_top_10000.txt',header=None,squeeze=True)

# Taking a look at the top 20
common_passwords.head(20)
0        123456
1      password
2      12345678
3        qwerty
4     123456789
5         12345
6          1234
7        111111
8       1234567
9        dragon
10       123123
11     baseball
12       abc123
13     football
14       monkey
15      letmein
16       696969
17       shadow
18       master
19       666666
Name: 0, dtype: object

4. Passwords should not be common passwords

The list of passwords was ordered, with the most common passwords first, and so we shouldn't be surprised to see passwords like 123456 and qwerty above. As hackers also have access to this list of common passwords, it's important that none of our users use these passwords!

Let's flag all the passwords in our user database that are among the top 10,000 used passwords.

# Flagging the users with passwords that are common passwords
users['common_password'] = users['password'].isin(common_passwords[:10000])

# Counting and printing the number of users using common passwords
print(users['common_password'].sum())
# Taking a look at the 12 first rows
users.head(12)
129
id user_name password length too_short common_password
0 1 vance.jennings joobheco 8 False False
1 2 consuelo.eaton 0869347314 10 False False
2 3 mitchel.perkins fabypotter 10 False False
3 4 odessa.vaughan aharney88 9 False False
4 5 araceli.wilder acecdn3000 10 False False
5 6 shawn.harrington 5278049 7 True False
6 7 evelyn.gay master 6 True True
7 8 noreen.hale murphy 6 True True
8 9 gladys.ward lwsves2 7 True False
9 10 brant.zimmerman 1190KAREN5572497 16 False False
10 11 leanna.abbott aivlys24 8 False False
11 12 milford.hubbard hubbard 7 True False

5. Passwords should not be common words

Ay ay ay! It turns out many of our users use common passwords, and of the first 12 users there are already two. However, as most common passwords also tend to be short, they were already flagged as being too short. What is the next thing we should check?

Verifiers SHALL compare the prospective secrets against a list that contains [...] dictionary words.

This follows the same logic as before: It is easy for hackers to check users' passwords against common English words and therefore common English words make bad passwords. Let's check our users' passwords against the top 10,000 English words from Google's Trillion Word Corpus.

# Reading in a list of the 10000 most common words
words = pd.read_csv('datasets/google-10000-english.txt',header=None,squeeze=True)

# Flagging the users with passwords that are common words
users['common_word'] = users['password'].str.lower().isin(words)

# Counting and printing the number of users using common words as passwords
print(users['common_word'].sum())
# Taking a look at the 12 first rows
users.head(12)
137
id user_name password length too_short common_password common_word
0 1 vance.jennings joobheco 8 False False False
1 2 consuelo.eaton 0869347314 10 False False False
2 3 mitchel.perkins fabypotter 10 False False False
3 4 odessa.vaughan aharney88 9 False False False
4 5 araceli.wilder acecdn3000 10 False False False
5 6 shawn.harrington 5278049 7 True False False
6 7 evelyn.gay master 6 True True True
7 8 noreen.hale murphy 6 True True True
8 9 gladys.ward lwsves2 7 True False False
9 10 brant.zimmerman 1190KAREN5572497 16 False False False
10 11 leanna.abbott aivlys24 8 False False False
11 12 milford.hubbard hubbard 7 True False False

6. Passwords should not be your name

It turns out many of our passwords were common English words too! Next up on the NIST list:

Verifiers SHALL compare the prospective secrets against a list that contains [...] context-specific words, such as the name of the service, the username, and derivatives thereof.

Ok, so there are many things we could check here. One thing to notice is that our users' usernames consist of their first names and last names separated by a dot. For now, let's just flag passwords that are the same as either a user's first or last name.

# Extracting first and last names into their own columns
users['first_name'] = users['user_name'].str.extract(r'(^\w+)', expand = False)
users['last_name'] = users['user_name'].str.extract(r'(\w+$)', expand = False)
#users['first_name'] = users['user_name'].str.split('.').str.get(0)
#users['last_name'] = users['user_name'].str.split('.').str.get(1)

# Flagging the users with passwords that matches their names
users['uses_name'] = ((users['password'] == users['first_name']) | (users['password'] == users['last_name']))

# Counting and printing the number of users using names as passwords
print(users['uses_name'].sum())

# Taking a look at the 12 first rows
users.head(12)
50
id user_name password length too_short common_password common_word first_name last_name uses_name
0 1 vance.jennings joobheco 8 False False False vance jennings False
1 2 consuelo.eaton 0869347314 10 False False False consuelo eaton False
2 3 mitchel.perkins fabypotter 10 False False False mitchel perkins False
3 4 odessa.vaughan aharney88 9 False False False odessa vaughan False
4 5 araceli.wilder acecdn3000 10 False False False araceli wilder False
5 6 shawn.harrington 5278049 7 True False False shawn harrington False
6 7 evelyn.gay master 6 True True True evelyn gay False
7 8 noreen.hale murphy 6 True True True noreen hale False
8 9 gladys.ward lwsves2 7 True False False gladys ward False
9 10 brant.zimmerman 1190KAREN5572497 16 False False False brant zimmerman False
10 11 leanna.abbott aivlys24 8 False False False leanna abbott False
11 12 milford.hubbard hubbard 7 True False False milford hubbard True

7. Passwords should not be repetitive

Milford Hubbard (user number 12 above), what where you thinking!? Ok, so the last thing we are going to check is a bit tricky:

verifiers SHALL compare the prospective secrets [so that they don't contain] repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).

This is tricky to check because what is repetitive is hard to define. Is 11111 repetitive? Yes! Is 12345 repetitive? Well, kind of. Is 13579 repetitive? Maybe not..? To check for repetitiveness can be arbitrarily complex, but here we're only going to do something simple. We're going to flag all passwords that contain 4 or more repeated characters.

### Flagging the users with passwords with >= 4 repeats
#users['too_many_repeats'] = users['password'].str.contains(r'(.)\1{3,}')
users['too_many_repeats'] = users['password'].str.contains(r'(.)\1\1\1')

# Taking a look at the users with too many repeats
users.query('too_many_repeats == True')
id user_name password length too_short common_password common_word first_name last_name uses_name too_many_repeats
146 147 patti.dixon 555555 6 True True False patti dixon False True
572 573 cornelia.bradley 555555 6 True True False cornelia bradley False True
644 645 essie.lopez 11111 5 True True False essie lopez False True
798 799 charley.key 888888 6 True True False charley key False True
807 808 thurman.osborne rinnnng0 8 False False False thurman osborne False True
941 942 mitch.ferguson aaaaaa 6 True True False mitch ferguson False True

8. All together now!

Now we have implemented all the basic tests for bad passwords suggested by NIST Special Publication 800-63B! What's left is just to flag all bad passwords and maybe to send these users an e-mail that strongly suggests they change their password.

# Flagging all passwords that are bad
#users['bad_password'] = users[['too_short','common_password','common_word','uses_name','too_many_repeats']].sum(axis=1).astype('bool')
users['bad_password'] = users['too_short'] | users['common_password'] | users['common_word'] | users['uses_name'] | users['too_many_repeats']
# Counting and printing the number of bad passwords
print(users['bad_password'].sum())
# Looking at the first 25 bad passwords
users.query('bad_password == True').head(25)
424
id user_name password length too_short common_password common_word first_name last_name uses_name too_many_repeats bad_password
5 6 shawn.harrington 5278049 7 True False False shawn harrington False False True
6 7 evelyn.gay master 6 True True True evelyn gay False False True
7 8 noreen.hale murphy 6 True True True noreen hale False False True
8 9 gladys.ward lwsves2 7 True False False gladys ward False False True
11 12 milford.hubbard hubbard 7 True False False milford hubbard True False True
13 14 jamie.cochran 310356 6 True False False jamie cochran False False True
15 16 lorrie.gay oZ4k0QE 7 True False False lorrie gay False False True
16 17 domingo.dyer chelsea 7 True True True domingo dyer False False True
17 18 martin.pacheco zvc1939 7 True False False martin pacheco False False True
18 19 shelby.massey nickgd 6 True False False shelby massey False False True
21 22 leticia.sanford cocacola 8 False True False leticia sanford False False True
22 23 jenny.woodard woodard 7 True False False jenny woodard True False True
25 26 dianna.munoz AJ9Da 5 True False False dianna munoz False False True
26 27 julia.savage ewokzs 6 True False False julia savage False False True
28 29 joaquin.walters YyGjz8E 7 True False False joaquin walters False False True
30 31 rosanna.reid reid 4 True False True rosanna reid True False True
34 35 roger.golden jOYZBs8 7 True False False roger golden False False True
38 39 gus.padilla wwewwf1 7 True False False gus padilla False False True
43 44 vito.nicholson 225377 6 True False False vito nicholson False False True
45 46 morris.price NdZ7E6 6 True False False morris price False False True
47 48 jarred.white CQB3Z 5 True False False jarred white False False True
48 49 junior.burch diffo 5 True False False junior burch False False True
51 52 ahmad.hopper 123456789 9 False True False ahmad hopper False False True
52 53 rosario.merrill y8uM7D6 7 True False False rosario merrill False False True
56 57 dwayne.hurst mikeloo 7 True False False dwayne hurst False False True

9. Otherwise, the password should be up to the user

In this notebook, we've implemented the password checks recommended by the NIST Special Publication 800-63B. It's certainly possible to better implement these checks, for example, by using a longer list of common passwords. Also note that the NIST checks in no way guarantee that a chosen password is good, just that it's not obviously bad.

Apart from the checks we've implemented above the NIST is also clear with what password rules should not be imposed:

Verifiers SHOULD NOT impose other composition rules (e.g., requiring mixtures of different character types or prohibiting consecutively repeated characters) for memorized secrets. Verifiers SHOULD NOT require memorized secrets to be changed arbitrarily (e.g., periodically).

So the next time a website or app tells you to "include both a number, symbol and an upper and lower case character in your password" you should send them a copy of NIST Special Publication 800-63B.

# Enter a password that passes the NIST requirements
# PLEASE DO NOT USE AN EXISTING PASSWORD HERE
new_password = "qwerty123"

Further Reading

  • You can read the full NIST Special Publication 800-63B online.
  • This blog post also gives you a summary of 800-63B.
  • Here is an article explaining where the 10,000 common passwords you used in this project come from.
  • Finally, some advice on how to come up with a good password.

Dig deeper?

You can find out more about this project at Github.



Passion ProjectProjectpythonpandasCleaning DataTensorflowNatural Language ProcessingNLPNational Institute of Standards and Technologyregex Share Tweet +1