Merge pull request #1397 from Bond-009/passfast

Streamline authentication proccess
pull/1568/head^2
Joshua M. Boniface 5 years ago committed by GitHub
commit a96fa7a5c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,7 +8,7 @@ using MediaBrowser.Model.Cryptography;
namespace Emby.Server.Implementations.Cryptography namespace Emby.Server.Implementations.Cryptography
{ {
public class CryptographyProvider : ICryptoProvider public class CryptographyProvider : ICryptoProvider, IDisposable
{ {
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>() private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{ {
@ -28,26 +28,28 @@ namespace Emby.Server.Implementations.Cryptography
"System.Security.Cryptography.SHA512" "System.Security.Cryptography.SHA512"
}; };
public string DefaultHashMethod => "PBKDF2";
private RandomNumberGenerator _randomNumberGenerator; private RandomNumberGenerator _randomNumberGenerator;
private const int _defaultIterations = 1000; private const int _defaultIterations = 1000;
private bool _disposed = false;
public CryptographyProvider() public CryptographyProvider()
{ {
//FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto // 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 // 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 // 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 // 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(); _randomNumberGenerator = RandomNumberGenerator.Create();
} }
public string DefaultHashMethod => "PBKDF2";
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
public Guid GetMD5(string str) public Guid GetMD5(string str)
{ => new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
}
[Obsolete("Use System.Security.Cryptography.SHA1 directly")]
public byte[] ComputeSHA1(byte[] bytes) public byte[] ComputeSHA1(byte[] bytes)
{ {
using (var provider = SHA1.Create()) 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) public byte[] ComputeMD5(Stream str)
{ {
using (var provider = MD5.Create()) 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) public byte[] ComputeMD5(byte[] bytes)
{ {
using (var provider = MD5.Create()) using (var provider = MD5.Create())
@ -73,9 +77,7 @@ namespace Emby.Server.Implementations.Cryptography
} }
public IEnumerable<string> GetSupportedHashMethods() public IEnumerable<string> GetSupportedHashMethods()
{ => _supportedHashMethods;
return _supportedHashMethods;
}
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) 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) public byte[] ComputeHash(string hashMethod, byte[] bytes)
{ => ComputeHash(hashMethod, bytes, Array.Empty<byte>());
return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
}
public byte[] ComputeHashWithDefaultMethod(byte[] bytes) public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
{ => ComputeHash(DefaultHashMethod, bytes);
return ComputeHash(DefaultHashMethod, bytes);
}
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) 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) public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
{ => PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
}
public byte[] ComputeHash(PasswordHash hash) public byte[] ComputeHash(PasswordHash hash)
{ {
int iterations = _defaultIterations; int iterations = _defaultIterations;
if (!hash.Parameters.ContainsKey("iterations")) 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 throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}");
{
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); return PBKDF2(hash.Id, hash.Hash, hash.Salt, iterations);
} }
public byte[] GenerateSalt() public byte[] GenerateSalt()
@ -164,5 +152,29 @@ namespace Emby.Server.Implementations.Cryptography
_randomNumberGenerator.GetBytes(salt); _randomNumberGenerator.GetBytes(salt);
return 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 public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser
{ {
private readonly ICryptoProvider _cryptographyProvider; private readonly ICryptoProvider _cryptographyProvider;
public DefaultAuthenticationProvider(ICryptoProvider crypto) public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider)
{ {
_cryptographyProvider = crypto; _cryptographyProvider = cryptographyProvider;
} }
public string Name => "Default"; public string Name => "Default";
@ -28,17 +28,17 @@ namespace Emby.Server.Implementations.Library
throw new NotImplementedException(); 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) public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{ {
bool success = false; bool success = false;
if (resolvedUser == null) 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 // 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 return Task.FromResult(new ProviderAuthenticationResult
{ {
@ -50,37 +50,24 @@ namespace Emby.Server.Implementations.Library
byte[] passwordbytes = Encoding.UTF8.GetBytes(password); byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
PasswordHash readyHash = new PasswordHash(resolvedUser.Password); PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
byte[] calculatedHash; if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
string calculatedHashString; || _cryptographyProvider.DefaultHashMethod == readyHash.Id)
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id)
{ {
if (string.IsNullOrEmpty(readyHash.Salt)) byte[] calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, 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) if (calculatedHash.SequenceEqual(readyHash.Hash))
{ {
success = true; success = true;
// throw new Exception("Invalid username or password");
} }
} }
else 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) if (!success)
{ {
throw new Exception("Invalid username or password"); throw new AuthenticationException("Invalid username or password");
} }
return Task.FromResult(new ProviderAuthenticationResult return Task.FromResult(new ProviderAuthenticationResult
@ -98,29 +85,22 @@ namespace Emby.Server.Implementations.Library
return; return;
} }
if (!user.Password.Contains("$")) if (user.Password.IndexOf('$') == -1)
{ {
string hash = user.Password; string hash = user.Password;
user.Password = string.Format("$SHA1${0}", hash); 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; string hash = user.EasyPassword;
user.EasyPassword = string.Format("$SHA1${0}", hash); user.EasyPassword = string.Format("$SHA1${0}", hash);
} }
} }
public Task<bool> HasPassword(User user) public bool HasPassword(User user)
{ => !string.IsNullOrEmpty(user.Password);
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 Task ChangePassword(User user, string newPassword) public Task ChangePassword(User user, string newPassword)
{ {
@ -129,30 +109,24 @@ namespace Emby.Server.Implementations.Library
if (string.IsNullOrEmpty(user.Password)) if (string.IsNullOrEmpty(user.Password))
{ {
PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); newPasswordHash.Salt = _cryptographyProvider.GenerateSalt();
newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash); newPasswordHash.Hash = GetHashedChangeAuth(newPassword, newPasswordHash);
user.Password = newPasswordHash.ToString(); user.Password = newPasswordHash.ToString();
return Task.CompletedTask; return Task.CompletedTask;
} }
PasswordHash passwordHash = new PasswordHash(user.Password); 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 = _cryptographyProvider.GenerateSalt();
passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
passwordHash.Id = _cryptographyProvider.DefaultHashMethod; passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); passwordHash.Hash = GetHashedChangeAuth(newPassword, passwordHash);
} }
else if (newPassword != null) else if (newPassword != null)
{ {
passwordHash.Hash = GetHashedString(user, newPassword); passwordHash.Hash = GetHashed(user, newPassword);
}
if (string.IsNullOrWhiteSpace(passwordHash.Hash))
{
throw new ArgumentNullException(nameof(passwordHash.Hash));
} }
user.Password = passwordHash.ToString(); user.Password = passwordHash.ToString();
@ -160,11 +134,6 @@ namespace Emby.Server.Implementations.Library
return Task.CompletedTask; return Task.CompletedTask;
} }
public string GetPasswordHash(User user)
{
return user.Password;
}
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
{ {
ConvertPasswordFormat(user); ConvertPasswordFormat(user);
@ -190,13 +159,13 @@ namespace Emby.Server.Implementations.Library
return string.IsNullOrEmpty(user.EasyPassword) return string.IsNullOrEmpty(user.EasyPassword)
? null ? 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); passwordHash.Hash = Encoding.UTF8.GetBytes(newPassword);
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); return _cryptographyProvider.ComputeHash(passwordHash);
} }
/// <summary> /// <summary>
@ -215,10 +184,10 @@ namespace Emby.Server.Implementations.Library
passwordHash = new PasswordHash(user.Password); 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 // 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)); return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
} }
else else
@ -227,5 +196,31 @@ namespace Emby.Server.Implementations.Library
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users; using MediaBrowser.Model.Users;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
{ {
public class DefaultPasswordResetProvider : IPasswordResetProvider public class DefaultPasswordResetProvider : IPasswordResetProvider
{ {
public string Name => "Default Password Reset Provider"; public string Name => "Default Password Reset Provider";
public bool IsEnabled => true; public bool IsEnabled => true;
private readonly string _passwordResetFileBase; private readonly string _passwordResetFileBase;
private readonly string _passwordResetFileBaseDir; private readonly string _passwordResetFileBaseDir;
private readonly string _passwordResetFileBaseName = "passwordreset"; private readonly string _passwordResetFileBaseName = "passwordreset";
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ICryptoProvider _crypto; private readonly ICryptoProvider _crypto;
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
{ {
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_userManager = userManager; _userManager = userManager;
_crypto = cryptoProvider; _crypto = cryptoProvider;
} }
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{ {
SerializablePasswordReset spr; SerializablePasswordReset spr;
HashSet<string> usersreset = new HashSet<string>(); HashSet<string> usersreset = new HashSet<string>();
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
{ {
using (var str = File.OpenRead(resetfile)) using (var str = File.OpenRead(resetfile))
{ {
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false); spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
} }
if (spr.ExpirationDate < DateTime.Now) if (spr.ExpirationDate < DateTime.Now)
{ {
File.Delete(resetfile); File.Delete(resetfile);
} }
else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase)) else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
{ {
var resetUser = _userManager.GetUserByName(spr.UserName); var resetUser = _userManager.GetUserByName(spr.UserName);
if (resetUser == null) if (resetUser == null)
{ {
throw new Exception($"User with a username of {spr.UserName} not found"); throw new Exception($"User with a username of {spr.UserName} not found");
} }
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
usersreset.Add(resetUser.Name); usersreset.Add(resetUser.Name);
File.Delete(resetfile); File.Delete(resetfile);
} }
} }
if (usersreset.Count < 1) if (usersreset.Count < 1)
{ {
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
} }
else else
{ {
return new PinRedeemResult return new PinRedeemResult
{ {
Success = true, Success = true,
UsersReset = usersreset.ToArray() UsersReset = usersreset.ToArray()
}; };
} }
} }
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
{ {
string pin = string.Empty; string pin = string.Empty;
using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
{ {
byte[] bytes = new byte[4]; byte[] bytes = new byte[4];
cryptoRandom.GetBytes(bytes); cryptoRandom.GetBytes(bytes);
pin = BitConverter.ToString(bytes); pin = BitConverter.ToString(bytes);
} }
DateTime expireTime = DateTime.Now.AddMinutes(30); DateTime expireTime = DateTime.Now.AddMinutes(30);
string filePath = _passwordResetFileBase + user.InternalId + ".json"; string filePath = _passwordResetFileBase + user.InternalId + ".json";
SerializablePasswordReset spr = new SerializablePasswordReset SerializablePasswordReset spr = new SerializablePasswordReset
{ {
ExpirationDate = expireTime, ExpirationDate = expireTime,
Pin = pin, Pin = pin,
PinFile = filePath, PinFile = filePath,
UserName = user.Name UserName = user.Name
}; };
try using (FileStream fileStream = File.OpenWrite(filePath))
{ {
using (FileStream fileStream = File.OpenWrite(filePath)) _jsonSerializer.SerializeToStream(spr, fileStream);
{ await fileStream.FlushAsync().ConfigureAwait(false);
_jsonSerializer.SerializeToStream(spr, fileStream); }
await fileStream.FlushAsync().ConfigureAwait(false);
} return new ForgotPasswordResult
} {
catch (Exception e) Action = ForgotPasswordAction.PinCode,
{ PinExpirationDate = expireTime,
throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e); PinFile = filePath
} };
}
return new ForgotPasswordResult
{ private class SerializablePasswordReset : PasswordPinCreationResult
Action = ForgotPasswordAction.PinCode, {
PinExpirationDate = expireTime, public string Pin { get; set; }
PinFile = filePath
}; public string UserName { get; set; }
} }
}
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 System.Threading.Tasks;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -16,12 +13,12 @@ namespace Emby.Server.Implementations.Library
public Task<ProviderAuthenticationResult> Authenticate(string username, string password) 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) 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) public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
{ {
// Nothing here // Nothing here
} }
public string GetPasswordHash(User user) public string GetPasswordHash(User user)

@ -266,6 +266,7 @@ namespace Emby.Server.Implementations.Library
builder.Append(c); builder.Append(c);
} }
} }
return builder.ToString(); return builder.ToString();
} }
@ -286,17 +287,17 @@ namespace Emby.Server.Implementations.Library
if (user != null) if (user != null)
{ {
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.Item1; authenticationProvider = authResult.authenticationProvider;
updatedUsername = authResult.Item2; updatedUsername = authResult.username;
success = authResult.Item3; success = authResult.success;
} }
else else
{ {
// user is null // user is null
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.Item1; authenticationProvider = authResult.authenticationProvider;
updatedUsername = authResult.Item2; updatedUsername = authResult.username;
success = authResult.Item3; success = authResult.success;
if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
{ {
@ -331,22 +332,25 @@ namespace Emby.Server.Implementations.Library
if (user == null) if (user == null)
{ {
throw new SecurityException("Invalid username or password entered."); throw new AuthenticationException("Invalid username or password entered.");
} }
if (user.Policy.IsDisabled) 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)) if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
{ {
throw new SecurityException("Forbidden."); throw new AuthenticationException("Forbidden.");
} }
if (!user.IsParentalScheduleAllowed()) 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 // Update LastActivityDate and LastLoginDate, then save
@ -357,6 +361,7 @@ namespace Emby.Server.Implementations.Library
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
UpdateUser(user); UpdateUser(user);
} }
UpdateInvalidLoginAttemptCount(user, 0); UpdateInvalidLoginAttemptCount(user, 0);
} }
else else
@ -429,7 +434,7 @@ namespace Emby.Server.Implementations.Library
return providers; 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 try
{ {
@ -444,23 +449,23 @@ namespace Emby.Server.Implementations.Library
authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); 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); _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
username = 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; string updatedUsername = null;
bool success = false; bool success = false;
@ -475,15 +480,15 @@ namespace Emby.Server.Implementations.Library
if (password == null) if (password == null)
{ {
// legacy // 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 else
{ {
foreach (var provider in GetAuthenticationProviders(user)) foreach (var provider in GetAuthenticationProviders(user))
{ {
var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
updatedUsername = providerAuthResult.Item1; updatedUsername = providerAuthResult.username;
success = providerAuthResult.Item2; success = providerAuthResult.success;
if (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) private void UpdateInvalidLoginAttemptCount(User user, int newValue)
@ -593,7 +598,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user)); 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 hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? 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 ValidateLogin { get; set; }
public bool ValidateListings { get; set; } public bool ValidateListings { get; set; }
public string Pw { get; set; }
} }
[Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")] [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
@ -867,28 +866,10 @@ namespace MediaBrowser.Api.LiveTv
public async Task<object> Post(AddListingProvider request) 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); var result = await _liveTvManager.SaveListingProvider(request, request.ValidateLogin, request.ValidateListings).ConfigureAwait(false);
return ToOptimizedResult(result); 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) public void Delete(DeleteListingProvider request)
{ {
_liveTvManager.DeleteListingsProvider(request.Id); _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; } string Name { get; }
bool IsEnabled { get; } bool IsEnabled { get; }
Task<ProviderAuthenticationResult> Authenticate(string username, string password); Task<ProviderAuthenticationResult> Authenticate(string username, string password);
Task<bool> HasPassword(User user); bool HasPassword(User user);
Task ChangePassword(User user, string newPassword); Task ChangePassword(User user, string newPassword);
void ChangeEasyPassword(User user, string newPassword, string newPasswordHash); void ChangeEasyPassword(User user, string newPassword, string newPasswordHash);
string GetPasswordHash(User user);
string GetEasyPasswordHash(User user); string GetEasyPasswordHash(User user);
} }

@ -12,6 +12,7 @@ namespace MediaBrowser.Controller.Authentication
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork); Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
Task<PinRedeemResult> RedeemPasswordResetPin(string pin); Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
} }
public class PasswordPinCreationResult public class PasswordPinCreationResult
{ {
public string PinFile { get; set; } public string PinFile { get; set; }

@ -6,9 +6,14 @@ namespace MediaBrowser.Model.Cryptography
{ {
public interface ICryptoProvider public interface ICryptoProvider
{ {
string DefaultHashMethod { get; }
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
Guid GetMD5(string str); Guid GetMD5(string str);
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
byte[] ComputeMD5(Stream str); byte[] ComputeMD5(Stream str);
[Obsolete("Use System.Security.Cryptography.MD5 directly")]
byte[] ComputeMD5(byte[] bytes); byte[] ComputeMD5(byte[] bytes);
[Obsolete("Use System.Security.Cryptography.SHA1 directly")]
byte[] ComputeSHA1(byte[] bytes); byte[] ComputeSHA1(byte[] bytes);
IEnumerable<string> GetSupportedHashMethods(); IEnumerable<string> GetSupportedHashMethods();
byte[] ComputeHash(string HashMethod, byte[] bytes); byte[] ComputeHash(string HashMethod, byte[] bytes);
@ -17,6 +22,5 @@ namespace MediaBrowser.Model.Cryptography
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
byte[] ComputeHash(PasswordHash hash); byte[] ComputeHash(PasswordHash hash);
byte[] GenerateSalt(); byte[] GenerateSalt();
string DefaultHashMethod { get; }
} }
} }

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Text; using System.Text;
namespace MediaBrowser.Model.Cryptography namespace MediaBrowser.Model.Cryptography
@ -16,86 +17,71 @@ namespace MediaBrowser.Model.Cryptography
private Dictionary<string, string> _parameters = new Dictionary<string, string>(); private Dictionary<string, string> _parameters = new Dictionary<string, string>();
private string _salt; private byte[] _salt;
private byte[] _saltBytes; private byte[] _hash;
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) public PasswordHash(string storageString)
{ {
string[] splitted = storageString.Split('$'); string[] splitted = storageString.Split('$');
_id = splitted[1]; // The string should at least contain the hash function and the hash itself
if (splitted[2].Contains("=")) if (splitted.Length < 3)
{
throw new ArgumentException("String doesn't contain enough segments", nameof(storageString));
}
// 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[2].Split(','))) foreach (string paramset in splitted[index++].Split(','))
{ {
if (!string.IsNullOrEmpty(paramset)) if (string.IsNullOrEmpty(paramset))
{ {
string[] fields = paramset.Split('='); continue;
if (fields.Length == 2)
{
_parameters.Add(fields[0], fields[1]);
}
else
{
throw new Exception($"Malformed parameter in password hash string {paramset}");
}
} }
string[] fields = paramset.Split('=');
if (fields.Length != 2)
{
throw new InvalidDataException($"Malformed parameter in password hash string {paramset}");
}
_parameters.Add(fields[0], fields[1]);
} }
if (splitted.Length == 5) }
{
_salt = splitted[3]; // Check if the string also contains a salt
_saltBytes = ConvertFromByteString(_salt); if (splitted.Length - index == 2)
_hash = splitted[4]; {
_hashBytes = ConvertFromByteString(_hash); _salt = ConvertFromByteString(splitted[index++]);
} _hash = ConvertFromByteString(splitted[index++]);
else
{
_salt = string.Empty;
_hash = splitted[3];
_hashBytes = ConvertFromByteString(_hash);
}
} }
else else
{ {
if (splitted.Length == 4) _salt = Array.Empty<byte>();
{ _hash = ConvertFromByteString(splitted[index++]);
_salt = splitted[2];
_saltBytes = ConvertFromByteString(_salt);
_hash = splitted[3];
_hashBytes = ConvertFromByteString(_hash);
}
else
{
_salt = string.Empty;
_hash = splitted[2];
_hashBytes = ConvertFromByteString(_hash);
}
} }
} }
public string Id { get => _id; set => _id = value; }
public Dictionary<string, string> Parameters { get => _parameters; set => _parameters = value; }
public byte[] Salt { get => _salt; set => _salt = value; }
public byte[] Hash { get => _hash; set => _hash = value; }
public PasswordHash(ICryptoProvider cryptoProvider) public PasswordHash(ICryptoProvider cryptoProvider)
{ {
_id = cryptoProvider.DefaultHashMethod; _id = cryptoProvider.DefaultHashMethod;
_saltBytes = cryptoProvider.GenerateSalt(); _salt = cryptoProvider.GenerateSalt();
_salt = ConvertToByteString(SaltBytes); _hash = Array.Empty<Byte>();
} }
public static byte[] ConvertFromByteString(string byteString) public static byte[] ConvertFromByteString(string byteString)
@ -111,43 +97,45 @@ namespace MediaBrowser.Model.Cryptography
} }
public static string ConvertToByteString(byte[] bytes) public static string ConvertToByteString(byte[] bytes)
{ => BitConverter.ToString(bytes).Replace("-", string.Empty);
return BitConverter.ToString(bytes).Replace("-", "");
}
private string SerializeParameters() private void SerializeParameters(StringBuilder stringBuilder)
{ {
string returnString = string.Empty; if (_parameters.Count == 0)
foreach (var KVP in _parameters)
{ {
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() public override string ToString()
{ {
string outString = "$" + _id; var str = new StringBuilder();
string paramstring = SerializeParameters(); str.Append('$');
if (!string.IsNullOrEmpty(paramstring)) str.Append(_id);
{ SerializeParameters(str);
outString += $"${paramstring}";
}
if (!string.IsNullOrEmpty(_salt)) if (_salt.Length == 0)
{ {
outString += $"${_salt}"; str.Append('$');
str.Append(ConvertToByteString(_salt));
} }
outString += $"${_hash}"; str.Append('$');
return outString; str.Append(ConvertToByteString(_hash));
return str.ToString();
} }
} }
} }

Loading…
Cancel
Save