Adds support for the super old password format so we can handle upgrades
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user