|
@@ -1,4 +1,5 @@
|
|
|
using System;
|
|
|
+using System.Linq;
|
|
|
using System.Text;
|
|
|
using System.Threading.Tasks;
|
|
|
using MediaBrowser.Controller.Authentication;
|
|
@@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
|
|
|
public string Name => "Default";
|
|
|
|
|
|
public bool IsEnabled => true;
|
|
|
-
|
|
|
+
|
|
|
+ // This is dumb and an artifact of the backwards way auth providers were designed.
|
|
|
+ // This version of authenticate was never meant to be called, but needs to be here for interface compat
|
|
|
+ // Only the providers that don't provide local user support use this
|
|
|
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
|
|
{
|
|
|
throw new NotImplementedException();
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ // This is the verson that we need to use for local users. Because reasons.
|
|
|
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
|
|
{
|
|
|
+ bool success = false;
|
|
|
if (resolvedUser == null)
|
|
|
{
|
|
|
throw new Exception("Invalid username or password");
|
|
|
}
|
|
|
|
|
|
- var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
|
|
+ // As long as jellyfin supports passwordless users, we need this little block here to accomodate
|
|
|
+ if (IsPasswordEmpty(resolvedUser, password))
|
|
|
+ {
|
|
|
+ return Task.FromResult(new ProviderAuthenticationResult
|
|
|
+ {
|
|
|
+ Username = username
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ ConvertPasswordFormat(resolvedUser);
|
|
|
+ byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
|
|
|
+
|
|
|
+ PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
|
|
|
+ byte[] calculatedHash;
|
|
|
+ string calculatedHashString;
|
|
|
+ if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(readyHash.Salt))
|
|
|
+ {
|
|
|
+ calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
|
|
|
+ calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
|
|
|
+ calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (calculatedHashString == readyHash.Hash)
|
|
|
+ {
|
|
|
+ success = true;
|
|
|
+ // throw new Exception("Invalid username or password");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
if (!success)
|
|
|
{
|
|
@@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
|
|
|
+ // but at least they are in the new format.
|
|
|
+ private void ConvertPasswordFormat(User user)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(user.Password))
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!user.Password.Contains("$"))
|
|
|
+ {
|
|
|
+ string hash = user.Password;
|
|
|
+ user.Password = string.Format("$SHA1${0}", hash);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
|
|
|
+ {
|
|
|
+ string hash = user.EasyPassword;
|
|
|
+ user.EasyPassword = string.Format("$SHA1${0}", hash);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public Task<bool> HasPassword(User user)
|
|
|
{
|
|
|
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
|
|
|
return Task.FromResult(hasConfiguredPassword);
|
|
|
}
|
|
|
|
|
|
- private bool IsPasswordEmpty(User user, string passwordHash)
|
|
|
+ private bool IsPasswordEmpty(User user, string password)
|
|
|
{
|
|
|
- return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
|
|
+ return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
|
|
|
}
|
|
|
|
|
|
public Task ChangePassword(User user, string newPassword)
|
|
|
{
|
|
|
- string newPasswordHash = null;
|
|
|
+ ConvertPasswordFormat(user);
|
|
|
+ // This is needed to support changing a no password user to a password user
|
|
|
+ if (string.IsNullOrEmpty(user.Password))
|
|
|
+ {
|
|
|
+ PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
|
|
|
+ newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
|
|
+ newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
|
|
|
+ newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
|
|
+ newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
|
|
|
+ user.Password = newPasswordHash.ToString();
|
|
|
+ return Task.CompletedTask;
|
|
|
+ }
|
|
|
|
|
|
- if (newPassword != null)
|
|
|
+ PasswordHash passwordHash = new PasswordHash(user.Password);
|
|
|
+ if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
|
|
|
{
|
|
|
- newPasswordHash = GetHashedString(user, newPassword);
|
|
|
+ passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
|
|
+ passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
|
|
|
+ passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
|
|
+ passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
|
|
|
+ }
|
|
|
+ else if (newPassword != null)
|
|
|
+ {
|
|
|
+ passwordHash.Hash = GetHashedString(user, newPassword);
|
|
|
}
|
|
|
|
|
|
- if (string.IsNullOrWhiteSpace(newPasswordHash))
|
|
|
+ if (string.IsNullOrWhiteSpace(passwordHash.Hash))
|
|
|
{
|
|
|
- throw new ArgumentNullException(nameof(newPasswordHash));
|
|
|
+ throw new ArgumentNullException(nameof(passwordHash.Hash));
|
|
|
}
|
|
|
|
|
|
- user.Password = newPasswordHash;
|
|
|
+ user.Password = passwordHash.ToString();
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
}
|
|
|
|
|
|
public string GetPasswordHash(User user)
|
|
|
{
|
|
|
- return string.IsNullOrEmpty(user.Password)
|
|
|
- ? GetEmptyHashedString(user)
|
|
|
- : user.Password;
|
|
|
+ return user.Password;
|
|
|
}
|
|
|
|
|
|
- public string GetEmptyHashedString(User user)
|
|
|
+ public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
|
|
|
{
|
|
|
- return GetHashedString(user, string.Empty);
|
|
|
+ passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
|
|
|
+ return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
|
|
|
/// </summary>
|
|
|
public string GetHashedString(User user, string str)
|
|
|
{
|
|
|
- var salt = user.Salt;
|
|
|
- if (salt != null)
|
|
|
+ PasswordHash passwordHash;
|
|
|
+ if (string.IsNullOrEmpty(user.Password))
|
|
|
+ {
|
|
|
+ passwordHash = new PasswordHash(_cryptographyProvider);
|
|
|
+ }
|
|
|
+ else
|
|
|
{
|
|
|
- // return BCrypt.HashPassword(str, salt);
|
|
|
+ ConvertPasswordFormat(user);
|
|
|
+ passwordHash = new PasswordHash(user.Password);
|
|
|
}
|
|
|
|
|
|
- // legacy
|
|
|
- return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
|
|
|
+ if (passwordHash.SaltBytes != null)
|
|
|
+ {
|
|
|
+ // the password is modern format with PBKDF and we should take advantage of that
|
|
|
+ passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
|
|
|
+ return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // the password has no salt and should be called with the older method for safety
|
|
|
+ return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|