using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { /// /// Handles password hashing and formatting /// public class PasswordSecurity { public IPasswordConfiguration PasswordConfiguration { get; } public PasswordGenerator _generator; public ConfiguredPasswordValidator _validator; /// /// Constructor /// /// public PasswordSecurity(IPasswordConfiguration passwordConfiguration) { PasswordConfiguration = passwordConfiguration; } /// /// Checks if the password passes validation rules /// /// /// public async Task>> IsValidPasswordAsync(string password) { if (_validator == null) _validator = new ConfiguredPasswordValidator(PasswordConfiguration); var result = await _validator.ValidateAsync(password); if (result.Succeeded) return Attempt>.Succeed(); return Attempt>.Fail(result.Errors); } public string GeneratePassword() { if (_generator == null) _generator = new PasswordGenerator(PasswordConfiguration); return _generator.GeneratePassword(); } /// /// Returns a hashed password value used to store in a data store /// /// /// public string HashPasswordForStorage(string password) { string salt; var hashed = HashNewPassword(password, out salt); return FormatPasswordForStorage(hashed, salt); } /// /// If the password format is a hashed keyed algorithm then we will pre-pend the salt used to hash the password /// to the hashed password itself. /// /// /// /// public string FormatPasswordForStorage(string hashedPassword, string salt) { if (PasswordConfiguration.UseLegacyEncoding) { return hashedPassword; } return salt + hashedPassword; } /// /// Hashes a password with a given salt /// /// /// /// public string HashPassword(string pass, string salt) { //if we are doing it the old way if (PasswordConfiguration.UseLegacyEncoding) { return LegacyEncodePassword(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 = GetHashAlgorithm(pass); var algorithm = hashAlgorithm as KeyedHashAlgorithm; if (algorithm != null) { var keyedHashAlgorithm = algorithm; if (keyedHashAlgorithm.Key.Length == saltBytes.Length) { //if the salt bytes is the required key length for the algorithm, use it as-is keyedHashAlgorithm.Key = saltBytes; } else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) { //if the salt bytes is too long for the required key length for the algorithm, reduce it var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); keyedHashAlgorithm.Key = numArray2; } else { //if the salt bytes is too short for the required key length for the algorithm, extend it var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; var dstOffset = 0; while (dstOffset < numArray2.Length) { var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); dstOffset += count; } keyedHashAlgorithm.Key = numArray2; } inArray = keyedHashAlgorithm.ComputeHash(bytes); } else { var buffer = new byte[saltBytes.Length + bytes.Length]; Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); inArray = hashAlgorithm.ComputeHash(buffer); } return Convert.ToBase64String(inArray); } /// /// Verifies if the password matches the expected hash+salt of the stored password string /// /// The password. /// The value of the password stored in a data store. /// public bool VerifyPassword(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); return storedHashedPass == hashed; } /// /// Create a new password hash and a new salt /// /// /// /// public string HashNewPassword(string newPassword, out string salt) { salt = GenerateSalt(); return HashPassword(newPassword, salt); } /// /// Parses out the hashed password and the salt from the stored password string value /// /// /// returns the salt /// public string ParseStoredHashPassword(string storedString, out string salt) { if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); if (PasswordConfiguration.UseLegacyEncoding) { salt = string.Empty; return storedString; } var saltLen = GenerateSalt(); salt = storedString.Substring(0, saltLen.Length); return storedString.Substring(saltLen.Length); } public static string GenerateSalt() { var numArray = new byte[16]; new RNGCryptoServiceProvider().GetBytes(numArray); return Convert.ToBase64String(numArray); } /// /// Return the hash algorithm to use /// /// /// public HashAlgorithm GetHashAlgorithm(string password) { if (PasswordConfiguration.UseLegacyEncoding) { return new HMACSHA1 { //the legacy salt was actually the password :( Key = Encoding.Unicode.GetBytes(password) }; } if (PasswordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace()) throw new InvalidOperationException("No hash algorithm type specified"); var alg = HashAlgorithm.Create(PasswordConfiguration.HashAlgorithmType); if (alg == null) throw new InvalidOperationException($"The hash algorithm specified {PasswordConfiguration.HashAlgorithmType} cannot be resolved"); return alg; } /// /// Encodes the password. /// /// The password. /// The encoded password. private string LegacyEncodePassword(string password) { var hashAlgorith = GetHashAlgorithm(password); var encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password))); return encodedPassword; } } }