aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2019-03-07 19:11:36 +0100
committerGitHub <noreply@github.com>2019-03-07 19:11:36 +0100
commitae0ecc1b10982d9240ecdcc82cb7299fc708aafb (patch)
treefbdce662f046bd191bd07385c1e27fe152dff1b2
parent8a53b609127cca6feee06b34f12b6d21a9eb37c7 (diff)
parentf486f5966f2fb9a3cf266ee816b8c247f0de5482 (diff)
Merge pull request #870 from LogicalPhallacy/betterauth
Better default authentication
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs129
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserRepository.cs34
-rw-r--r--Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs141
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs37
-rw-r--r--MediaBrowser.Model/Cryptography/ICryptoProvider.cs9
-rw-r--r--MediaBrowser.Model/Cryptography/PasswordHash.cs153
6 files changed, 459 insertions, 44 deletions
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 09fdbc856..982bba625 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -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;
+ }
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
index db359d7dd..182df0edc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
@@ -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>
diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
index 4013ac0c8..3ec1f81d3 100644
--- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
+++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
@@ -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;
+ ConvertPasswordFormat(user);
+ // This is needed to support changing a no password user to a password user
+ if (string.IsNullOrEmpty(user.Password))
+ {
+ 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 (newPassword != null)
+ PasswordHash passwordHash = new PasswordHash(user.Password);
+ if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
{
- newPasswordHash = GetHashedString(user, newPassword);
+ 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);
}
- if (string.IsNullOrWhiteSpace(newPasswordHash))
+ if (string.IsNullOrWhiteSpace(passwordHash.Hash))
{
- throw new ArgumentNullException(nameof(newPasswordHash));
+ throw new ArgumentNullException(nameof(passwordHash.Hash));
}
- user.Password = newPasswordHash;
+ 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))
+ {
+ passwordHash = new PasswordHash(_cryptographyProvider);
+ }
+ else
{
- // return BCrypt.HashPassword(str, salt);
+ 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)));
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index dfef8e997..efb1ef4a5 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -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)))
diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
index b027d2ad0..5988112c2 100644
--- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
+++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
@@ -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; }
}
}
diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs
new file mode 100644
index 000000000..a9d0f6744
--- /dev/null
+++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs
@@ -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;
+ }
+ }
+
+}