Nope, not cannabis, nor potato, but rather this:
Yup, password hashes in Sitecore. Unfortunately, they’re not all that secure – but they can be.
To recap, Hashing algorithms are mathematical processes that take an input, jumble it all around, and produce an output. That output is called a hash. These algorithms have a number of features:
- The output is unpredictable – A change of 1 binary digit can produce a completely different output, and you can’t guess what that is – you have to go through all the steps to get there.
- They are non-reversible – You can’t take an output, and figure out what the input was.
A common use of hashes is passwords. We don’t want to store user password in the clear, ‘cos if some nefarious soul with access to where we’re storing them (e.g. your System Admin) wanted to, they could steal and sell all those passwords.
If we store the hash, though, then we don’t know their password. When the user tries to log-in again, we’ll take their password, hash it, and compare it against the hash we’ve stored. If the two match, then that password – whatever it was – must be correct.
However, not all hashing algorithms are made equal, or remain so over time. For example, MD5 was regarded as a secure, but was ‘broken’ – that is, shortcuts were found that made its output predictable, thus defeating its security. Recently, the same has happened for SHA1 – a team at Google were able to predict most of the steps of the hash, and so only had to try a few different inputs to get a “valid“ (i.e. matching) output.
Unfortunately, SHA1 is what Sitecore uses.
That’s not a critical issue – although ‘broken’ and now regarded as ‘weak’, it still takes a massive amount of computation to generate a collision (think major corporate and nation-state).
This attack required over 9,223,372,036,854,775,808 SHA1 computations. This took the equivalent processing power as 6,500 years of single-CPU computations and 110 years of single-GPU computations
It is, however, time to move to something better. Sitecore have offered the following (let’s skip over the question “Why is this not the standard configuration for a new Sitecore instance?”) :
When you create a new website, you must change the weak default hash algorithm (SHA1) that is used to encrypt user passwords to a stronger algorithm.
To change the hash algorithm:
- Open the web.config file and in the <membership> node, set the hashAlgorithmType setting to the appropriate value. We recommend SHA512.
Oke-doke, that seems sound… except what about my systems that already have users in them? Like, say, any project that isn’t a brand-new Sitecore instance? Any existing user’s passwords will have been stored with the old hashing algorithm, and so their passwords will fail. I don’t imagine our users will like that.
Well, it turns out that we’ve a nice example of how to do fallback in the membership provider: https://gist.github.com/kamsar/6407742
It tries to log someone in using the configured algorithm, and if it fails it tries again with SHA1. Thus, new users, and users who change their password can benefit from more secure hashes, but everyone can still log in.
I’ve tested this myself; it works nicely. I changed my Admin password (to the default ‘b’ again), and you can see the new (longer) hash:
It works; worth doing.
I’ve got an example of this here, though it’s basically a rip-off of the Kam Figy’s
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Security.Cryptography;
using System.Text;
using System.Web.Security;
namespace AWBTest.Foundation.Example
{
/*
* https://gist.github.com/kamsar/6407742
* https://kamsar.net/index.php/2013/09/upgrading-sitecores-password-security/
*
* Don't forget to add this to web.config:
* <membership defaultProvider="sitecore" hashAlgorithmType="SHA512">
* <providers>
* <clear />
* <add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="sql" providerWildcard="%" raiseEvents="true" />
* <add name="sql" type="AWBTest.Foundation.Example.HashFallbackSqlMembershipProvider, AWBTest.Foundation.Example" connectionStringName="core" applicationName="sitecore" minRequiredPasswordLength="1" minRequiredNonalphanumericCharacters="0" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" />
* <add name="switcher" type="Sitecore.Security.SwitchingMembershipProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/membership" />
* </providers>
* </membership>
*
* There are better algorithms to use than SHA512, but that's one of the better built-in ones; you don't have to add
* an assembly that implements bcrypt or pbkdf2.
*/
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; }
}
}
}
[…] this this relates to my recent post on password hashing in Sitecore, and why we should move away from SHA1. Let’s say you’ve decided to use […]