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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Linq;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Cryptography
|
namespace Emby.Server.Implementations.Cryptography
|
||||||
{
|
{
|
||||||
public class CryptographyProvider : ICryptoProvider
|
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)
|
public Guid GetMD5(string str)
|
||||||
{
|
{
|
||||||
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
|
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
|
||||||
|
@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
|
||||||
return provider.ComputeHash(bytes);
|
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);
|
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>
|
/// <summary>
|
||||||
/// Save a user in the repo
|
/// Save a user in the repo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
|
@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
|
||||||
public string Name => "Default";
|
public string Name => "Default";
|
||||||
|
|
||||||
public bool IsEnabled => true;
|
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)
|
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
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)
|
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
||||||
{
|
{
|
||||||
|
bool success = false;
|
||||||
if (resolvedUser == null)
|
if (resolvedUser == null)
|
||||||
{
|
{
|
||||||
throw new Exception("Invalid username or password");
|
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)
|
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)
|
public Task<bool> HasPassword(User user)
|
||||||
{
|
{
|
||||||
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
|
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
|
||||||
return Task.FromResult(hasConfiguredPassword);
|
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)
|
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 (newPassword != null)
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPasswordHash(User user)
|
public string GetPasswordHash(User user)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(user.Password)
|
return user.Password;
|
||||||
? GetEmptyHashedString(user)
|
|
||||||
: 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>
|
/// <summary>
|
||||||
|
@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetHashedString(User user, string str)
|
public string GetHashedString(User user, string str)
|
||||||
{
|
{
|
||||||
var salt = user.Salt;
|
PasswordHash passwordHash;
|
||||||
if (salt != null)
|
if (string.IsNullOrEmpty(user.Password))
|
||||||
{
|
{
|
||||||
// return BCrypt.HashPassword(str, salt);
|
passwordHash = new PasswordHash(_cryptographyProvider);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConvertPasswordFormat(user);
|
||||||
|
passwordHash = new PasswordHash(user.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// legacy
|
if (passwordHash.SaltBytes != null)
|
||||||
return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
|
{
|
||||||
|
// 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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Events;
|
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 (.)
|
//This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||||
foreach (var currentChar in username)
|
//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 (.)
|
||||||
if (!IsValidUsernameCharacter(currentChar))
|
return Regex.IsMatch(username, "^[\\w-'._@]*$");
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidUsernameCharacter(char i)
|
private static bool IsValidUsernameCharacter(char i)
|
||||||
{
|
{
|
||||||
return !char.Equals(i, '<') && !char.Equals(i, '>');
|
return IsValidUsername(i.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MakeValidUsername(string username)
|
public string MakeValidUsername(string username)
|
||||||
|
@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
|
||||||
private string GetLocalPasswordHash(User user)
|
private string GetLocalPasswordHash(User user)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(user.EasyPassword)
|
return string.IsNullOrEmpty(user.EasyPassword)
|
||||||
? _defaultAuthenticationProvider.GetEmptyHashedString(user)
|
? null
|
||||||
: user.EasyPassword;
|
: user.EasyPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPasswordEmpty(User user, string passwordHash)
|
|
||||||
{
|
|
||||||
return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads the users from the repository
|
/// Loads the users from the repository
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
|
||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||||
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
|
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 :
|
hasConfiguredEasyPassword :
|
||||||
hasConfiguredPassword;
|
hasConfiguredPassword;
|
||||||
|
|
||||||
var dto = new UserDto
|
UserDto dto = new UserDto
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Name = user.Name,
|
Name = user.Name,
|
||||||
|
@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
dto.EnableAutoLogin = true;
|
dto.EnableAutoLogin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var image = user.GetImageInfo(ImageType.Primary, 0);
|
ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
|
||||||
|
|
||||||
if (image != null)
|
if (image != null)
|
||||||
{
|
{
|
||||||
|
@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
if (!IsValidUsername(name))
|
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)))
|
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Cryptography
|
namespace MediaBrowser.Model.Cryptography
|
||||||
{
|
{
|
||||||
|
@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography
|
||||||
byte[] ComputeMD5(Stream str);
|
byte[] ComputeMD5(Stream str);
|
||||||
byte[] ComputeMD5(byte[] bytes);
|
byte[] ComputeMD5(byte[] bytes);
|
||||||
byte[] ComputeSHA1(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