Merge pull request #870 from LogicalPhallacy/betterauth
Better default authentication
This commit is contained in:
commit
ae0ecc1b10
|
@ -1,13 +1,49 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
public class CryptographyProvider : ICryptoProvider
|
||||
{
|
||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||
{
|
||||
"MD5",
|
||||
"System.Security.Cryptography.MD5",
|
||||
"SHA",
|
||||
"SHA1",
|
||||
"System.Security.Cryptography.SHA1",
|
||||
"SHA256",
|
||||
"SHA-256",
|
||||
"System.Security.Cryptography.SHA256",
|
||||
"SHA384",
|
||||
"SHA-384",
|
||||
"System.Security.Cryptography.SHA384",
|
||||
"SHA512",
|
||||
"SHA-512",
|
||||
"System.Security.Cryptography.SHA512"
|
||||
};
|
||||
|
||||
public string DefaultHashMethod => "PBKDF2";
|
||||
|
||||
private RandomNumberGenerator _randomNumberGenerator;
|
||||
|
||||
private const int _defaultIterations = 1000;
|
||||
|
||||
public CryptographyProvider()
|
||||
{
|
||||
//FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
||||
//Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
|
||||
//there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
|
||||
//Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
|
||||
_randomNumberGenerator = RandomNumberGenerator.Create();
|
||||
}
|
||||
|
||||
public Guid GetMD5(string str)
|
||||
{
|
||||
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
|
||||
|
@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
return provider.ComputeHash(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetSupportedHashMethods()
|
||||
{
|
||||
return _supportedHashMethods;
|
||||
}
|
||||
|
||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
||||
{
|
||||
//downgrading for now as we need this library to be dotnetstandard compliant
|
||||
//with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
||||
if (method == DefaultHashMethod)
|
||||
{
|
||||
using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
|
||||
{
|
||||
return r.GetBytes(32);
|
||||
}
|
||||
}
|
||||
|
||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes)
|
||||
{
|
||||
return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
|
||||
}
|
||||
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
|
||||
{
|
||||
return ComputeHash(DefaultHashMethod, bytes);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
||||
{
|
||||
if (hashMethod == DefaultHashMethod)
|
||||
{
|
||||
return PBKDF2(hashMethod, bytes, salt, _defaultIterations);
|
||||
}
|
||||
else if (_supportedHashMethods.Contains(hashMethod))
|
||||
{
|
||||
using (var h = HashAlgorithm.Create(hashMethod))
|
||||
{
|
||||
if (salt.Length == 0)
|
||||
{
|
||||
return h.ComputeHash(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] salted = new byte[bytes.Length + salt.Length];
|
||||
Array.Copy(bytes, salted, bytes.Length);
|
||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
||||
return h.ComputeHash(salted);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
||||
{
|
||||
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(PasswordHash hash)
|
||||
{
|
||||
int iterations = _defaultIterations;
|
||||
if (!hash.Parameters.ContainsKey("iterations"))
|
||||
{
|
||||
hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
iterations = int.Parse(hash.Parameters["iterations"]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e);
|
||||
}
|
||||
}
|
||||
|
||||
return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
|
||||
}
|
||||
|
||||
public byte[] GenerateSalt()
|
||||
{
|
||||
byte[] salt = new byte[64];
|
||||
_randomNumberGenerator.GetBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
TryMigrateToLocalUsersTable(connection);
|
||||
}
|
||||
|
||||
RemoveEmptyPasswordHashes();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data
|
|||
}
|
||||
}
|
||||
|
||||
private void RemoveEmptyPasswordHashes()
|
||||
{
|
||||
foreach (var user in RetrieveAllUsers())
|
||||
{
|
||||
// If the user password is the sha1 hash of the empty string, remove it
|
||||
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|
||||
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
user.Password = null;
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = CreateConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||
{
|
||||
statement.TryBind("@InternalId", user.InternalId);
|
||||
statement.TryBind("@data", serialized);
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a user in the repo
|
||||
/// </summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
|
@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
|
|||
public string Name => "Default";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
|
||||
// This is dumb and an artifact of the backwards way auth providers were designed.
|
||||
// This version of authenticate was never meant to be called, but needs to be here for interface compat
|
||||
// Only the providers that don't provide local user support use this
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
// This is the verson that we need to use for local users. Because reasons.
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
||||
{
|
||||
bool success = false;
|
||||
if (resolvedUser == null)
|
||||
{
|
||||
throw new Exception("Invalid username or password");
|
||||
}
|
||||
|
||||
var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
||||
// As long as jellyfin supports passwordless users, we need this little block here to accomodate
|
||||
if (IsPasswordEmpty(resolvedUser, password))
|
||||
{
|
||||
return Task.FromResult(new ProviderAuthenticationResult
|
||||
{
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
ConvertPasswordFormat(resolvedUser);
|
||||
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
|
||||
|
||||
PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
|
||||
byte[] calculatedHash;
|
||||
string calculatedHashString;
|
||||
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
|
||||
{
|
||||
if (string.IsNullOrEmpty(readyHash.Salt))
|
||||
{
|
||||
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
|
||||
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
|
||||
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
||||
}
|
||||
|
||||
if (calculatedHashString == readyHash.Hash)
|
||||
{
|
||||
success = true;
|
||||
// throw new Exception("Invalid username or password");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
|
||||
}
|
||||
|
||||
// var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
|
@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
|
|||
});
|
||||
}
|
||||
|
||||
// This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
|
||||
// but at least they are in the new format.
|
||||
private void ConvertPasswordFormat(User user)
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.Password.Contains("$"))
|
||||
{
|
||||
string hash = user.Password;
|
||||
user.Password = string.Format("$SHA1${0}", hash);
|
||||
}
|
||||
|
||||
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
|
||||
{
|
||||
string hash = user.EasyPassword;
|
||||
user.EasyPassword = string.Format("$SHA1${0}", hash);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> HasPassword(User user)
|
||||
{
|
||||
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
|
||||
return Task.FromResult(hasConfiguredPassword);
|
||||
}
|
||||
|
||||
private bool IsPasswordEmpty(User user, string passwordHash)
|
||||
private bool IsPasswordEmpty(User user, string password)
|
||||
{
|
||||
return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
||||
return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
|
||||
}
|
||||
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
string newPasswordHash = null;
|
||||
|
||||
if (newPassword != null)
|
||||
ConvertPasswordFormat(user);
|
||||
// This is needed to support changing a no password user to a password user
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
newPasswordHash = GetHashedString(user, newPassword);
|
||||
PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
|
||||
newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||
newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
|
||||
newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||
newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
|
||||
user.Password = newPasswordHash.ToString();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPasswordHash))
|
||||
PasswordHash passwordHash = new PasswordHash(user.Password);
|
||||
if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newPasswordHash));
|
||||
passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||
passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
|
||||
passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||
passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
|
||||
}
|
||||
else if (newPassword != null)
|
||||
{
|
||||
passwordHash.Hash = GetHashedString(user, newPassword);
|
||||
}
|
||||
|
||||
user.Password = newPasswordHash;
|
||||
if (string.IsNullOrWhiteSpace(passwordHash.Hash))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(passwordHash.Hash));
|
||||
}
|
||||
|
||||
user.Password = passwordHash.ToString();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetPasswordHash(User user)
|
||||
{
|
||||
return string.IsNullOrEmpty(user.Password)
|
||||
? GetEmptyHashedString(user)
|
||||
: user.Password;
|
||||
return user.Password;
|
||||
}
|
||||
|
||||
public string GetEmptyHashedString(User user)
|
||||
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
|
||||
{
|
||||
return GetHashedString(user, string.Empty);
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
|
|||
/// </summary>
|
||||
public string GetHashedString(User user, string str)
|
||||
{
|
||||
var salt = user.Salt;
|
||||
if (salt != null)
|
||||
PasswordHash passwordHash;
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
// return BCrypt.HashPassword(str, salt);
|
||||
passwordHash = new PasswordHash(_cryptographyProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvertPasswordFormat(user);
|
||||
passwordHash = new PasswordHash(user.Password);
|
||||
}
|
||||
|
||||
// legacy
|
||||
return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
|
||||
if (passwordHash.SaltBytes != null)
|
||||
{
|
||||
// the password is modern format with PBKDF and we should take advantage of that
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||
}
|
||||
else
|
||||
{
|
||||
// the password has no salt and should be called with the older method for safety
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Events;
|
||||
|
@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
public bool IsValidUsername(string username)
|
||||
public static bool IsValidUsername(string username)
|
||||
{
|
||||
// Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
||||
foreach (var currentChar in username)
|
||||
{
|
||||
if (!IsValidUsernameCharacter(currentChar))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
//This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||
//In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
||||
return Regex.IsMatch(username, "^[\\w-'._@]*$");
|
||||
}
|
||||
|
||||
private static bool IsValidUsernameCharacter(char i)
|
||||
{
|
||||
return !char.Equals(i, '<') && !char.Equals(i, '>');
|
||||
return IsValidUsername(i.ToString());
|
||||
}
|
||||
|
||||
public string MakeValidUsername(string username)
|
||||
|
@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
|
|||
private string GetLocalPasswordHash(User user)
|
||||
{
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? _defaultAuthenticationProvider.GetEmptyHashedString(user)
|
||||
? null
|
||||
: user.EasyPassword;
|
||||
}
|
||||
|
||||
private bool IsPasswordEmpty(User user, string passwordHash)
|
||||
{
|
||||
return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the users from the repository
|
||||
/// </summary>
|
||||
|
@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
|
|||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
|
||||
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
|
||||
|
||||
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||
hasConfiguredEasyPassword :
|
||||
hasConfiguredPassword;
|
||||
|
||||
var dto = new UserDto
|
||||
UserDto dto = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Name,
|
||||
|
@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
|
|||
dto.EnableAutoLogin = true;
|
||||
}
|
||||
|
||||
var image = user.GetImageInfo(ImageType.Primary, 0);
|
||||
ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
|
@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (!IsValidUsername(name))
|
||||
{
|
||||
throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
||||
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
||||
}
|
||||
|
||||
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
|
@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography
|
|||
byte[] ComputeMD5(Stream str);
|
||||
byte[] ComputeMD5(byte[] bytes);
|
||||
byte[] ComputeSHA1(byte[] bytes);
|
||||
IEnumerable<string> GetSupportedHashMethods();
|
||||
byte[] ComputeHash(string HashMethod, byte[] bytes);
|
||||
byte[] ComputeHashWithDefaultMethod(byte[] bytes);
|
||||
byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt);
|
||||
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
|
||||
byte[] ComputeHash(PasswordHash hash);
|
||||
byte[] GenerateSalt();
|
||||
string DefaultHashMethod { get; }
|
||||
}
|
||||
}
|
||||
|
|
153
MediaBrowser.Model/Cryptography/PasswordHash.cs
Normal file
153
MediaBrowser.Model/Cryptography/PasswordHash.cs
Normal file
|
@ -0,0 +1,153 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
public class PasswordHash
|
||||
{
|
||||
// Defined from this hash storage spec
|
||||
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
// $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
|
||||
// with one slight amendment to ease the transition, we're writing out the bytes in hex
|
||||
// rather than making them a BASE64 string with stripped padding
|
||||
|
||||
private string _id;
|
||||
|
||||
private Dictionary<string, string> _parameters = new Dictionary<string, string>();
|
||||
|
||||
private string _salt;
|
||||
|
||||
private byte[] _saltBytes;
|
||||
|
||||
private string _hash;
|
||||
|
||||
private byte[] _hashBytes;
|
||||
|
||||
public string Id { get => _id; set => _id = value; }
|
||||
|
||||
public Dictionary<string, string> Parameters { get => _parameters; set => _parameters = value; }
|
||||
|
||||
public string Salt { get => _salt; set => _salt = value; }
|
||||
|
||||
public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; }
|
||||
|
||||
public string Hash { get => _hash; set => _hash = value; }
|
||||
|
||||
public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; }
|
||||
|
||||
public PasswordHash(string storageString)
|
||||
{
|
||||
string[] splitted = storageString.Split('$');
|
||||
_id = splitted[1];
|
||||
if (splitted[2].Contains("="))
|
||||
{
|
||||
foreach (string paramset in (splitted[2].Split(',')))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(paramset))
|
||||
{
|
||||
string[] fields = paramset.Split('=');
|
||||
if (fields.Length == 2)
|
||||
{
|
||||
_parameters.Add(fields[0], fields[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Malformed parameter in password hash string {paramset}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (splitted.Length == 5)
|
||||
{
|
||||
_salt = splitted[3];
|
||||
_saltBytes = ConvertFromByteString(_salt);
|
||||
_hash = splitted[4];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_salt = string.Empty;
|
||||
_hash = splitted[3];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (splitted.Length == 4)
|
||||
{
|
||||
_salt = splitted[2];
|
||||
_saltBytes = ConvertFromByteString(_salt);
|
||||
_hash = splitted[3];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_salt = string.Empty;
|
||||
_hash = splitted[2];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public PasswordHash(ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_id = cryptoProvider.DefaultHashMethod;
|
||||
_saltBytes = cryptoProvider.GenerateSalt();
|
||||
_salt = ConvertToByteString(SaltBytes);
|
||||
}
|
||||
|
||||
public static byte[] ConvertFromByteString(string byteString)
|
||||
{
|
||||
byte[] bytes = new byte[byteString.Length / 2];
|
||||
for (int i = 0; i < byteString.Length; i += 2)
|
||||
{
|
||||
// TODO: NetStandard2.1 switch this to use a span instead of a substring.
|
||||
bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static string ConvertToByteString(byte[] bytes)
|
||||
{
|
||||
return BitConverter.ToString(bytes).Replace("-", "");
|
||||
}
|
||||
|
||||
private string SerializeParameters()
|
||||
{
|
||||
string returnString = string.Empty;
|
||||
foreach (var KVP in _parameters)
|
||||
{
|
||||
returnString += $",{KVP.Key}={KVP.Value}";
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(returnString)) && returnString[0] == ',')
|
||||
{
|
||||
returnString = returnString.Remove(0, 1);
|
||||
}
|
||||
|
||||
return returnString;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string outString = "$" + _id;
|
||||
string paramstring = SerializeParameters();
|
||||
if (!string.IsNullOrEmpty(paramstring))
|
||||
{
|
||||
outString += $"${paramstring}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_salt))
|
||||
{
|
||||
outString += $"${_salt}";
|
||||
}
|
||||
|
||||
outString += $"${_hash}";
|
||||
return outString;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user