You are on page 1of 9

Sec pass authentication:

Introduction
It's unbelievable to me that I am still running into websites that store passwords themselves, and you see rules like "password must be no more than 16 characters" or "passwords must not include '<> *& etc.." Just last night, I ran into a government website that had a maximum password length of 12 characters, and while they allowed certain special characters, others were disallowed. These kinds of rules sound like the site is storing the password in plain text, or worse, not using parameters for the storage and validation of the passwords from a database. After so many years of passwords, I would have assumed most programmers would have figured these things out. In many cases, storing passwords in your own application or website is pointless; there are many alternatives. 1. Integration with your network security, such as ActiveDirectory 2. OpenID This article is for those who must store usernames and passwords themselves. I am going to use C#, MS Access, and ASP.NET for this example; though the premise would obviously be able to be used with any application stack you like.

Background
Other articles have been written covering this information, but I want to keep this article very straightforward. I am not going to go into the depths of cryptography, nor am I going to cover keeping people signed in, or validating minimum complexity, or anything of that nature. My intention is to keep the information and code to only what is required to store passwords securely. For the purposes of this article, my goals are very simple. 1. Allow passwords of unlimited length, and of any characters the user likes. 2. Be able to store the password in a database efficiently. 3. Prevent any SQL injection attacks. So, let's start with the first 2 items. I don't believe that passwords should ever need to be "read" or "recovered". The only exception to this would be if we need to keep passwords to other applications, and even then, only if you have no other choice, such as converting all the applications in question to OpenID. Should a password be lost or compromised, there should be a "reset", this philosophy means that I do not need to ever store passwords in plain text or a decryptable form, at least not for authentication. Thus, the simplest way to solve these two problems is by using a one way hash, that is an encryption that can't be decrypted. Hashing algorithms like MD5, SHA-1, and others can be used to create a fixed length series of values that is more of a unique signature for data rather than encrypted data.

For example, the MD5 hash of "test" is "098f6bcd4621d373cade4e832627b4f6". The nice thing about these signatures is that no matter how big the data is, the size of the data returned is always the same. This is why you will see MD5 and SHA-1 signatures for validating large files like Linux DVD ISO images. Different algorithms will have different lengths of output signatures:
y

y y y y

MD5 has a length of 128 bits, which is 16 bytes. Interestingly enough this is the same length as a UUID/GUID. This means that if you are storing something non-security related, you can store the data in a UUID/GUID field in your database. SHA-1 has a length of 160 bits, which is 20 bytes. SHA-256 has a length of 256 bits, which is 32 bytes. SHA-384 has a length of 384 bits, which is 48 bytes. SHA-512 has a length of 512 bits, which is 64 bytes.

Now, if we use a signature to store our password, we get all three of my requirements in one shot. First, because the signatures don't contain anything that could be used for SQL injection or that we have to worry about encoding, any character the user wants to use can be used for a password. Second, we can store passwords of any size we like in a fixed length field, such as 64 bytes. Before going any further, we need to look at the fact that MD5 is now easily cracked. In fact, you can decrypt many MD5 signatures at websites like this one. SHA-1 is also showing signs of being weakened, and will soon be obsolete as well. So, what can be done to "shore up" the signature? First, we use the strongest hash available to us in our code base. Obviously, we could write our own implementation of stronger ones, but it's better to use tried and true encryption code rather than make our own. The second thing we can do is to "salt" our passwords; in short, this means creating a random value to append to the end of the password to make it more unique. This could be a short series of bytes, but we can use as much data as is reasonable. Of course, the other reason to "salt" passwords is to prevent analysis of the passwords. If all you do is hash the password, then the password of "test" is always "098f6bcd4621d373cade4e832627b4f6". This means that should the list of passwords be compromised, everyone who has the same password would have the same hash. By using a random salt value, the stored hashes become unique, even if the same password is used. The most common question often asked about salts is "If a salt is random, how do you reliably generate the same salt every time verification is done"? The answer is simple, you don't. You store the salt separately from the password hash. Storing the data is fairly straightforward, but because we are using an array of bytes (an OLE object in Access), pure text SQL will not work. Instead, we need to use parameters, this is good since this also helps prevent SQL injection.

Since I want this article to cover more than just rehashing the same data that was covered everywhere else, I figured I would make this example more portable to other databases. See my previous article for more details. Thus, I will start with a static DB class that reads the configuration file and creates the proper types based on the provider. Collapse | Copy Code
public static class DB { private static DbProviderFactory _factory = null; private static string _connectionString = null; private static string _quotePrefix = string.Empty; private static string _quoteSuffix = string.Empty; public static DbProviderFactory Factory { get { if (_factory == null) { ConnectionStringSettings connectionSettings = ConfigurationManager.ConnectionStrings["DSN"]; _factory = DbProviderFactories.GetFactory( connectionSettings.ProviderName); _connectionString = connectionSettings.ConnectionString; } return _factory; } } public static string ConnectionString { get { return _connectionString; } } public static string QuotePrefix { get { if (string.IsNullOrEmpty(_quotePrefix)) { FillQuotes(); } return _quotePrefix; } } public static string QuoteSuffix { get {

if (string.IsNullOrEmpty(_quoteSuffix)) { FillQuotes(); } return _quoteSuffix; } } //this function gets the proper characters to wrap //database, table, and column names. private static void FillQuotes() { var cb = Factory.CreateCommandBuilder(); if (!string.IsNullOrEmpty(cb.QuotePrefix)) { _quoteSuffix = cb.QuoteSuffix; _quotePrefix = cb.QuotePrefix; return; } using (var conn = GetConnection()) { using (var cmd = conn.CreateCommand()) { //test to see if we can wrap names in square brackets. cmd.CommandText = "SELECT '1' as [default]"; try { using (var dr = cmd.ExecuteReader()) { while (dr.Read()) { } } _quotePrefix = "["; _quoteSuffix = "]"; } catch { try { //square brackets failed, try double quotes. cmd.CommandText = "SELECT '1' as \"default\""; using (var dr = cmd.ExecuteReader()) { while (dr.Read()) { } } _quotePrefix = _quoteSuffix = "\""; } catch { //no characters appear to work } }

} } } private static DbConnection GetConnection() { DbConnection conn = Factory.CreateConnection(); conn.ConnectionString = ConnectionString; conn.Open(); return conn; } public static int ExecuteNonQuery(string sql, IEnumerable<DbParameter> parameters) { using (var conn = GetConnection()) { DbCommand cmd = null; try { cmd = conn.CreateCommand(); cmd.CommandText = sql; foreach (var parameter in parameters) { cmd.Parameters.Add(parameter); } return cmd.ExecuteNonQuery(); } finally { if (cmd != null) { cmd.Parameters.Clear(); cmd.Dispose(); } cmd = null; } } } public static DbDataReader ExecuteReader(string sql, IEnumerable<DbParameter> parameters) { var conn = GetConnection(); DbCommand cmd = null; try { cmd = conn.CreateCommand(); cmd.CommandText = sql; foreach (var parameter in parameters) { cmd.Parameters.Add(parameter); } return cmd.ExecuteReader(CommandBehavior.CloseConnection); } finally

{ if (cmd != null) { cmd.Parameters.Clear(); cmd.Dispose(); } cmd = null; } } }

This class is not complete; therefore, it is not good for use in production code. It lacks the ability to use multiple connection strings, and more importantly, it doesn't support transactions. Probably, the biggest shortcoming is the complete lack of error handling. It does, however, have a couple really nice features; it takes full advantage of connection pooling, and it exposes the Quote characters for the provider. This example only uses one table: "Users". Users Unique Constraint ID user Primary Key
salt UUID/GUID varchar(255) byte[16]

password byte[64]

I intentionally made the column names collide with SQL keywords to show the functionality of wrapping column and table names. Therefore, you must wrap the column names correctly.

Using the code


Before we can authenticate a user, we must register them. To register a user, we have to do the following:
y y y y y

Get the username Get the password Generate a random salt Create the password hash Store the username, hash, and salt in the database

Surprisingly, this comes down to a very small block of code. You will see two odd things in the following code. I am not using any hardcoded provider types, and I am using RNGCryptoServiceProvider. The .NET Random object provides pseudo-random numbers; this would be fine, except they will repeat every time you create a new object. To solve this problem, Microsoft tells you to either use RNGCryptoServiceProvider or simply create a single static Random object that all the code in your project uses for random numbers.

Collapse | Copy Code


bool successful = false; try { string insertUserSQL = string.Format( "INSERT INTO {0}Users{1} ({0}user{1}," + "{0}salt{1},{0}password{1}) VALUES (?,?,?)", DB.QuotePrefix, DB.QuoteSuffix); List<DbParameter> parameters = new List<DbParameter>(); //you can change this line out for any Hash algorithm you like. HashAlgorithm hashAlgorithm = SHA512.Create(); byte[] b = new byte[16]; RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); rng.GetBytes(b); //create all 3 parameters, the order is critical since //these are positional parameters. //user DbParameter p = DB.Factory.CreateParameter(); p.DbType = DbType.String; p.Value = txtUserName.Text; parameters.Add(p); //salt p = DB.Factory.CreateParameter(); p.DbType = DbType.Binary; p.Value = b; parameters.Add(p); //password p = DB.Factory.CreateParameter(); p.DbType = DbType.Binary; p.Value = b; parameters.Add(p); string s = txtPassword.Text; List<byte> pass = new List<byte>(Encoding.Unicode.GetBytes(s)); pass.AddRange(b); p.Value = hashAlgorithm.ComputeHash(pass.ToArray()); DB.ExecuteNonQuery(insertUserSQL, parameters); successful = true; } catch(Exception ex) { Debug.WriteLine(ex.ToString()); } Label3.Text = "Registration " + (successful ? "successful" : "failed");

The above code is as simple as possible. It should include checks for usernames already existing in the database and more. The next thing is actually responding to a login request. Again, it is a surprisingly simple bit of code. Our list of things to do is:
y y y y y y

Wait 2 seconds to "tarpit" attackers, thus slowing brute force attacks to a crawl. Get the username and password from the user. Get the correct record from the database. Use the salt from the database to create a hash from the salt and password attempt. Compare the resulting hash to the password hash that is stored in the database. Return "login failed" or "login successful"; we don't give want to show "user not found" or "incorrect password" as that would give attackers too much information.

Here is the code to do all of that: Collapse | Copy Code


Thread.Sleep(2000); //tarpit bool successful = false; HashAlgorithm hashAlgorithm = SHA512.Create(); string retrieveUser = string.Format( "SELECT {0}salt{1}, {0}password{1} FROM {0}Users{1} WHERE {0}user{1}=?", DB.QuotePrefix, DB.QuoteSuffix); List<DbParameter> parameters = new List<DbParameter>(); try { DbParameter p = DB.Factory.CreateParameter(); p.DbType = DbType.String; p.Value = txtUserName.Text; parameters.Add(p); using (DbDataReader dr = DB.ExecuteReader(retrieveUser, parameters)) { while (dr.Read()) { byte[] salt = (byte[])dr.GetValue(0); byte[] password = (byte[])dr.GetValue(1); List<byte> buffer = new List<byte>(Encoding.Unicode.GetBytes(txtPassword.Text)); buffer.AddRange(salt); byte[] computedHash = hashAlgorithm.ComputeHash(buffer.ToArray()); bool tmp = true; tmp = (computedHash.Length == password.Length); if (tmp) { for (int i = 0; i < computedHash.Length; i++) { tmp &= computedHash[i] == password[i]; if (!tmp)

{ break; } } } successful = tmp; } } } catch (Exception ex) { Debug.WriteLine(ex.ToString()); } Label3.Text = "Login " + (successful ? "successful" : "failed");

Points of interest
As stated above, an MD5 hash is the same size as a GUID, and could actually be used to fill in the ID if your database doesn't have an equivalent to SQL Server's newid(). I have seen people state that the "ID" of the user's row could be used as a "salt". However, if you try to use the ID value as the salt, be it a GUID or an auto-increment value, it bypasses the security afforded by a totally random seed. Even if my opinion is wrong, generating a series of random bytes, as above, is easy enough to do, that it still doesn't seem like using the "ID" is a good idea. Because I am using a parameter for the values, I can use any value I like for the username, be it an email address, or something with normally disallowed characters, such as an apostrophe like in "O'Brian". Remember to use SSL to secure any page that deals with sensitive information, especially passwords since people tend to reuse passwords. Cost is not an excuse. While some sites like Verisign and Thawte charge over $100 (US) to purchase an SSL certificate, you can get cheap certificates from GoDaddy for around $30 (US) or free from StartSSL, and just like Verisign and Thawte, they work in all major browsers as well. Before the CAcert crowd starts responding, I don't recommend CA, because it will require all who use your site to insert the root certificate manually.

You might also like