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;
}
}
}