Today we're going to think about the security of our login application and improve it.
If you're writing software for the DoD you have to follow Secure Technical Implementation Guides (STIG). These guides help ensure that you're building good security practice into your software.
We're going to make some improvements to our login app so that it meets some of the STIG requirements for application login screens.
Open up Quorum Studio. The application you were working on yesterday should still be loaded.
If you didn't finish the lab yesterday, you can catch up by copy-and-pasting the code listing below into your Main.quorum file. To catch up, use this code to replace the contents of your current LoginBehavior.quorum file.
Code listing:
use Libraries.Interface.Forms.FormBehavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Forms.Page
use Libraries.Interface.Controls.TextField
class LoginBehavior is FormBehavior
text myUsername = "Luggage"
text myPassword = "1234"
// Action triggered by button press.
action Run(BehaviorEvent event)
Page page = GetPage()
TextField usernameField = page:GetTextField("Username")
TextField passwordField = page:GetTextField("Password")
boolean userCorrect = false
boolean passwordCorrect = false
// Gather username info.
text user = usernameField:GetText()
if myUsername = user
userCorrect = true
end
// Gather password info.
text pass = passwordField:GetText()
if myPassword = pass
passwordCorrect = true
end
// Check to see if user can log in.
if userCorrect and passwordCorrect
output "Logged in as: " + usernameField:GetText()
else
output "Could not log you in. Did you forget your password?"
end
end
end
One of the problems with our login interface is that it shows the password in clear text as you enter it. Good security practice is to mask the password by showing dots or asterisks so that shoulder surfers can't steal your password. The Application Security and Development STIG includes a check, V-222554, to make sure that all passwords are masked.
Fortunately, the Quorum language makes it easy to mask the contents of TextFields by letting the program know it will be used for passwords. The documentation for this action is available at this link.
To apply the password mask make sure you've selected the Main.quorum file in Quorum Studio and look the line of code that creates your password TextField. It should be on line 13 and it looks like this:
TextField password = page:AddTextField("Password")
After that line add this new line:
password:SetPassword(true)
This line tells the program that it should treat the TextField named password as a password field. Then Quorum will automatically start masking the TextField.
Your complete code listing for Main.quorum should now look like this:
use Libraries.Interface.Forms.Form
use Libraries.Interface.Forms.Page
use Libraries.Interface.Controls.TextField
use Libraries.Interface.Controls.Button
// Setup Login Form.
Form form
Page page = form:GetMainPage()
page:AddBanner("Secret CIA Server of Doom")
page:AddLabel("Username")
TextField username = page:AddTextField("Username")
page:AddLabel("Password")
TextField password = page:AddTextField("Password")
password:SetPassword(true)
// Add button to Form.
Button button = page:AddButton("Login")
LoginBehavior behavior
button:SetBehavior(behavior)
form:Display()
To run your program you can either click the green play button in the main toolbar over the editor window or press ctrl+r. You know the drill by now.
The app should look the same as it did yesterday. However, as you start to type the password in the Password TextField instead of seeing characters you'll now see an asterisk for each character you type.
The STIG check for password masking, V-222554, actually describes the check an auditor will do to make sure the mask also prevents the copy-and-pasting of a masked password. Select the masked characters in your Password TextField and copy them using ctrl-c. Now, try and paste them into a new document somewhere else. You should not be able to steal the password from your masked TextField using copy-and-paste.
Right now, our application is storing the correct password in the source code. You can see it on line 8 of the LoginBehavior.quorum file. This is an obvious security problem because if anyone got read-access to the source code they would be able to impersonate a valid user. The Application Security and Development STIG has a check, V-222542, which mandates that only cryptologic representation of passwords may be stored. To meet this check our application should store the hash of the password instead of the actual clear text password.
Currently, the password for this application is "1234", which we know is not a very secure password. The hash value of 1234 is:
$2y$10$zJETA0lA8XImNzWetBRxGumKgOMte8BmwX7Iu43.c4HUoPcxyknCq
Where did that hash come from? We can write a simple Quorum console application to compute it for us. If you'd like to do that yourself the instructions are right here.
Navigate to your Login app's LoginBehavior.quorum file.
Currently, we set the correct password value for our app on line 8. It looks like this:
text myPassword = "1234"
Update the line so that it uses the hash value and looks like this instead:
text myPassword = "$2y$10$zJETA0lA8XImNzWetBRxGumKgOMte8BmwX7Iu43.c4HUoPcxyknCq"
(The website might show this on two lines, but this should be a single line in your Quorum editor.)
From a security perspective, this is a big improvement because we are no longer storing the clear-text value of our password in our source code. However, it does create a problem. Currently our program is expecting to compare the password the user enters against the clear-text version of the password. Now that we're storing the hashed value of the password, that comparison will always fail. We need to update the logic where we check if the password is correct to expect a hashed value.
To do that we're going to need to add a new library to our application. The first four lines of your LoginBehavior.quorum file should look like this:
use Libraries.Interface.Forms.FormBehavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Forms.Page
use Libraries.Interface.Controls.TextField
Insert the following library as the new line 5:
use Libraries.Network.NetworkExchange
This library contains an action that we can use to compare hashed passwords.
Next, we need to update the logic that compares the password that the user entered against what is stored in the source code. The section of code that does that is on lines 26-30 and looks like this:
// Gather password info.
text pass = passwordField:GetText()
if myPassword = pass
passwordCorrect = true
end
Update this section of code so instead it looks like this:
// Gather password info.
text pass = passwordField:GetText()
NetworkExchange exchange
passwordCorrect = exchange:VerifyPassword(pass, myPassword)
This replaces the conditional if statement that we were using to compare the clear-text versions of the password with a special action from the library that we just added that can compare hashed passwords.
After all of the changes that allow our application to store and compare hashed passwords the code listing should look like this:
use Libraries.Interface.Forms.FormBehavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Forms.Page
use Libraries.Interface.Controls.TextField
use Libraries.Network.NetworkExchange
class LoginBehavior is FormBehavior
text myUsername = "Luggage"
text myPassword = "$2y$10$zJETA0lA8XImNzWetBRxGumKgOMte8BmwX7Iu43.c4HUoPcxyknCq"
// Action triggered by button press.
action Run(BehaviorEvent event)
Page page = GetPage()
TextField usernameField = page:GetTextField("Username")
TextField passwordField = page:GetTextField("Password")
boolean userCorrect = false
boolean passwordCorrect = false
// Gather username info.
text user = usernameField:GetText()
if myUsername = user
userCorrect = true
end
// Gather password info.
text pass = passwordField:GetText()
NetworkExchange exchange
passwordCorrect = exchange:VerifyPassword(pass, myPassword)
// Check to see if user can log in.
if userCorrect and passwordCorrect
output "Logged in as: " + usernameField:GetText()
else
output "Could not log you in. Did you forget your password?"
end
end
end
Ok, let's test the app to make sure it still works as expected. Either click the green play button in the main toolbar over the editor window or press ctrl+r.
Make sure your app built successfully by checking the output in palette in the bottom of the screen or by pressing ctrl+3. If your form application has any errors, the error information will be shown in the error tab of this palette. You'll need to fix the errors before you can continue with running your app.
If your app compiles correctly then a new window should appear. Try entering random text into the username and password TextFields and press the Login button. Go back and check the output in the Console tab of Quorum Studio. You should see the message "Could not log you in. Did you forget your password?"
Now try entering the correct username and password into your app and press the Login button. As a reminder, the correct username is "Luggage" and the password is "1234".
Go back and check the output message in the Console tab of Quorum Studio. You should see the message "Logged in as: Luggage".
Congratulation, your application now stores the cryptological representation of the password instead of the actual password and would pass STIG check V-222542.
The V-222432 STIG check stipulates that apps must limit the number of invalid login attempts to 3 within a 15 minute window. To do that we need to start recording the time when someone makes an invalid attempt and keep track of the number of attempts they make.
To record the logon attempt time we're going to need to add a new library to our app that deals with time.
The first 5 lines of your LoginBehavior.quorum files should look like this:
use Libraries.Interface.Forms.FormBehavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Forms.Page
use Libraries.Interface.Controls.TextField
use Libraries.Network.NetworkExchange
Update that section by adding a sixth library that will allow us to declare variables that can store dates and times like so:
use Libraries.Interface.Forms.FormBehavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Forms.Page
use Libraries.Interface.Controls.TextField
use Libraries.Network.NetworkExchange
use Libraries.System.DateTime
Now we need to create some new variables that we can use to store the login attempt time, the number of times attempted, and if the user account is currently enabled or disabled.
On line 9 you should see the variables that we are using to store our username and password. They look like this:
text myUsername = "Luggage"
text myPassword = "$2y$10$zJETA0lA8XImNzWetBRxGumKgOMte8BmwX7Iu43.c4HUoPcxyknCq"
(Note, even if the myPassword variable takes two lines on the webpage, it should be a single line in your code editor.)
Below this section we are going to insert a new code section that contains the variables we're going to use to track invalid login attempts. Add the following lines to your program so it matches the code listing below:
text myUsername = "Luggage"
text myPassword = "$2y$10$zJETA0lA8XImNzWetBRxGumKgOMte8BmwX7Iu43.c4HUoPcxyknCq"
// Used to limit login attempts
DateTime firstAttempt
integer numAttempts = 0
boolean accountEnabled = true
The comment on line 12 is used to remind us what these variables are for.
The DateTime variable is from the library we recently added to the program. It will store the timestamp of the first invalid attempt to log into the application.
The numAttempts variable will keep count of how many times an invalid login has been attempted. We have initialized the variable to 0 attempts.
The accountEnabled variable is a boolean and will be used to determine if the user's account is currently enabled or disabled. You can see we have initialized the variable to true, which means the account is initially enabled.
We need to add one last variable to our program to determine if the user has tried too many invalid attempts in a 15 minute timespan. We've already added a DateTime variable to store the first invalid attempt. Now, we'll add a second DateTime variable to store the time of the current attempt.
Find the section of code that gathers the info about the password the user is attempting to login with. It should start on line 32 and look like this:
// Gather password info.
text pass = passwordField:GetText()
NetworkExchange exchange
passwordCorrect = exchange:VerifyPassword(pass, myPassword)
We're going to add a DateTime variable to the end of this section that will store the timestamp of the current login attempt. Add the line shown below so this section now looks like this:
// Gather password info.
text pass = passwordField:GetText()
NetworkExchange exchange
passwordCorrect = exchange:VerifyPassword(pass, myPassword)
DateTime attemptTime
Right below that section of code we're going to add new section of code that will be responsible for determining if more than 15 minutes have passed since the first invalid login attempt. If they have, then it will reset the number of attempts.
Starting on line 38 insert the code section shown below:
// Reset the attempt cooldown timer.
if attemptTime:GetMinuteDifference(firstAttempt) > 15
numAttempts = 0
DateTime currentTime
firstAttempt:SetEpochTime(currentTime:GetEpochTime())
end
This code compares the timestamp of the current login attempt to the first login attempt. If more than 15 minutes have elapsed, it will reset the number of login attempts and change the time of the first attempt to the current time. Effectively, this resets the 15 minute timespan the user has to make invalid attempts.
Ok, the last thing we need to do is add logic that can capture the time of the first invalid attempt, increment the number of invalid attempts, and disable the account if too many invalid attempts are made.
To do that, we're going to replace the section of code that checks if the user can login with a more sophisticated code section.
Locate the section of code the currently checks if the user can login. It should start on line 45 and looks like this:
// Check to see if user can log in.
if userCorrect and passwordCorrect
output "Logged in as: " + usernameField:GetText()
else
output "Could not log you in. Did you forget your password?"
end
Replace this code section with this more complicated version:
// Check to see if user can log in.
if userCorrect and passwordCorrect and accountEnabled
output "Logged in as: " + user
numAttempts = 0
elseif numAttempts >= 3
accountEnabled = false
output "This account is disabled."
else
output "Could not log you in. Did you forget your password?"
numAttempts = numAttempts + 1
// Save the time of the first failed attempt.
if numAttempts = 1
DateTime currentTime
firstAttempt:SetEpochTime(currentTime:GetEpochTime())
end
end
Let's examine this new complicated conditional. The first section on lines 46-48 is very similar to the previous version. It checks to see if the user has entered the correct username and password. If they have, it outputs a message indicating that the user has successfully logged in. Now, it also sets the number of invalid attempts to 0.
The section on lines 49-51 is new. This section checks how many invalid attempts the user has made. If they have made 3 or more attempts, then the accountEnabled variable is set to false which effectively locks the users account. It also outputs a message informing the user that the account has been disabled.
The section on lines 52-61 is handling the case where the user has entered invalid credentials, but has yet disabled their account. Line 53 outputs a message to the user that lets them know the login attempt failed. On line 54, the number of invalid login attempts is incremented by 1.
The section on lines 56-60 is used to capture the timestamp of the first invalid attempt. This is used to determine the start of the 15 minute window they have to make subsequent invalid attempts.
Altogether your new LoginBehavior.quorum code listing should look like this:
use Libraries.Interface.Forms.FormBehavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Forms.Page
use Libraries.Interface.Controls.TextField
use Libraries.Network.NetworkExchange
use Libraries.System.DateTime
class LoginBehavior is FormBehavior
text myUsername = "Luggage"
text myPassword = "$2y$10$zJETA0lA8XImNzWetBRxGumKgOMte8BmwX7Iu43.c4HUoPcxyknCq"
// Used to limit login attempts
DateTime firstAttempt
integer numAttempts = 0
boolean accountEnabled = true
// Action triggered by button press.
action Run(BehaviorEvent event)
Page page = GetPage()
TextField usernameField = page:GetTextField("Username")
TextField passwordField = page:GetTextField("Password")
boolean userCorrect = false
boolean passwordCorrect = false
// Gather username info.
text user = usernameField:GetText()
if myUsername = user
userCorrect = true
end
// Gather password info.
text pass = passwordField:GetText()
NetworkExchange exchange
passwordCorrect = exchange:VerifyPassword(pass, myPassword)
DateTime attemptTime
// Reset the attempt cooldown timer.
if attemptTime:GetMinuteDifference(firstAttempt) > 15
numAttempts = 0
DateTime currentTime
firstAttempt:SetEpochTime(currentTime:GetEpochTime())
end
// Check to see if user can log in.
if userCorrect and passwordCorrect and accountEnabled
output "Logged in as: " + user
numAttempts = 0
elseif numAttempts >= 3
accountEnabled = false
output "This account is disabled."
else
output "Could not log you in. Did you forget your password?"
numAttempts = numAttempts + 1
// Save the time of the first failed attempt.
if numAttempts = 1
DateTime currentTime
firstAttempt:SetEpochTime(currentTime:GetEpochTime())
end
end
end
end
Now we're going to test all the new code we just added to the app. Either click the green play button in the main toolbar over the editor window or press ctrl+r.
Make sure your app built successfully by checking the output in palette in the bottom of the screen or by pressing ctrl+3. If your form application has any errors, the error information will be shown in the error tab of this palette. You'll need to fix the errors before you can continue with running your app.
If your app compiles correctly then a new window should appear.
Try entering the wrong username and password and logging in. Each time you make an invalid attempt you should see the "Could not log you in. Did you forget your password?" output message in the Console tab of Quorum Studio.
On your fourth invalid attempt you should see a new message that reads "This account is disabled" on the Console tab of Quorum Studio. This indicates that the account was locked after three invalid attempts.
Close the app and restart it. You should also make sure that is still accepts the correct username and password. Remember that the correct username and password are "Luggage" and "1234".
The STIG says that you're not allowed to have three invalid attempts within a 15 minute window. We also need to test if the 15 minute window is working. We could do that by trying invalid login attempts two times, then waiting for more than 15 minutes, and trying a third invalid login attempt to see if the account is locked. If the 15 minute cooldown window is working, we shouldn't lock the account as long as we waited at least 15 minutes. However, 15 minutes is a long time to wait to test our app. Can you think of a way to speed this test up?
Perhaps for testing, we could decrease the cooldown window from 15 minutes to something shorter. On line 39 of LoginBehavio.quorum you should see the following line:
if attemptTime:GetMinuteDifference(firstAttempt) > 15
This line checks to see if the current attempt is 15 minutes after the first attempt. Let's change that to two minutes instead. Alter the line to it looks like this:
if attemptTime:GetMinuteDifference(firstAttempt) > 2
Now, run the app and try entering two invalid login attempts. You should see the "Could not log you in. Did you forget your password?" message twice in the Console tab of Quorum Studio.
Now wait for at least two minutes before making your third invalid attempt.
After two minutes, make a third and forth invalid attempt. You should see that the account is not disabled. If you keep making invalid attempts you should eventually see the account is disabled because you've exceeded three invalid attempts within the two minute window.
Once you're satisfied the time window works as designed, revert line 39 to 15 minutes.
if attemptTime:GetMinuteDifference(firstAttempt) > 15
Today we improved the security of our login app by adding features that would be required to pass a Security Technical Implementation Guide (STIG) audit.
We masked the user's password to meet check V-222554.
We stored the hash of the password instead of the clear-text password to meet check V-222542.
We limited the number of invalid login attempts to meet check V-222432.
All of these are real-world checks that you'd have to meet if you were writing software for the DoD. The full STIG has more then 280 checks.