Adds support for the super old password format so we can handle upgrades

This commit is contained in:
Shannon
2020-10-07 15:20:43 +11:00
parent a7c4fa5f2e
commit e5c272b5d2
9 changed files with 96 additions and 86 deletions

View File

@@ -62,6 +62,7 @@
public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3";
public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2";
public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256";
public const string AspNetUmbraco4PasswordHashAlgorithmName = "HMACSHA1";
}
}
}

View File

@@ -11,9 +11,6 @@ namespace Umbraco.Core.Security
/// </summary>
public class LegacyPasswordSecurity
{
// TODO: This class no longer has the logic available to verify the old old old password format, we should
// include this ability so that upgrades for very old versions/data can work and then auto-migrate to the new password format.
private readonly IPasswordConfiguration _passwordConfiguration;
private readonly PasswordGenerator _generator;
@@ -34,13 +31,14 @@ namespace Umbraco.Core.Security
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
public string HashPasswordForStorage(string password)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("password cannot be empty", nameof(password));
string salt;
var hashed = HashNewPassword(password, out salt);
var hashed = HashNewPassword(_passwordConfiguration.HashAlgorithmType, password, out salt);
return FormatPasswordForStorage(hashed, salt);
}
@@ -51,6 +49,7 @@ namespace Umbraco.Core.Security
/// <param name="hashedPassword"></param>
/// <param name="salt"></param>
/// <returns></returns>
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
public string FormatPasswordForStorage(string hashedPassword, string salt)
{
return salt + hashedPassword;
@@ -59,18 +58,24 @@ namespace Umbraco.Core.Security
/// <summary>
/// Hashes a password with a given salt
/// </summary>
/// <param name="algorithmType">The hashing algorithm for the password.</param>
/// <param name="pass"></param>
/// <param name="salt"></param>
/// <returns></returns>
public string HashPassword(string pass, string salt)
public string HashPassword(string algorithmType, string pass, string salt)
{
if (IsLegacySHA1Algorithm(algorithmType))
{
return HashLegacySHA1Password(pass);
}
//This is the correct way to implement this (as per the sql membership provider)
var bytes = Encoding.Unicode.GetBytes(pass);
var saltBytes = Convert.FromBase64String(salt);
byte[] inArray;
var hashAlgorithm = GetCurrentHashAlgorithm();
var hashAlgorithm = GetHashAlgorithm(algorithmType);
var algorithm = hashAlgorithm as KeyedHashAlgorithm;
if (algorithm != null)
{
@@ -116,43 +121,55 @@ namespace Umbraco.Core.Security
/// <summary>
/// Verifies if the password matches the expected hash+salt of the stored password string
/// </summary>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="password">The password.</param>
/// <param name="dbPassword">The value of the password stored in a data store.</param>
/// <returns></returns>
public bool VerifyPassword(string password, string dbPassword)
public bool VerifyPassword(string algorithm, string password, string dbPassword)
{
if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix))
return false;
var storedHashedPass = ParseStoredHashPassword(dbPassword, out var salt);
var hashed = HashPassword(password, salt);
var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt);
var hashed = HashPassword(algorithm, password, salt);
return storedHashedPass == hashed;
}
/// <summary>
/// Create a new password hash and a new salt
/// </summary>
/// <param name="algorithm">The hashing algorithm for the password.</param>
/// <param name="newPassword"></param>
/// <param name="salt"></param>
/// <returns></returns>
public string HashNewPassword(string newPassword, out string salt)
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
public string HashNewPassword(string algorithm, string newPassword, out string salt)
{
salt = GenerateSalt();
return HashPassword(newPassword, salt);
return HashPassword(algorithm, newPassword, salt);
}
/// <summary>
/// Parses out the hashed password and the salt from the stored password string value
/// </summary>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="storedString"></param>
/// <param name="salt">returns the salt</param>
/// <returns></returns>
public string ParseStoredHashPassword(string storedString, out string salt)
public string ParseStoredHashPassword(string algorithm, string storedString, out string salt)
{
if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
// This is for the <= v4 hashing algorithm for which there was no salt
if (IsLegacySHA1Algorithm(algorithm))
{
salt = string.Empty;
return storedString;
}
var saltLen = GenerateSalt();
salt = storedString.Substring(0, saltLen.Length);
return storedString.Substring(saltLen.Length);
@@ -168,28 +185,61 @@ namespace Umbraco.Core.Security
/// <summary>
/// Return the hash algorithm to use based on the <see cref="IPasswordConfiguration"/>
/// </summary>
/// <param name="algorithm">The hashing algorithm name.</param>
/// <param name="password"></param>
/// <returns></returns>
private HashAlgorithm GetCurrentHashAlgorithm()
private HashAlgorithm GetHashAlgorithm(string algorithm)
{
if (_passwordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace())
if (algorithm.IsNullOrWhiteSpace())
throw new InvalidOperationException("No hash algorithm type specified");
var alg = HashAlgorithm.Create(_passwordConfiguration.HashAlgorithmType);
var alg = HashAlgorithm.Create(algorithm);
if (alg == null)
throw new InvalidOperationException($"The hash algorithm specified {_passwordConfiguration.HashAlgorithmType} cannot be resolved");
throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved");
return alg;
}
public bool SupportHashAlgorithm(string algorithm)
{
if (algorithm.InvariantEquals(typeof(HMACSHA256).Name))
// This is for the v6-v8 hashing algorithm
if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName))
return true;
// This is for the <= v4 hashing algorithm
if (IsLegacySHA1Algorithm(algorithm))
return true;
// TODO: Need to add the old old old format in here too which was just HMACSHA1 IIRC but had a custom key implementation as the password itself
return false;
}
private bool IsLegacySHA1Algorithm(string algorithm) => algorithm.InvariantEquals(Constants.Security.AspNetUmbraco4PasswordHashAlgorithmName);
/// <summary>
/// Hashes the password with the old v4 algorithm
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The encoded password.</returns>
private string HashLegacySHA1Password(string password)
{
var hashAlgorithm = GetLegacySHA1Algorithm(password);
var hash = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
return hash;
}
/// <summary>
/// Returns the old v4 algorithm and settings
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
private HashAlgorithm GetLegacySHA1Algorithm(string password)
{
return new HMACSHA1
{
//the legacy salt was actually the password :(
Key = Encoding.Unicode.GetBytes(password)
};
}
}
}

View File

@@ -14,14 +14,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
[Test]
public void Check_Password_Hashed_Non_KeyedHashAlgorithm()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "SHA256"));
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "SHA256");
var passwordSecurity = new LegacyPasswordSecurity(passwordConfiguration);
string salt;
var pass = "ThisIsAHashedPassword";
var hashed = passwordSecurity.HashNewPassword(pass, out salt);
var hashed = passwordSecurity.HashNewPassword(passwordConfiguration.HashAlgorithmType, pass, out salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(hashed, salt);
var result = passwordSecurity.VerifyPassword("ThisIsAHashedPassword", storedPassword);
var result = passwordSecurity.VerifyPassword(passwordConfiguration.HashAlgorithmType, "ThisIsAHashedPassword", storedPassword);
Assert.IsTrue(result);
}
@@ -29,14 +30,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
[Test]
public void Check_Password_Hashed_KeyedHashAlgorithm()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
var passwordSecurity = new LegacyPasswordSecurity(passwordConfiguration);
string salt;
var pass = "ThisIsAHashedPassword";
var hashed = passwordSecurity.HashNewPassword(pass, out salt);
var hashed = passwordSecurity.HashNewPassword(passwordConfiguration.HashAlgorithmType, pass, out salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(hashed, salt);
var result = passwordSecurity.VerifyPassword("ThisIsAHashedPassword", storedPassword);
var result = passwordSecurity.VerifyPassword(passwordConfiguration.HashAlgorithmType, "ThisIsAHashedPassword", storedPassword);
Assert.IsTrue(result);
}
@@ -57,13 +59,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
[Test]
public void Get_Stored_Password_Hashed()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
var passwordSecurity = new LegacyPasswordSecurity(passwordConfiguration);
var salt = LegacyPasswordSecurity.GenerateSalt();
var stored = salt + "ThisIsAHashedPassword";
string initSalt;
var result = passwordSecurity.ParseStoredHashPassword(stored, out initSalt);
var result = passwordSecurity.ParseStoredHashPassword(passwordConfiguration.HashAlgorithmType, stored, out initSalt);
Assert.AreEqual("ThisIsAHashedPassword", result);
}

View File

@@ -106,8 +106,8 @@ namespace Umbraco.Tests.Membership
Assert.AreNotEqual("test", createdMember.RawPasswordValue);
string salt;
var storedPassword = provider.PasswordSecurity.ParseStoredHashPassword(createdMember.RawPasswordValue, out salt);
var hashedPassword = provider.PasswordSecurity.HashPassword("testtest$1", salt);
var storedPassword = provider.PasswordSecurity.ParseStoredHashPassword(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName, createdMember.RawPasswordValue, out salt);
var hashedPassword = provider.PasswordSecurity.HashPassword(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName, "testtest$1", salt);
Assert.AreEqual(hashedPassword, storedPassword);
}

View File

@@ -13,12 +13,12 @@ namespace Umbraco.Web.BackOffice.Security
/// </summary>
public class BackOfficePasswordHasher : PasswordHasher<BackOfficeIdentityUser>
{
private readonly LegacyPasswordSecurity _passwordSecurity;
private readonly LegacyPasswordSecurity _legacyPasswordSecurity;
private readonly IJsonSerializer _jsonSerializer;
public BackOfficePasswordHasher(LegacyPasswordSecurity passwordSecurity, IJsonSerializer jsonSerializer)
{
_passwordSecurity = passwordSecurity;
_legacyPasswordSecurity = passwordSecurity;
_jsonSerializer = jsonSerializer;
}
@@ -27,9 +27,12 @@ namespace Umbraco.Web.BackOffice.Security
if (!user.PasswordConfig.IsNullOrWhiteSpace())
{
// check if the (legacy) password security supports this hash algorith and if so then use it
// TODO: I don't think we really want to do this because we want to migrate all old password formats
// to the new format, not keep using it. AFAIK this block should be removed along with the code within the
// LegacyPasswordSecurity except the part that just validates old ones.
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
if (_passwordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
return _passwordSecurity.HashPasswordForStorage(password);
if (_legacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
return _legacyPasswordSecurity.HashPasswordForStorage(password);
// We will explicitly detect names here since this allows us to future proof these checks.
// The default is PBKDF2.ASPNETCORE.V3:
@@ -58,9 +61,9 @@ namespace Umbraco.Web.BackOffice.Security
{
// check if the (legacy) password security supports this hash algorith and if so then use it
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
if (_passwordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
if (_legacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
{
var result = _passwordSecurity.VerifyPassword(providedPassword, hashedPassword);
var result = _legacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
return result
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;

View File

@@ -7,15 +7,14 @@ using Microsoft.Extensions.Options;
using Microsoft.Owin.Security.DataProtection;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Mapping;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Net;
namespace Umbraco.Web.Security
{
// TODO: Most of this is already migrated to netcore, there's probably not much more to go and then we can complete remove it
public class BackOfficeOwinUserManager : BackOfficeUserManager
{
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
@@ -118,11 +117,6 @@ namespace Umbraco.Web.Security
#endregion
protected override IPasswordHasher<BackOfficeIdentityUser> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
{
return new UserAwarePasswordHasher<BackOfficeIdentityUser>(new LegacyPasswordSecurity(passwordConfiguration));
}
protected void InitUserManager(BackOfficeOwinUserManager manager, IDataProtectionProvider dataProtectionProvider)
{
// use a custom hasher based on our membership provider

View File

@@ -83,7 +83,7 @@ namespace Umbraco.Web.Security.Providers
if (m == null) return false;
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(newPassword, out salt);
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, newPassword, out salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(encodedPassword, salt);
m.LastPasswordChangeDate = DateTime.Now;
@@ -143,7 +143,7 @@ namespace Umbraco.Web.Security.Providers
}
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(password, out salt);
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, password, out salt);
var member = MemberService.CreateWithIdentity(
username,
@@ -406,7 +406,7 @@ namespace Umbraco.Web.Security.Providers
}
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(generatedPassword, out salt);
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, generatedPassword, out salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(encodedPassword, salt);
m.LastPasswordChangeDate = DateTime.Now;
MemberService.Save(m);
@@ -519,7 +519,7 @@ namespace Umbraco.Web.Security.Providers
};
}
var authenticated = PasswordSecurity.VerifyPassword(password, member.RawPasswordValue);
var authenticated = PasswordSecurity.VerifyPassword(Membership.HashAlgorithmType, password, member.RawPasswordValue);
var requiresFullSave = false;

View File

@@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Security;
namespace Umbraco.Web.Security
{
public class UserAwarePasswordHasher<T> : IPasswordHasher<T>
where T : BackOfficeIdentityUser
{
private readonly LegacyPasswordSecurity _passwordSecurity;
public UserAwarePasswordHasher(LegacyPasswordSecurity passwordSecurity)
{
_passwordSecurity = passwordSecurity;
}
public string HashPassword(string password)
{
return _passwordSecurity.HashPasswordForStorage(password);
}
public string HashPassword(T user, string password)
{
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return HashPassword(password);
}
public PasswordVerificationResult VerifyHashedPassword(T user, string hashedPassword, string providedPassword)
{
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword)
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
}
}

View File

@@ -194,7 +194,6 @@
<Compile Include="Security\OwinDataProtectorTokenProvider.cs" />
<Compile Include="Security\PublicAccessChecker.cs" />
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />
<Compile Include="Security\UserAwarePasswordHasher.cs" />
<Compile Include="StringExtensions.cs" />
<Compile Include="UmbracoContext.cs" />
<Compile Include="UmbracoContextFactory.cs" />