mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-04-24 05:57:20 -04:00
Streamline authentication proccess
This commit is contained in:
parent
4f2d601f02
commit
0f897589ed
11 changed files with 372 additions and 369 deletions
|
@ -8,7 +8,7 @@ using MediaBrowser.Model.Cryptography;
|
|||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
public class CryptographyProvider : ICryptoProvider
|
||||
public class CryptographyProvider : ICryptoProvider, IDisposable
|
||||
{
|
||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||
{
|
||||
|
@ -28,26 +28,28 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
"System.Security.Cryptography.SHA512"
|
||||
};
|
||||
|
||||
public string DefaultHashMethod => "PBKDF2";
|
||||
|
||||
private RandomNumberGenerator _randomNumberGenerator;
|
||||
|
||||
private const int _defaultIterations = 1000;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
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
|
||||
// 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)));
|
||||
}
|
||||
public string DefaultHashMethod => "PBKDF2";
|
||||
|
||||
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
|
||||
public Guid GetMD5(string str)
|
||||
=> new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
|
||||
|
||||
[Obsolete("Use System.Security.Cryptography.SHA1 directly")]
|
||||
public byte[] ComputeSHA1(byte[] bytes)
|
||||
{
|
||||
using (var provider = SHA1.Create())
|
||||
|
@ -56,6 +58,7 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
|
||||
public byte[] ComputeMD5(Stream str)
|
||||
{
|
||||
using (var provider = MD5.Create())
|
||||
|
@ -64,6 +67,7 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
|
||||
public byte[] ComputeMD5(byte[] bytes)
|
||||
{
|
||||
using (var provider = MD5.Create())
|
||||
|
@ -73,9 +77,7 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
}
|
||||
|
||||
public IEnumerable<string> GetSupportedHashMethods()
|
||||
{
|
||||
return _supportedHashMethods;
|
||||
}
|
||||
=> _supportedHashMethods;
|
||||
|
||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
||||
{
|
||||
|
@ -93,14 +95,10 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
}
|
||||
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes)
|
||||
{
|
||||
return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
|
||||
}
|
||||
=> ComputeHash(hashMethod, bytes, Array.Empty<byte>());
|
||||
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
|
||||
{
|
||||
return ComputeHash(DefaultHashMethod, bytes);
|
||||
}
|
||||
=> ComputeHash(DefaultHashMethod, bytes);
|
||||
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
||||
{
|
||||
|
@ -125,37 +123,27 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
}
|
||||
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
|
||||
}
|
||||
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
||||
{
|
||||
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
|
||||
}
|
||||
=> 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));
|
||||
hash.Parameters.Add("iterations", iterations.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
else if (!int.TryParse(hash.Parameters["iterations"], out iterations))
|
||||
{
|
||||
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);
|
||||
}
|
||||
throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}");
|
||||
}
|
||||
|
||||
return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
|
||||
return PBKDF2(hash.Id, hash.Hash, hash.Salt, iterations);
|
||||
}
|
||||
|
||||
public byte[] GenerateSalt()
|
||||
|
@ -164,5 +152,29 @@ namespace Emby.Server.Implementations.Cryptography
|
|||
_randomNumberGenerator.GetBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_randomNumberGenerator.Dispose();
|
||||
}
|
||||
|
||||
_randomNumberGenerator = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ namespace Emby.Server.Implementations.Library
|
|||
public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser
|
||||
{
|
||||
private readonly ICryptoProvider _cryptographyProvider;
|
||||
public DefaultAuthenticationProvider(ICryptoProvider crypto)
|
||||
public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider)
|
||||
{
|
||||
_cryptographyProvider = crypto;
|
||||
_cryptographyProvider = cryptographyProvider;
|
||||
}
|
||||
|
||||
public string Name => "Default";
|
||||
|
@ -28,17 +28,17 @@ namespace Emby.Server.Implementations.Library
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// This is the verson that we need to use for local users. Because reasons.
|
||||
// This is the version 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");
|
||||
throw new ArgumentNullException(nameof(resolvedUser));
|
||||
}
|
||||
|
||||
// As long as jellyfin supports passwordless users, we need this little block here to accomodate
|
||||
if (IsPasswordEmpty(resolvedUser, password))
|
||||
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
|
||||
{
|
||||
return Task.FromResult(new ProviderAuthenticationResult
|
||||
{
|
||||
|
@ -50,37 +50,24 @@ namespace Emby.Server.Implementations.Library
|
|||
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
|
||||
|
||||
PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
|
||||
byte[] calculatedHash;
|
||||
string calculatedHashString;
|
||||
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id)
|
||||
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
|
||||
|| _cryptographyProvider.DefaultHashMethod == 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);
|
||||
}
|
||||
byte[] calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.Salt);
|
||||
|
||||
if (calculatedHashString == readyHash.Hash)
|
||||
if (calculatedHash.SequenceEqual(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}"));
|
||||
throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}");
|
||||
}
|
||||
|
||||
// var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Exception("Invalid username or password");
|
||||
throw new AuthenticationException("Invalid username or password");
|
||||
}
|
||||
|
||||
return Task.FromResult(new ProviderAuthenticationResult
|
||||
|
@ -98,29 +85,22 @@ namespace Emby.Server.Implementations.Library
|
|||
return;
|
||||
}
|
||||
|
||||
if (!user.Password.Contains("$"))
|
||||
if (user.Password.IndexOf('$') == -1)
|
||||
{
|
||||
string hash = user.Password;
|
||||
user.Password = string.Format("$SHA1${0}", hash);
|
||||
}
|
||||
|
||||
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
|
||||
if (user.EasyPassword != null
|
||||
&& user.EasyPassword.IndexOf('$') == -1)
|
||||
{
|
||||
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 password)
|
||||
{
|
||||
return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
|
||||
}
|
||||
public bool HasPassword(User user)
|
||||
=> !string.IsNullOrEmpty(user.Password);
|
||||
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
|
@ -129,30 +109,24 @@ namespace Emby.Server.Implementations.Library
|
|||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
|
||||
newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||
newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
|
||||
newPasswordHash.Salt = _cryptographyProvider.GenerateSalt();
|
||||
newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||
newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
|
||||
newPasswordHash.Hash = GetHashedChangeAuth(newPassword, newPasswordHash);
|
||||
user.Password = newPasswordHash.ToString();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
PasswordHash passwordHash = new PasswordHash(user.Password);
|
||||
if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
|
||||
if (passwordHash.Id == "SHA1"
|
||||
&& passwordHash.Salt.Length == 0)
|
||||
{
|
||||
passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||
passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
|
||||
passwordHash.Salt = _cryptographyProvider.GenerateSalt();
|
||||
passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||
passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
|
||||
passwordHash.Hash = GetHashedChangeAuth(newPassword, passwordHash);
|
||||
}
|
||||
else if (newPassword != null)
|
||||
{
|
||||
passwordHash.Hash = GetHashedString(user, newPassword);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(passwordHash.Hash))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(passwordHash.Hash));
|
||||
passwordHash.Hash = GetHashed(user, newPassword);
|
||||
}
|
||||
|
||||
user.Password = passwordHash.ToString();
|
||||
|
@ -160,11 +134,6 @@ namespace Emby.Server.Implementations.Library
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetPasswordHash(User user)
|
||||
{
|
||||
return user.Password;
|
||||
}
|
||||
|
||||
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
|
||||
{
|
||||
ConvertPasswordFormat(user);
|
||||
|
@ -190,13 +159,13 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? null
|
||||
: (new PasswordHash(user.EasyPassword)).Hash;
|
||||
: PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash);
|
||||
}
|
||||
|
||||
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
|
||||
internal byte[] GetHashedChangeAuth(string newPassword, PasswordHash passwordHash)
|
||||
{
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||
passwordHash.Hash = Encoding.UTF8.GetBytes(newPassword);
|
||||
return _cryptographyProvider.ComputeHash(passwordHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -215,10 +184,10 @@ namespace Emby.Server.Implementations.Library
|
|||
passwordHash = new PasswordHash(user.Password);
|
||||
}
|
||||
|
||||
if (passwordHash.SaltBytes != null)
|
||||
if (passwordHash.Salt != null)
|
||||
{
|
||||
// the password is modern format with PBKDF and we should take advantage of that
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
|
||||
passwordHash.Hash = Encoding.UTF8.GetBytes(str);
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||
}
|
||||
else
|
||||
|
@ -227,5 +196,31 @@ namespace Emby.Server.Implementations.Library
|
|||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] GetHashed(User user, string str)
|
||||
{
|
||||
PasswordHash passwordHash;
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
passwordHash = new PasswordHash(_cryptographyProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvertPasswordFormat(user);
|
||||
passwordHash = new PasswordHash(user.Password);
|
||||
}
|
||||
|
||||
if (passwordHash.Salt != null)
|
||||
{
|
||||
// the password is modern format with PBKDF and we should take advantage of that
|
||||
passwordHash.Hash = Encoding.UTF8.GetBytes(str);
|
||||
return _cryptographyProvider.ComputeHash(passwordHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
// the password has no salt and should be called with the older method for safety
|
||||
return _cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,132 +1,125 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public class DefaultPasswordResetProvider : IPasswordResetProvider
|
||||
{
|
||||
public string Name => "Default Password Reset Provider";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
private readonly string _passwordResetFileBase;
|
||||
private readonly string _passwordResetFileBaseDir;
|
||||
private readonly string _passwordResetFileBaseName = "passwordreset";
|
||||
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ICryptoProvider _crypto;
|
||||
|
||||
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
||||
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_userManager = userManager;
|
||||
_crypto = cryptoProvider;
|
||||
}
|
||||
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
SerializablePasswordReset spr;
|
||||
HashSet<string> usersreset = new HashSet<string>();
|
||||
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
|
||||
{
|
||||
using (var str = File.OpenRead(resetfile))
|
||||
{
|
||||
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (spr.ExpirationDate < DateTime.Now)
|
||||
{
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var resetUser = _userManager.GetUserByName(spr.UserName);
|
||||
if (resetUser == null)
|
||||
{
|
||||
throw new Exception($"User with a username of {spr.UserName} not found");
|
||||
}
|
||||
|
||||
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||
usersreset.Add(resetUser.Name);
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
}
|
||||
|
||||
if (usersreset.Count < 1)
|
||||
{
|
||||
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PinRedeemResult
|
||||
{
|
||||
Success = true,
|
||||
UsersReset = usersreset.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
|
||||
{
|
||||
string pin = string.Empty;
|
||||
using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
cryptoRandom.GetBytes(bytes);
|
||||
pin = BitConverter.ToString(bytes);
|
||||
}
|
||||
|
||||
DateTime expireTime = DateTime.Now.AddMinutes(30);
|
||||
string filePath = _passwordResetFileBase + user.InternalId + ".json";
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = filePath,
|
||||
UserName = user.Name
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (FileStream fileStream = File.OpenWrite(filePath))
|
||||
{
|
||||
_jsonSerializer.SerializeToStream(spr, fileStream);
|
||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e);
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.PinCode,
|
||||
PinExpirationDate = expireTime,
|
||||
PinFile = filePath
|
||||
};
|
||||
}
|
||||
|
||||
private class SerializablePasswordReset : PasswordPinCreationResult
|
||||
{
|
||||
public string Pin { get; set; }
|
||||
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public class DefaultPasswordResetProvider : IPasswordResetProvider
|
||||
{
|
||||
public string Name => "Default Password Reset Provider";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
private readonly string _passwordResetFileBase;
|
||||
private readonly string _passwordResetFileBaseDir;
|
||||
private readonly string _passwordResetFileBaseName = "passwordreset";
|
||||
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ICryptoProvider _crypto;
|
||||
|
||||
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
||||
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_userManager = userManager;
|
||||
_crypto = cryptoProvider;
|
||||
}
|
||||
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
SerializablePasswordReset spr;
|
||||
HashSet<string> usersreset = new HashSet<string>();
|
||||
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
|
||||
{
|
||||
using (var str = File.OpenRead(resetfile))
|
||||
{
|
||||
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (spr.ExpirationDate < DateTime.Now)
|
||||
{
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var resetUser = _userManager.GetUserByName(spr.UserName);
|
||||
if (resetUser == null)
|
||||
{
|
||||
throw new Exception($"User with a username of {spr.UserName} not found");
|
||||
}
|
||||
|
||||
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||
usersreset.Add(resetUser.Name);
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
}
|
||||
|
||||
if (usersreset.Count < 1)
|
||||
{
|
||||
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PinRedeemResult
|
||||
{
|
||||
Success = true,
|
||||
UsersReset = usersreset.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
|
||||
{
|
||||
string pin = string.Empty;
|
||||
using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
cryptoRandom.GetBytes(bytes);
|
||||
pin = BitConverter.ToString(bytes);
|
||||
}
|
||||
|
||||
DateTime expireTime = DateTime.Now.AddMinutes(30);
|
||||
string filePath = _passwordResetFileBase + user.InternalId + ".json";
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = filePath,
|
||||
UserName = user.Name
|
||||
};
|
||||
|
||||
using (FileStream fileStream = File.OpenWrite(filePath))
|
||||
{
|
||||
_jsonSerializer.SerializeToStream(spr, fileStream);
|
||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.PinCode,
|
||||
PinExpirationDate = expireTime,
|
||||
PinFile = filePath
|
||||
};
|
||||
}
|
||||
|
||||
private class SerializablePasswordReset : PasswordPinCreationResult
|
||||
{
|
||||
public string Pin { get; set; }
|
||||
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -16,12 +13,12 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
||||
{
|
||||
throw new SecurityException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
|
||||
throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
|
||||
}
|
||||
|
||||
public Task<bool> HasPassword(User user)
|
||||
public bool HasPassword(User user)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
|
@ -31,7 +28,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
|
||||
{
|
||||
// Nothing here
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
public string GetPasswordHash(User user)
|
||||
|
|
|
@ -266,6 +266,7 @@ namespace Emby.Server.Implementations.Library
|
|||
builder.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
|
@ -286,17 +287,17 @@ namespace Emby.Server.Implementations.Library
|
|||
if (user != null)
|
||||
{
|
||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
|
||||
authenticationProvider = authResult.Item1;
|
||||
updatedUsername = authResult.Item2;
|
||||
success = authResult.Item3;
|
||||
authenticationProvider = authResult.authenticationProvider;
|
||||
updatedUsername = authResult.username;
|
||||
success = authResult.success;
|
||||
}
|
||||
else
|
||||
{
|
||||
// user is null
|
||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
|
||||
authenticationProvider = authResult.Item1;
|
||||
updatedUsername = authResult.Item2;
|
||||
success = authResult.Item3;
|
||||
authenticationProvider = authResult.authenticationProvider;
|
||||
updatedUsername = authResult.username;
|
||||
success = authResult.success;
|
||||
|
||||
if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
|
||||
{
|
||||
|
@ -331,22 +332,25 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (user == null)
|
||||
{
|
||||
throw new SecurityException("Invalid username or password entered.");
|
||||
throw new AuthenticationException("Invalid username or password entered.");
|
||||
}
|
||||
|
||||
if (user.Policy.IsDisabled)
|
||||
{
|
||||
throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name));
|
||||
throw new AuthenticationException(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"The {0} account is currently disabled. Please consult with your administrator.",
|
||||
user.Name));
|
||||
}
|
||||
|
||||
if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
|
||||
{
|
||||
throw new SecurityException("Forbidden.");
|
||||
throw new AuthenticationException("Forbidden.");
|
||||
}
|
||||
|
||||
if (!user.IsParentalScheduleAllowed())
|
||||
{
|
||||
throw new SecurityException("User is not allowed access at this time.");
|
||||
throw new AuthenticationException("User is not allowed access at this time.");
|
||||
}
|
||||
|
||||
// Update LastActivityDate and LastLoginDate, then save
|
||||
|
@ -357,6 +361,7 @@ namespace Emby.Server.Implementations.Library
|
|||
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
|
||||
UpdateUser(user);
|
||||
}
|
||||
|
||||
UpdateInvalidLoginAttemptCount(user, 0);
|
||||
}
|
||||
else
|
||||
|
@ -429,7 +434,7 @@ namespace Emby.Server.Implementations.Library
|
|||
return providers;
|
||||
}
|
||||
|
||||
private async Task<Tuple<string, bool>> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
||||
private async Task<(string username, bool success)> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -444,23 +449,23 @@ namespace Emby.Server.Implementations.Library
|
|||
authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if(authenticationResult.Username != username)
|
||||
if (authenticationResult.Username != username)
|
||||
{
|
||||
_logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
|
||||
username = authenticationResult.Username;
|
||||
}
|
||||
|
||||
return new Tuple<string, bool>(username, true);
|
||||
return (username, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (AuthenticationException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name);
|
||||
_logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
|
||||
|
||||
return new Tuple<string, bool>(username, false);
|
||||
return (username, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Tuple<IAuthenticationProvider, string, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
|
||||
private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
|
||||
{
|
||||
string updatedUsername = null;
|
||||
bool success = false;
|
||||
|
@ -475,15 +480,15 @@ namespace Emby.Server.Implementations.Library
|
|||
if (password == null)
|
||||
{
|
||||
// legacy
|
||||
success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(user.Password, hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var provider in GetAuthenticationProviders(user))
|
||||
{
|
||||
var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
|
||||
updatedUsername = providerAuthResult.Item1;
|
||||
success = providerAuthResult.Item2;
|
||||
updatedUsername = providerAuthResult.username;
|
||||
success = providerAuthResult.success;
|
||||
|
||||
if (success)
|
||||
{
|
||||
|
@ -510,7 +515,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
return new Tuple<IAuthenticationProvider, string, bool>(authenticationProvider, username, success);
|
||||
return (authenticationProvider, username, success);
|
||||
}
|
||||
|
||||
private void UpdateInvalidLoginAttemptCount(User user, int newValue)
|
||||
|
@ -593,7 +598,7 @@ namespace Emby.Server.Implementations.Library
|
|||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user);
|
||||
bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
|
||||
|
||||
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||
|
|
|
@ -599,7 +599,6 @@ namespace MediaBrowser.Api.LiveTv
|
|||
{
|
||||
public bool ValidateLogin { get; set; }
|
||||
public bool ValidateListings { get; set; }
|
||||
public string Pw { get; set; }
|
||||
}
|
||||
|
||||
[Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
|
||||
|
@ -867,28 +866,10 @@ namespace MediaBrowser.Api.LiveTv
|
|||
|
||||
public async Task<object> Post(AddListingProvider request)
|
||||
{
|
||||
if (request.Pw != null)
|
||||
{
|
||||
request.Password = GetHashedString(request.Pw);
|
||||
}
|
||||
|
||||
request.Pw = null;
|
||||
|
||||
var result = await _liveTvManager.SaveListingProvider(request, request.ValidateLogin, request.ValidateListings).ConfigureAwait(false);
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hashed string.
|
||||
/// </summary>
|
||||
private string GetHashedString(string str)
|
||||
{
|
||||
// legacy
|
||||
return BitConverter.ToString(
|
||||
_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str)))
|
||||
.Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void Delete(DeleteListingProvider request)
|
||||
{
|
||||
_liveTvManager.DeleteListingsProvider(request.Id);
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
namespace MediaBrowser.Controller.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when an attempt to authenticate fails.
|
||||
/// </summary>
|
||||
public class AuthenticationException : Exception
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public AuthenticationException() : base()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AuthenticationException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AuthenticationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,10 +9,9 @@ namespace MediaBrowser.Controller.Authentication
|
|||
string Name { get; }
|
||||
bool IsEnabled { get; }
|
||||
Task<ProviderAuthenticationResult> Authenticate(string username, string password);
|
||||
Task<bool> HasPassword(User user);
|
||||
bool HasPassword(User user);
|
||||
Task ChangePassword(User user, string newPassword);
|
||||
void ChangeEasyPassword(User user, string newPassword, string newPasswordHash);
|
||||
string GetPasswordHash(User user);
|
||||
string GetEasyPasswordHash(User user);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace MediaBrowser.Controller.Authentication
|
|||
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
|
||||
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
|
||||
}
|
||||
|
||||
public class PasswordPinCreationResult
|
||||
{
|
||||
public string PinFile { get; set; }
|
||||
|
|
|
@ -6,9 +6,14 @@ namespace MediaBrowser.Model.Cryptography
|
|||
{
|
||||
public interface ICryptoProvider
|
||||
{
|
||||
string DefaultHashMethod { get; }
|
||||
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
|
||||
Guid GetMD5(string str);
|
||||
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
|
||||
byte[] ComputeMD5(Stream str);
|
||||
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
|
||||
byte[] ComputeMD5(byte[] bytes);
|
||||
[Obsolete("Use System.Security.Cryptography.SHA1 directly")]
|
||||
byte[] ComputeSHA1(byte[] bytes);
|
||||
IEnumerable<string> GetSupportedHashMethods();
|
||||
byte[] ComputeHash(string HashMethod, byte[] bytes);
|
||||
|
@ -17,6 +22,5 @@ namespace MediaBrowser.Model.Cryptography
|
|||
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
|
||||
byte[] ComputeHash(PasswordHash hash);
|
||||
byte[] GenerateSalt();
|
||||
string DefaultHashMethod { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
|
@ -16,86 +17,71 @@ namespace MediaBrowser.Model.Cryptography
|
|||
|
||||
private Dictionary<string, string> _parameters = new Dictionary<string, string>();
|
||||
|
||||
private string _salt;
|
||||
private byte[] _salt;
|
||||
|
||||
private byte[] _saltBytes;
|
||||
private byte[] _hash;
|
||||
|
||||
private string _hash;
|
||||
public PasswordHash(string storageString)
|
||||
{
|
||||
string[] splitted = storageString.Split('$');
|
||||
// The string should at least contain the hash function and the hash itself
|
||||
if (splitted.Length < 3)
|
||||
{
|
||||
throw new ArgumentException("String doesn't contain enough segments", nameof(storageString));
|
||||
}
|
||||
|
||||
private byte[] _hashBytes;
|
||||
// Start at 1, the first index shouldn't contain any data
|
||||
int index = 1;
|
||||
|
||||
// Name of the hash function
|
||||
_id = splitted[index++];
|
||||
|
||||
// Optional parameters
|
||||
if (splitted[index].IndexOf('=') != -1)
|
||||
{
|
||||
foreach (string paramset in splitted[index++].Split(','))
|
||||
{
|
||||
if (string.IsNullOrEmpty(paramset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] fields = paramset.Split('=');
|
||||
if (fields.Length != 2)
|
||||
{
|
||||
throw new InvalidDataException($"Malformed parameter in password hash string {paramset}");
|
||||
}
|
||||
|
||||
_parameters.Add(fields[0], fields[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the string also contains a salt
|
||||
if (splitted.Length - index == 2)
|
||||
{
|
||||
_salt = ConvertFromByteString(splitted[index++]);
|
||||
_hash = ConvertFromByteString(splitted[index++]);
|
||||
}
|
||||
else
|
||||
{
|
||||
_salt = Array.Empty<byte>();
|
||||
_hash = ConvertFromByteString(splitted[index++]);
|
||||
}
|
||||
}
|
||||
|
||||
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[] 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 byte[] Hash { get => _hash; set => _hash = value; }
|
||||
|
||||
public PasswordHash(ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_id = cryptoProvider.DefaultHashMethod;
|
||||
_saltBytes = cryptoProvider.GenerateSalt();
|
||||
_salt = ConvertToByteString(SaltBytes);
|
||||
_salt = cryptoProvider.GenerateSalt();
|
||||
_hash = Array.Empty<Byte>();
|
||||
}
|
||||
|
||||
public static byte[] ConvertFromByteString(string byteString)
|
||||
|
@ -111,43 +97,45 @@ namespace MediaBrowser.Model.Cryptography
|
|||
}
|
||||
|
||||
public static string ConvertToByteString(byte[] bytes)
|
||||
{
|
||||
return BitConverter.ToString(bytes).Replace("-", "");
|
||||
}
|
||||
=> BitConverter.ToString(bytes).Replace("-", string.Empty);
|
||||
|
||||
private string SerializeParameters()
|
||||
private void SerializeParameters(StringBuilder stringBuilder)
|
||||
{
|
||||
string returnString = string.Empty;
|
||||
foreach (var KVP in _parameters)
|
||||
if (_parameters.Count == 0)
|
||||
{
|
||||
returnString += $",{KVP.Key}={KVP.Value}";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(returnString)) && returnString[0] == ',')
|
||||
stringBuilder.Append('$');
|
||||
foreach (var pair in _parameters)
|
||||
{
|
||||
returnString = returnString.Remove(0, 1);
|
||||
stringBuilder.Append(pair.Key);
|
||||
stringBuilder.Append('=');
|
||||
stringBuilder.Append(pair.Value);
|
||||
stringBuilder.Append(',');
|
||||
}
|
||||
|
||||
return returnString;
|
||||
// Remove last ','
|
||||
stringBuilder.Length -= 1;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string outString = "$" + _id;
|
||||
string paramstring = SerializeParameters();
|
||||
if (!string.IsNullOrEmpty(paramstring))
|
||||
var str = new StringBuilder();
|
||||
str.Append('$');
|
||||
str.Append(_id);
|
||||
SerializeParameters(str);
|
||||
|
||||
if (_salt.Length == 0)
|
||||
{
|
||||
outString += $"${paramstring}";
|
||||
str.Append('$');
|
||||
str.Append(ConvertToByteString(_salt));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_salt))
|
||||
{
|
||||
outString += $"${_salt}";
|
||||
}
|
||||
str.Append('$');
|
||||
str.Append(ConvertToByteString(_hash));
|
||||
|
||||
outString += $"${_hash}";
|
||||
return outString;
|
||||
return str.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue