Pārlūkot izejas kodu

Streamline authentication proccess

Bond_009 6 gadi atpakaļ
vecāks
revīzija
0f897589ed

+ 49 - 37
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

@@ -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 string DefaultHashMethod => "PBKDF2";
+
+        [Obsolete("Use System.Security.Cryptography.MD5 directly")]
         public Guid GetMD5(string str)
-        {
-            return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(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;
+        }
     }
 }

+ 55 - 60
Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs

@@ -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));
+            }
+        }
     }
 }

+ 125 - 132
Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs

@@ -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; }
+        }
+    }
+}

+ 4 - 7
Emby.Server.Implementations/Library/InvalidAuthProvider.cs

@@ -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)

+ 27 - 22
Emby.Server.Implementations/Library/UserManager.cs

@@ -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) ?

+ 0 - 19
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -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);

+ 28 - 0
MediaBrowser.Controller/Authentication/AuthenticationException.cs

@@ -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)
+        {
+
+        }
+    }
+}

+ 1 - 2
MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs

@@ -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);
     }
 

+ 1 - 0
MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs

@@ -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; }

+ 5 - 1
MediaBrowser.Model/Cryptography/ICryptoProvider.cs

@@ -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; }
     }
 }

+ 70 - 82
MediaBrowser.Model/Cryptography/PasswordHash.cs

@@ -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 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; }
+        private byte[] _hash;
 
         public PasswordHash(string storageString)
         {
             string[] splitted = storageString.Split('$');
-            _id = splitted[1];
-            if (splitted[2].Contains("="))
+            // 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));
+            }
+
+            // 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('=');
-                        if (fields.Length == 2)
-                        {
-                            _parameters.Add(fields[0], fields[1]);
-                        }
-                        else
-                        {
-                            throw new Exception($"Malformed parameter in password hash string {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]);
                 }
-                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);
-                }
+            }
+
+            // Check if the string also contains a salt
+            if (splitted.Length - index == 2)
+            {
+                _salt = ConvertFromByteString(splitted[index++]);
+                _hash = ConvertFromByteString(splitted[index++]);
             }
             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);
-                }
-
+                _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 byte[] Salt { get => _salt; set => _salt = value; }
+
+        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))
-            {
-                outString += $"${paramstring}";
-            }
+            var str = new StringBuilder();
+            str.Append('$');
+            str.Append(_id);
+            SerializeParameters(str);
 
-            if (!string.IsNullOrEmpty(_salt))
+            if (_salt.Length == 0)
             {
-                outString += $"${_salt}";
+                str.Append('$');
+                str.Append(ConvertToByteString(_salt));
             }
 
-            outString += $"${_hash}";
-            return outString;
+            str.Append('$');
+            str.Append(ConvertToByteString(_hash));
+
+            return str.ToString();
         }
     }
-
 }