-
-
Save AlexKasaku/19a94e25079a5fe3985b116c71e57f58 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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): <membership defaultProvider="sitecore" hashAlgorithmType="pbkdf2_local"> | |
/// 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" | |
/// <membership defaultProvider="sitecore" hashAlgorithmType="pbkdf2_local"> | |
/// | |
/// 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