Today we're going to build on the login program that we started yesterday. The program we finished yesterday was functional, but was missing some security features that we'd expect from a modern program. In this lab we're going to add additional security features to our login.
Modern systems often mask passwords as they are being entered to discourage shoulder surfing. Some systems do this by only showing * characters instead of the actual password characters. Other systems do this by simply not showing any characters on the screen while you are typing your password. We're going to add the display no characters method to our program.
Start by replacing the second line of your program so it matches the following:
1 | # GenCyber Login Program
2 | import getpass
3 |
4 | # Password "database".
Line 2 tells Python to import a library called getpass. Libraries are prebuilt functions that we can reuse in our own programs. This allows us to quickly add in functionality without having to program it from scratch.
As you might have guessed, the getpass library is a library that is designed to get masked passwords. How convenient.
Now we just have to use the getpass library to collect a masked password. You can do that by updating line 17 of your program to match the following:
15 | print("\n\n=========Login Page===========")
16 | username = input("Username: ")
17 | password = getpass.getpass()
18 |
Line 17 replaced the input() command with the getpass() command which help collect the password more securely.
Ok, now that we've added masked passwords to our program it is time to run to try it out and make sure we haven't introduced any errors.
Press the "Run Python File" button in the upper-right corner of the IDE or use the Ctrl + F5 shortcut to run the program then check the Terminal panel (Ctrl + `) to make sure we see the expected output.
In the terminal panel you should once again see the following output:
=========Login Page===========
Username:
Go ahead and enter a username and press the Enter key. You should once again see the prompt to enter a password. This time when you start to type your password should see nothing happen! You just have to confidently type in the password and press the Enter key. Once you press enter the screen should update and either tell you that there was a problem or that you've successfully logged in.
It would probably be a good idea to run the program a couple of times and make sure all the expected behavior still works. Try to log in successfully, with an unknown user, and with a bad password to make sure you sell all three possible results.
Another common login security feature is limiting the number of attempts that can be made in a given period of time. This prevents a brute force attack where an attacker tries to gain entry by trying out lots and lots of different passwords hoping to stumble on one that works.
We're going to limit the number of guesses to three within a 15 minute period.
To do this we're going have to add a new library and some new variables that we'll use to track login attempts. The following listing is an updated version of your current program. You'll need to insert line 3 and lines 11 through 16 into your program. When you're done the first 18 lines should match the following:
1 | # GenCyber Login Program
2 | import getpass
3 | import datetime
4 |
5 | # Password "database".
6 | password_db = {"Bill" : "Clawhammer",
7 | "Jesse" : "Mitsu",
8 | "Steven" : "Muddy",
9 | "Ashley" : "Shadow"}
10 |
11 | # Keep track of login attempts.
12 | attempt_list = []
13 |
14 | # Keep track if too many login attempts.
15 | is_too_many_attempts = False
16 |
17 | # Keep track of if we should keep looping.
18 | loop_again = True
Explaination: Line 3 imports a libary call datetime. This library is going to let us access the current computer time so we can save timestamps when someone attempts to login.
Line 11 introduces a list variable that we'll use to store login attempt timestamps in. By checking the timestamps we'll be able to tell if someone has tried to login 3 times within 15 minutes.
Line 14 introduces a variable we'll use to track if someone has tried to login too many times. We set the initial value of the variable to False. If we detect too many logins we'll change that to True so the program can react.
Now that our program has variables to store login attempts we need to add the logic to populate those variables and react when someone try to login too many times. The following listing updates the end of your program to add in that logic.
Lines 35 through 37 should already exist in your program, so we'll be adding lines 39 through 67. Be sure to pay attention to how indented each line is supposed to be. Lines 39 through 41 should be as indented as 35 through 37. You can use the tab key to quickly indent a line. When you're done the end of your program should match the following:
35 | elif correct_password == password:
36 | # If the password match stop looping and log in user.
37 | loop_again = False
38 |
39 | # If we're going to loop again check to see if there have been
40 | # too many attempts.
41 | if loop_again:
42 | if len(attempt_list) == 3:
43 | # If list has three items remove the first.
44 | attempt_list.pop(0)
45 |
46 | # Add current time to the end of the attempt list.
47 | attempt_list.append(datetime.datetime.now())
48 |
49 | # Check if there are three attempts in the list.
50 | if len(attempt_list) == 3:
51 | # Calculate time between first and third attempt.
52 | delta = attempt_list[2] - attempt_list[0]
53 |
54 | # Check if the time between first and third attempt
55 | # is less than 15 minutes.
56 | if delta.total_seconds() <= (15 * 60):
57 | # Set too many attempts to true.
58 | is_too_many_attempts = True
59 | # Set loop again to false.
60 | loop_again = False
61 |
62 | # Check if there have been too many attempts.
63 | if is_too_many_attempts:
64 | print("Too many login attempts")
65 | else :
66 | # If we get this far we have logged in successfully.
67 | print("Congratulations, you are logged in.")
Explanation: Line 41 adds a new if statement block that contains the bulk of our new logic. If the program is going to loop again, this if statement block checks to see if there have been too many login attempts.
Lines 42 through 44 add another if statement block. This one checks to see if there are 3 timestamps in our timestamp list. If there are, it removes the earliest to to make room for a new one.
Lines 46 through 47 use the datetime library to get the current time from the computer and store it in the attempt_list variable.
Lines 49 through 60 is where we actually check to see if there have been too many login attempts. Line 50 adds an if statement block that checks to see if we have three timestamps in our list of timestamps. If we do, then line 52 calculates the difference between the first and third timestamp and save that value in a variable named delta.
Line 56 is an if statement block that checks to see if the value stored in delta is less than 15 minutes. It does this be calculating the number of seconds in 15 minutes (15 * 60). If the difference between the first and third attempt are less than 15 minutes line 58 sets the is_too_many_attempts variable to True and line 60 tells the program to stop looping.
Line 62 through 67 is the part of the program that runs after it stops looping to provide login attempts. Line 63 checks to see if the is_too_many_attempts variable is set to True. If it is, then the program display a message on line 64 telling the users "Too many login attempts". Otherwise, line 67 congratulates the user on logging in.
We just added a bunch of new code to our program. What is the chance that you were able to transcribe all of it without introducing a bug? Only one way to find out!
Press the "Run Python File" button in the upper-right corner of the IDE or use the Ctrl + F5 shortcut to run the program then check the Terminal panel (Ctrl + `) to make sure we see the expected output.
If there are errors review your code to see if you can spot the problem. If you can't ask for help.
If everything is working correctly the terminal panel should once again display the following output:
=========Login Page===========
Username:
Test your code out by entering three wrong passwords to make sure the program locks you out after three attempts. Once you are locked out you'll have to run the program again to make another attempt.
Also, you'll want to make sure the program still accepts the correct password. So test that out.
Lastly, how do we test out the 15 minutes timespan for login attempts? One approach would be to enter two incorrect passwords and then wait for 16 minutes to make a third incorrect attempt. The program should not lock you out if you have waited 15 minutes to make a third attempt.
However, if you want a quicker way to test the program you can modify line 56. On line 56 it calculates the amount of seconds in 15 minutes. If you change (15 * 60) to (1 * 60) it will change the 15 minute window for making a third attempt to 1 minute. Now you can enter two incorrect login attempts, wait a minute, and then make a third attempt to make sure it doesn't lock you out. After you verify the logic works you can change line 56 back to (15 * 60).
One of the things that our login program does that a modern program probably wouldn't do is store and compare plaintext passwords. This is inherently dangerous because if the password database was compromised, the attacker could read everyone's password.
The common solution to this problem is to hash the passwords. Hashing is a technology that converts a plaintext password into a different string of characters that can still be compared, but the attacker won't know what the original correct password was.
To do this we need to add a library called bcrypt to your program. However, the bcrypt Python library isn't on your computer so we'll need to add it first.
We can add additional Python libraries using the command prompt. In the search box next to the Start button on your computer enter the following command:
cmd
This should open a Command Prompt window. Then in the Command Prompt window enter the following command:
python -m pip install bcrypt
Pip is a program known as a package manager. A package manager installs and manages software packages for other programs. Pip is actually written in Python and is the package manager that the Python Software Foundation recommends for installing new packages in Python. The command you entered tells Pip to go out and find the bcrypt package and install it on your copy of Python.
You should see some output that the bcrypt package is installed.
Now that we have the bcrypt library installed we're going to modify our login program to use it. We need to insert line 4 and lines 12 through 14 into our code. Modify the start of your program so it matches the lines below:
1 | # GenCyber Login Program
2 | import getpass
3 | import datetime
4 | import bcrypt
5 |
6 | # Password "database".
7 | password_db = {"Bill" : "Clawhammer",
8 | "Jesse" : "Mitsu",
9 | "Steven" : "Muddy",
10 | "Ashley" : "Shadow"}
11 |
12 | # Hash all of the passwords in the database.
13 | for key, value in password_db.items():
14 | password_db[key] = bcrypt.hashpw(value.encode(), bcrypt.gensalt())
15 |
16 | # Keep track of login attempts.
17 | attempt_list = []
Explanation: Line 4 imports the bcrypt library so that we can use it in our program.
Lines 13 through 14 generates hashed values for all of the passwords we've stored in our password_db dictionary. Now typically we wouldn't want to hardcode the plaintext version of the passwords and then hash them because anyone who could view our source code could just read the passwords. So this is suboptimal from a security perspective. At least this way the passwords are hashed while they are sitting in the computers memory.
Now that our password database is hashed will need to modify our code so that the password the user enters can be compared against the saved hashes. Lines 34 through 36 and lines 46 through 48 are already in your program. Lines 37 through 44 will replace some existing lines in your code. Be sure to pay attention to how indented the new lines are. Modify your code so it matches the lines below:
34 | if correct_password == None:
35 | # If correct_password is None that means user was not found.
36 | print("User Not Found!")
37 | else:
38 | # Compare password to the hashed password database.
39 | if bcrypt.checkpw(password.encode(), correct_password):
40 | # If the password match stop looping and log in user.
41 | loop_again = False
42 | else:
43 | # If pasword do not match then password is wrong.
44 | print("Invalid Password")
45 |
46 | # If we're going to loop again check to see if there have been
47 | # too many attempts.
48 | if loop_again:
Explanation: This section ends up restructuring the if statement block that checks if the user's password is correct or not. Line 39 uses the bcrypt library to check if the password the user entered matches the hashed version of the password we've saved in correct_password. If they match line 41 sets the loop_again variable to False so don't prompt the user to try again. If they do not match line 44 displays the invalid password message to the user.
You know the drill by now. If we modify our code we need to test it to make sure it still works as expected. If we've accidently introduced bugs we need to fix them before moving on.
Press the "Run Python File" button in the upper-right corner of the IDE or use the Ctrl + F5 shortcut to run the program then check the Terminal panel (Ctrl + `) to make sure we see the expected output.
If there are errors review your code to see if you can spot the problem. If you can't ask for help.
If everything is working correctly the terminal panel should once again display the following output:
=========Login Page===========
Username:
Test your code out make sure it still accepts the correct password and still rejects bad passwords.
Are you curious what the hashed value of the password looks like? Since we're using our code to has the passwords it's not difficult to simply print out the hashed values so that we can inspect them.
If we insert lines 15 through 18 into our program we can have it print out the hashed values of our users passwords. Modify your program to match the listing below. Lines 12 though 14 and lines 21 through 22 are already in your code.
12 | # Hash all of the passwords in the database.
13 | for key, value in password_db.items():
14 | password_db[key] = bcrypt.hashpw(value.encode(), bcrypt.gensalt())
15 |
16 | print(password_db["Bill"])
17 | print(password_db["Jesse"])
18 | print(password_db["Steven"])
19 | print(password_db["Ashley"])
20 |
21 | # Keep track of login attempts.
22 | attempt_list = []
Explanation: Lines 16 though 19 simply prints out the hashed version of the password that we have saved into our password_db dictionary. If you run your program you should now see 4 lines of garblygook before your login page prompt that look something like this:
b'$2b$12$dnQQ.FIoaQyeI7JXRyTUxeDxrR6KjpktT25g75Hs0gPD6Dutcq4Vq'
b'$2b$12$A6VdBBZ196ejfUa1/1/v7u/YVCCHw1txXTCbsSjYSdhnXQjfhq1H2'
b'$2b$12$ceNrTnvEbLgTKuR7/e/8vuTUdXNimcpw7oRPWyLSwxTfplN82UxmS'
b'$2b$12$iooJxNTTdU77rwJF6SbqH.HHzLjq013lsSvb5qEBK3nELaQhHny4u'
=========Login Page===========
Username:
The first line is the hashed version of Bill's password Clawhammer.
The first line is the hashed version of Jesse's password Matsu.
The first line is the hashed version of Steven's password Muddy.
The first line is the hashed version of Ashley's password Shadow.
Notice that even though everyone's passwords are different lengths the hashed version are all the same length.
Remember earlier when we said that storing plaintext passwords in our source code was a bad idea. Now that we can see the hashed versions of our passwords, we could just build the password database with the hashed values so the plaintext password is never saved in the DB.
The following code replaces the plaintext passwords with hashed values. It's probably best if you copy-and-paste the hashed values and don't try to transcribe them. Also, even though line 7 though 10 are shown spanning two lines in the code listing, they are a single line in VS Code.
1 | # GenCyber Login Program
2 | import getpass
3 | import datetime
4 | import bcrypt
5 |
6 | # Password "database".
7 | password_db = {"Bill" :
| b'$2b$12$5LDl.CSlUuvwNuK13V5HLO4ttZ/Vn99W5m0pbSQvUl4f7ydtTnbmy',
8 | "Jesse" :
| b'$2b$12$rJ32ZU6Z6aeFBTOrR1RHh.vRq.ul5udl/tAZgcGkz9lu.q3YJbl5O',
9 | "Steven" :
| b'$2b$12$3HttUIKLbVne3JEdWPmSPeDvbcgolhdiaHIq6e1gWURgNRxj.8c1G',
10 | "Ashley" :
| b'$2b$12$NIfL873dQaXDEZMErzHFs.SIpVXyHZTTKVPbA4ySTvYGqtNhav86G'}
11 |
12 | # Keep track of login attempts.
13 | attempt_list = []
Explanation: This simply populates our password database with hashed passwords. Notice that we've also deleted the lines that we were using to hash the passwords because we don't need to do that anymore since our passwords are pre-hashed.
One last test of the program to make sure everything still works.
Press the "Run Python File" button in the upper-right corner of the IDE or use the Ctrl + F5 shortcut to run the program then check the Terminal panel (Ctrl + `) to make sure we see the expected output.
If there are errors review your code to see if you can spot the problem. If you can't ask for help.
If everything is working correctly the terminal panel should once again display the following output:
=========Login Page===========
Username:
Test your code out make sure it still accepts the correct password and still rejects bad passwords.
In this lab we started with a simple login program that worked, but had multiple security problems. By adding features like masking the password, limiting unsuccessful login attempts, and hashing the password database we made the program more secure.
Along the way we kept testing our code as we made modifications to make sure it still worked as expected.
Can you think of a GenCyber concept that would relate to adding multiple security features to our program?