Skip to content

Instantly share code, notes, and snippets.

@kamsar
Created September 1, 2013 22:29
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kamsar/6407742 to your computer and use it in GitHub Desktop.
Save kamsar/6407742 to your computer and use it in GitHub Desktop.
This class provides a method to 'upgrade' the hash algorithm used by the SqlMembershipProvider without resetting all existing passwords. Users can effectively then be authenticated using either legacy or modern hashes, and any time a hash gets touched it will be upgraded automatically.
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Security;
namespace Kamsar.Security
{
/// <summary>
/// This class provides a method to 'upgrade' the hash algorithm used by the SqlMembershipProvider without resetting all existing passwords.
/// Users can effectively then be authenticated using either legacy or modern hashes, and any time a hash gets touched it will be upgraded automatically.
/// </summary>
/// <remarks>
/// To utilize this provider, you must perform the following steps:
/// 1. Provide a new hash algorithm to use (using the hashAlgorithmType attribute on the membership element of web.config)
/// e.g. (using Zetetic.Security): &lt;membership defaultProvider="sitecore" hashAlgorithmType="pbkdf2_local"&gt;
/// 2. Replace the existing SqlMembershipProvider provider you are using with this class as the implementation
/// 3. Enjoy legacy users being able to still sign in, and new users and changed passwords switching to your new algorithm
///
/// If you need to use a legacy algorithm other than SHA1 just inherit from the class and pass the algorithm to the protected constructor.
///
/// If using Zetetic.Security to upgrade to PBKDF2 (or its BCrypt support), you can follow these instructions to activate the PBKDF algorithm:
/// 1. Install the Zetetic.Security NuGet package (this proxies built in .NET classes for PBKDF2, so it is well tested)
/// 2. Add the following line to Application_Start in the Global.asax:
/// System.Security.Cryptography.CryptoConfig.AddAlgorithm(typeof(Zetetic.Security.Pbkdf2Hash), "pbkdf2_local");
/// 3. Reference the named algorithm in your membership web.config hashAlgorithmType setting as "pbkdf_local"
/// &lt;membership defaultProvider="sitecore" hashAlgorithmType="pbkdf2_local"&gt;
///
/// See also http://zetetic.net/blog/2012/7/3/secure-password-hashing-for-aspnet-in-one-line.html
/// </remarks>
public class HashFallbackSqlMembershipProvider : SqlMembershipProvider
{
private bool _enableFallback = true;
private string _connectionString;
private readonly HashAlgorithm _fallbackAlgorithm;
public HashFallbackSqlMembershipProvider() : this(new SHA1Managed())
{
}
protected HashFallbackSqlMembershipProvider(HashAlgorithm fallbackAlgorithm)
{
_fallbackAlgorithm = fallbackAlgorithm;
}
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
throw new ArgumentNullException("config");
_enableFallback = (config["passwordFormat"] ?? "Hashed") == "Hashed";
_connectionString = config["connectionString"];
if (string.IsNullOrEmpty(_connectionString))
{
string connectionStringName = config["connectionStringName"];
if (!string.IsNullOrEmpty(connectionStringName))
_connectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
}
base.Initialize(name, config);
}
public override bool ValidateUser(string username, string password)
{
if (base.ValidateUser(username, password)) return true;
if (_enableFallback)
return FallbackValidateUser(username, password);
return false;
}
private bool FallbackValidateUser(string username, string password)
{
var targetHash = GetPasswordHash(username);
if (targetHash.Hash == null || targetHash.Salt == null) return false;
var hash = HashFallbackPassword(password, targetHash.Salt);
return hash.Equals(targetHash.Hash);
}
private KeyedHash GetPasswordHash(string username)
{
using (var connection = new SqlConnection(_connectionString))
{
using (var sqlCommand = new SqlCommand("dbo.aspnet_Membership_GetPasswordWithFormat", connection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlCommand.Parameters.Add(CreateInputParam("@ApplicationName", SqlDbType.NVarChar, ApplicationName));
sqlCommand.Parameters.Add(CreateInputParam("@UserName", SqlDbType.NVarChar, username));
sqlCommand.Parameters.Add(CreateInputParam("@UpdateLastLoginActivityDate", SqlDbType.Bit, 0));
sqlCommand.Parameters.Add(CreateInputParam("@CurrentTimeUtc", SqlDbType.DateTime, DateTime.UtcNow));
var sqlParameter = new SqlParameter("@ReturnValue", SqlDbType.Int);
sqlParameter.Direction = ParameterDirection.ReturnValue;
sqlCommand.Parameters.Add(sqlParameter);
connection.Open();
using (var sqlDataReader = sqlCommand.ExecuteReader(CommandBehavior.SingleRow))
{
var result = new KeyedHash();
if (sqlDataReader.Read())
{
result.Hash = sqlDataReader.GetString(0);
result.Salt = sqlDataReader.GetString(2);
}
else
{
result.Hash = null;
result.Salt = null;
}
return result;
}
}
}
}
private SqlParameter CreateInputParam(string paramName, SqlDbType dbType, object objValue)
{
var sqlParameter = new SqlParameter(paramName, dbType);
if (objValue == null)
{
sqlParameter.IsNullable = true;
sqlParameter.Value = DBNull.Value;
}
else
sqlParameter.Value = objValue;
return sqlParameter;
}
private string HashFallbackPassword(string password, string salt)
{
byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
byte[] saltBytes = Convert.FromBase64String(salt);
byte[] hashBytes;
var keyedAlgorithm = _fallbackAlgorithm as KeyedHashAlgorithm;
if (keyedAlgorithm != null)
{
if (keyedAlgorithm.Key.Length == saltBytes.Length)
keyedAlgorithm.Key = saltBytes;
else if (keyedAlgorithm.Key.Length < saltBytes.Length)
{
var completeHashBytes = new byte[keyedAlgorithm.Key.Length];
Buffer.BlockCopy(saltBytes, 0, completeHashBytes, 0, completeHashBytes.Length);
keyedAlgorithm.Key = completeHashBytes;
}
else
{
var keyBytes = new byte[keyedAlgorithm.Key.Length];
int dstOffset = 0;
while (dstOffset < keyBytes.Length)
{
int count = Math.Min(saltBytes.Length, keyBytes.Length - dstOffset);
Buffer.BlockCopy(saltBytes, 0, keyBytes, dstOffset, count);
dstOffset += count;
}
keyedAlgorithm.Key = keyBytes;
}
hashBytes = keyedAlgorithm.ComputeHash(passwordBytes);
}
else
{
var buffer = new byte[saltBytes.Length + passwordBytes.Length];
Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length);
Buffer.BlockCopy(passwordBytes, 0, buffer, saltBytes.Length, passwordBytes.Length);
hashBytes = _fallbackAlgorithm.ComputeHash(buffer);
}
return Convert.ToBase64String(hashBytes);
}
private class KeyedHash
{
public string Salt { get; set; }
public string Hash { get; set; }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment