Thursday, 29 November 2007

Commerce Server 2007 and password hashing

Commerce Server 2007 supports hashing and encryption of database fields, such as passwords, through encryption and/or hashing algorithms that can be specified in config. Out of the box CS2007 supports three hashing algorithms, SHA1, SHA256, and MD5. I think SHA1 is the default algorithm used for one-way hashing.

If you use the UpmMembershipProvider hashing and validation is handled for you. Should you need to hash or verify a password without the help of this provider, you may be in for a fun time trying to figure out how it all works.

Microsoft has published examples of how to validate CS2007 passwords and you can deduce from the examples how the whole hashing business takes place. However, there are two curious things to note about these examples - and they're not good:

  1. The examples ignore SHA1 hashing even though that's probably what most standard setups use.
  2. The GenerateSaltValue() method is just wrong. Try running the example and passing in null as a salt value to HashPassword() and you'll see what I mean.

Before I continue, a quick overview of the standard hashing procedure is appropriate.

Given a plain-text password, we generate a random salt value that that the hash algorithm uses to hash the plain-text password. Next we turn the salt value and the plain-text passwords into byte arrays, and then combine the two byte arrays into one with the salt preceding the plain-text password. Finally we call ComputeHash(myByteArray) on our specified hash algorithm instance. This call returns another byte array which we can then traverse to generate a string. The final hashed password consists of the salt value pre-pended on the hashed password. Look at the code examples below and this will make more sense. Note also that CS2007 uses a hexadecimal representation of the bytes in our hashed password, meaning that each byte is converted to a 2-digit hexadecimal string (myByte.ToString("x2")).

Now, back to the GenerateSaltValue() method in Microsoft's example. This method creates a byte array, fills it with random integers, and turns it into a string by calling GetString() on an instance of UnicodeEncoding. In the example, the size of the byte array is 4 (specified by the SaltValueSize member), which results a 2 character string. A quick look at the HashPassword() methods shows you that any salt value passed in as a string is expected to be 8 characters. Furthermore, the byte.Parse() calls fail because the input data is of an invalid format.

What the Microsoft example has left out is that the returnes salt string should be hexadecimal. So, once the byte array has been created, each byte should be turned into a two digit hex value and appended to a string. If you do that it will work.

It took me a while to figure this out, but one particular post on Christiano's blog shed some light on the problems I was having and helped me create a better solution, shown below.

If you want to use Microsoft's example as it stands, replace the GenerateSaltValue with this method:


public string GenerateSaltValue()
{
StringBuilder builder = new StringBuilder();
UnicodeEncoding encoding = new UnicodeEncoding();
byte[] salt = new byte[SaltValueSize];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetNonZeroBytes(salt);

foreach (byte outputByte in salt)
builder.Append(outputByte.ToString("x2").ToUpper());

return builder.ToString();
}


Otherwise, the below class may be a starting point for your hashing:


using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;

public class Hasher
{
public readonly int SaltValueSize = 4;

public string Hash(string stringToHash, HashAlgorithm hash)
{
return Hash(stringToHash, null, hash);
}

public string Hash(string stringToHash, string saltValue, HashAlgorithm hash)
{
return ComputeHash(stringToHash, saltValue, hash);
}


private string ComputeHash(string stringToHash, string saltValue, HashAlgorithm hash)
{
UnicodeEncoding encoding = new UnicodeEncoding();
byte[] hashedBytes;
byte[] salt = null;
byte[] dataBuffer;
byte[] stringToHashBytes;
StringBuilder builder = new StringBuilder();

if (saltValue == null)
salt = GetSalt();
else
salt = GetSaltFromString(saltValue);

dataBuffer = new byte[encoding.GetByteCount(stringToHash) + SaltValueSize];
stringToHashBytes = encoding.GetBytes(stringToHash);

salt.CopyTo(dataBuffer, 0);
stringToHashBytes.CopyTo(dataBuffer, SaltValueSize);

hashedBytes = hash.ComputeHash(dataBuffer);

foreach (byte outputByte in salt) builder.Append(outputByte.ToString("x2").ToUpper());
foreach (byte outputByte in hashedBytes) builder.Append(outputByte.ToString("x2").ToUpper());

return builder.ToString();
}

private byte[] GetSalt()
{
byte[] saltBytes = new byte[SaltValueSize];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetNonZeroBytes(saltBytes);

return saltBytes;
}

private byte[] GetSaltFromString(string saltValue)
{
byte[] saltBytes = new byte[SaltValueSize];

binarySaltValue[0] = byte.Parse(saltValue.Substring(0, 2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);
binarySaltValue[1] = byte.Parse(saltValue.Substring(2, 2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);
binarySaltValue[2] = byte.Parse(saltValue.Substring(4, 2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);
binarySaltValue[3] = byte.Parse(saltValue.Substring(6, 2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);

return saltBytes;
}

}

No comments: