Introduction
I came up with the concept for this after watching Die Hard 4.0 where the White Hat hacker in the movie had written what he called a "mutating security algorithm". I thought that it might be useful to have a system that changes (or mutates) passwords if a login fails enough times that it might present a security risk.
This login system is not like your conventional system. Classic login systems rely on a defined username and password combination to allow users access to private information. A lot of people don't often use sufficiently strong passwords when they register to be part of a community website or even when they set their login information on internet banking systems.
These passwords are normally things like birthdates or names that have meaning to the registrant. This is where the security risk comes in.
A plausible scenario would be a situation in which a woman signs up for facebook and sets her password as her daughter's full name. Friends of hers on facebook that may be feeling "mischievous" might guess that her password has something to do with her daughter, and make attempts at guessing her login details.
In this situation, a classic login system would allow for an unlimited number of guesses during which the "attacker" might eventually guess right and gain access.
This login system addresses such a situation and provides more security to the registered user.
Background
This system was written in Visual Basic.NET and uses a SQL Server database for storage of user information. Most user data is stored as plain text, with the exception of the user's password which is encrypted using SHA1.
Challenges during development were small issues around the encryption of the user's password as well as the tracking of login attempts through the use of a session variable which gets counted on each attempt. These issues were purely conceptual in nature and were resolved relatively quickly.
How It Works
Upon registration, a new user will be sent an email which will contain details about his/her account. These details include their user id, username, and password.
To log in, the user will have to enter their user id first to verify that they indeed do have an account on the system. Once the user id is verified, they will be asked to supply the username and password that is associated with the user id that they entered.
Where there are less than 3 login attempts on record for the current session, the login hashes and salts the value entered into the password field on the login form, and selects a record with user id, username, and password value that are identical to user input. If this selection doesn't return a single row from the database, the login was invalid and an attempt gets counted in the user's session.
Should there be 3 login attempts counted, the account is flagged as locked and, until such time as it is recovered by the user, will not be included in selection for future login attempts.
In addition, a 36 character long value is generated after the account gets locked. This value is then hashed and salted and stored in the database as the new password for the account.
Login.aspx.vb
Imports System.Data
Imports System.Data.SqlClient
Public Class Login
Inherits System.Web.UI.Page
Private connstr As String = "Data Source=.\SQLEXPRESS; Initial Catalog=test; Integrated Security=True;"
Private conn As SqlConnection = New SqlConnection(connstr)
Public NewPass As String = Nothing
Public HashedPass As String = Nothing
Public UserId As Integer = Nothing
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not IsPostBack Then
pnlLoginForm.Visible = False
Session("LoginAttempt") = 0
End If
If Session("LoginAttempt") = 2 Then
Dim s As String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
Dim r As New Random
Dim sb As New StringBuilder
Dim idx As Integer = Nothing
For i As Integer = 1 To 8
idx = r.Next(0, 61)
NewPass += sb.Append(s.Substring(idx, 1)).ToString
Next
HashedPass = Crypt.Compute(NewPass, "SHA512", Nothing)
Dim sql As String = "UPDATE Users SET Password = @Pass, LockedYN = @Lock"
Dim cmd As New SqlCommand(sql, conn)
cmd.Parameters.AddWithValue("@Pass", NewPass)
cmd.Parameters.AddWithValue("@Lock", "Yes")
conn.Open()
cmd.ExecuteNonQuery()
conn.Close()
End If
End Sub
Private Sub btnVerifyID_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnVerifyID.Click
If txtUserId.Text IsNot Nothing Then
Dim UserData As New DataSet
Dim UserAdapter As New SqlDataAdapter
UserAdapter.SelectCommand = New SqlCommand("SELECT * FROM Users " & _
"WHERE UserId = @ID", conn)
UserAdapter.SelectCommand.Parameters.AddWithValue("@ID", txtUserId.Text)
UserAdapter.Fill(UserData)
If UserData.Tables(0).Rows.Count <> 1 Then
lblError.Text = "Specified User ID does not exist."
lblError.ForeColor = Drawing.Color.Red
Else
If UserData.Tables(0).Rows(0)(4).ToString = "Yes" Then
lblError.Text = "The account you tried to log into has been locked to prevent possible unauthorized access. " & _
"If this is your account, please check your email for instructions to unlock it."
lblError.ForeColor = Drawing.Color.Red
Else
UserId = txtUserId.Text
pnlLoginForm.Visible = True
End If
End If
End If
End Sub
Private Sub btnLogin_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnLogin.Click
If txtUser.Text IsNot Nothing And txtPass.Text IsNot Nothing Then
If Session("LoginAttempt") <> 2 Then
Dim pass As String = Crypt.Compute(txtPass.Text, "SHA512", Encoding.UTF8.GetBytes(Crypt.SaltValue))
Dim UserData As New DataSet
Dim UserAdapter As New SqlDataAdapter
UserAdapter.SelectCommand = _
New SqlCommand("SELECT * FROM Users " & _
"WHERE Username = @User AND Password = @Pass " & _
"AND LockedYN = @Lock", conn)
UserAdapter.SelectCommand.Parameters.AddWithValue("@User", txtUser.Text)
UserAdapter.SelectCommand.Parameters.AddWithValue("@Pass", pass)
UserAdapter.SelectCommand.Parameters.AddWithValue("@Lock", "No")
UserAdapter.Fill(UserData)
If UserData.Tables(0).Rows.Count <> 1 Then
lblError.Text = "Invalid username or password."
lblError.ForeColor = Drawing.Color.Red
Session("LoginAttempt") = CInt(Session("LoginAttempt")) + 1
Else
Session("LoggedIn") = True
Response.Redirect("Home.aspx")
End If
Else
lblError.Text = "The account you tried to log into is now locked due to excessive failed login attempts. " & _
"If this is your account, please check your email for instructions to unlock it."
lblError.ForeColor = Drawing.Color.Red
End If
Else
lblError.Text = "Please enter a username and password."
lblError.ForeColor = Drawing.Color.Red
End If
End Sub
End Class
What this release contains
This release contains the basic security "measures". Due to challenges with free time, I haven't been able to add the code to process the emails yet.
Currently included are:
- Functions to handle SHA1 encryption of user passwords
- General session management that keeps track of login attempts
- Bare-bones user interface used while testing. This has not been cleaned up in this version.
Plans for the future
I always like it when I can find software that works on multiple platforms, so I'll be working on a PHP version that uses mysql as a data backend.
To put any security worries to bed, the next version will include functionality to redirect users off of the login page if they are currently logged in to their account.
Probably the most crucial change in the works will be the modification of the user id from an integer field to a guid(). This will eliminate the possibility of user ids being guessed and make "spoofing" another user's account a lot more complicated.
Using the code
Before you can use this system, you'll need to change the connection strings. Currently each file that works with the data source has one in it:
- Create.aspx.vb
- Login.aspx.vb
Private connstr As String = "Data Source=.\SQLEXPRESS; Initial Catalog=test; Integrated Security=True;"
Only one table is needed for the system to function correctly. The table must include the following structure for the system to work correctly.
Users Table:
UserID (PK, int, not null) -- identity(1,1)
Username (varchar(20), not null)
Password (varchar(36), not null)
EmailAddress (varchar(100), not null)
LockedYN (char(3), not null) -- default 'No'
Here's a CREATE sample to get you going:
CREATE TABLE [dbo].[Users](
[UserID] [int] IDENTITY(1,1) NOT NULL,
[Username] [varchar](20) NOT NULL,
[Password] [varchar](36) NOT NULL,
[EmailAddress] [varchar](100) NOT NULL,
[LockedYN] [char](3) NOT NULL,
CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
(
[UserID] ASC
)
Feel free to add more columns to your user table if you wish, but if you remove or change any of these, the system will break.
Points of Interest
Google is your friend. The encryption was particularly challenging for me to make work. I found a number of samples that I worked through and based my code (Crypt.aspx.vb) on.