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.

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

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

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

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

{ if (cmd != null) { cmd. You will see two odd things in the following code. this comes down to a very small block of code. I am not using any hardcoded provider types. and more importantly. Therefore. it doesn't support transactions. we must register them. } } } This class is not complete. it is not good for use in production code. It does. and salt in the database Surprisingly. and it exposes the Quote characters for the provider. 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. hash.Parameters.Dispose(). Using the code Before we can authenticate a user. Probably. The . except they will repeat every time you create a new object. To register a user. It lacks the ability to use multiple connection strings. To solve this problem. this would be fine. however. 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. therefore. 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. have a couple really nice features. the biggest shortcoming is the complete lack of error handling.Clear().NET Random object provides pseudo-random numbers. it takes full advantage of connection pooling. } cmd = null. . cmd. This example only uses one table: "Users". you must wrap the column names correctly. and I am using RNGCryptoServiceProvider.

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

DbType = DbType. byte[] computedHash = hashAlgorithm. i < computedHash. {0}password{1} FROM {0}Users{1} WHERE {0}user{1}=?".Value = txtUserName.The above code is as simple as possible. byte[] password = (byte[])dr. It should include checks for usernames already existing in the database and more.QuoteSuffix).Length). if (!tmp) .Add(p). string retrieveUser = string.Create(). HashAlgorithm hashAlgorithm = SHA512.Factory. parameters. Get the correct record from the database. Our list of things to do is: y y y y y y Wait 2 seconds to "tarpit" attackers. Get the username and password from the user. tmp = (computedHash. using (DbDataReader dr = DB. if (tmp) { for (int i = 0.ToArray()).String.GetValue(0). p. Compare the resulting hash to the password hash that is stored in the database.Format( "SELECT {0}salt{1}.Length == password. Again. we don't give want to show "user not found" or "incorrect password" as that would give attackers too much information. buffer.AddRange(salt). it is a surprisingly simple bit of code. p. Here is the code to do all of that: Collapse | Copy Code Thread. bool tmp = true.CreateParameter().Unicode. DB. Return "login failed" or "login successful".Text)). //tarpit bool successful = false.Sleep(2000). parameters)) { while (dr.Read()) { byte[] salt = (byte[])dr.QuotePrefix. thus slowing brute force attacks to a crawl.ExecuteReader(retrieveUser. DB.ComputeHash(buffer.GetBytes(txtPassword.GetValue(1). List<byte> buffer = new List<byte>(Encoding.Length. Use the salt from the database to create a hash from the salt and password attempt. List<DbParameter> parameters = new List<DbParameter>(). The next thing is actually responding to a login request.Text. i++) { tmp &= computedHash[i] == password[i]. try { DbParameter p = DB.

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