diff options
Diffstat (limited to 'Emby.Server.Implementations')
4 files changed, 621 insertions, 374 deletions
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 09fdbc856..2f2fd9592 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,40 +1,149 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Cryptography -{ - public class CryptographyProvider : ICryptoProvider - { - public Guid GetMD5(string str) +using System;
+using System.Collections.Generic;
+using System.Globalization; +using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using MediaBrowser.Model.Cryptography;
+
+namespace Emby.Server.Implementations.Cryptography
+{
+ public class CryptographyProvider : ICryptoProvider
+ {
+ private HashSet<string> SupportedHashMethods;
+ public string DefaultHashMethod => "SHA256";
+ private RandomNumberGenerator rng;
+ private int defaultiterations = 1000;
+ public CryptographyProvider()
+ {
+ //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
+ SupportedHashMethods = new HashSet<string>()
+ {
+ "MD5"
+ ,"System.Security.Cryptography.MD5"
+ ,"SHA"
+ ,"SHA1"
+ ,"System.Security.Cryptography.SHA1"
+ ,"SHA256"
+ ,"SHA-256"
+ ,"System.Security.Cryptography.SHA256"
+ ,"SHA384"
+ ,"SHA-384"
+ ,"System.Security.Cryptography.SHA384"
+ ,"SHA512"
+ ,"SHA-512"
+ ,"System.Security.Cryptography.SHA512"
+ };
+ rng = RandomNumberGenerator.Create();
+ }
+
+ public Guid GetMD5(string str)
+ {
+ return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
+ }
+
+ public byte[] ComputeSHA1(byte[] bytes)
+ {
+ using (var provider = SHA1.Create())
+ {
+ return provider.ComputeHash(bytes);
+ }
+ }
+
+ public byte[] ComputeMD5(Stream str)
+ {
+ using (var provider = MD5.Create())
+ {
+ return provider.ComputeHash(str);
+ }
+ }
+
+ public byte[] ComputeMD5(byte[] bytes)
+ {
+ using (var provider = MD5.Create())
+ {
+ return provider.ComputeHash(bytes);
+ }
+ }
+
+ public IEnumerable<string> GetSupportedHashMethods()
+ {
+ return SupportedHashMethods;
+ }
+
+ private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
{ - return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); - } - - public byte[] ComputeSHA1(byte[] bytes) - { - using (var provider = SHA1.Create()) - { - return provider.ComputeHash(bytes); - } - } - - public byte[] ComputeMD5(Stream str) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(str); - } - } - - public byte[] ComputeMD5(byte[] bytes) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(bytes); - } - } - } -} + //downgrading for now as we need this library to be dotnetstandard compliant
+ using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
+ {
+ return r.GetBytes(32);
+ }
+ }
+
+ public byte[] ComputeHash(string HashMethod, byte[] bytes)
+ {
+ return ComputeHash(HashMethod, bytes, new byte[0]);
+ }
+
+ public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
+ {
+ return ComputeHash(DefaultHashMethod, bytes);
+ }
+
+ public byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt)
+ {
+ if (SupportedHashMethods.Contains(HashMethod))
+ {
+ if (salt.Length == 0)
+ {
+ using (var h = HashAlgorithm.Create(HashMethod))
+ {
+ return h.ComputeHash(bytes);
+ }
+ }
+ else
+ {
+ return PBKDF2(HashMethod, bytes, salt, defaultiterations);
+ }
+ }
+ else
+ {
+ throw new CryptographicException($"Requested hash method is not supported: {HashMethod}");
+ }
+ }
+
+ public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
+ {
+ return PBKDF2(DefaultHashMethod, bytes, salt, defaultiterations);
+ }
+
+ public byte[] ComputeHash(PasswordHash hash)
+ {
+ int iterations = defaultiterations;
+ if (!hash.Parameters.ContainsKey("iterations"))
+ {
+ hash.Parameters.Add("iterations", defaultiterations.ToString(CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ try
+ {
+ iterations = int.Parse(hash.Parameters["iterations"]);
+ }
+ catch (Exception e)
+ {
+ throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e);
+ }
+ }
+ return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
+ }
+
+ public byte[] GenerateSalt()
+ {
+ byte[] salt = new byte[64];
+ rng.GetBytes(salt);
+ return salt;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index db359d7dd..3df91f71c 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -1,230 +1,264 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - /// <summary> - /// Class SQLiteUserRepository - /// </summary> - public class SqliteUserRepository : BaseSqliteRepository, IUserRepository - { - private readonly IJsonSerializer _jsonSerializer; - - public SqliteUserRepository( - ILoggerFactory loggerFactory, - IServerApplicationPaths appPaths, - IJsonSerializer jsonSerializer) - : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository))) - { - _jsonSerializer = jsonSerializer; - - DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); - } - - /// <summary> - /// Gets the name of the repository - /// </summary> - /// <value>The name.</value> - public string Name => "SQLite"; - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public void Initialize() - { - using (var connection = CreateConnection()) - { - RunDefaultInitialization(connection); - - var localUsersTableExists = TableExists(connection, "LocalUsersv2"); - - connection.RunQueries(new[] { - "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", - "drop index if exists idx_users" - }); - - if (!localUsersTableExists && TableExists(connection, "Users")) - { - TryMigrateToLocalUsersTable(connection); +using System;
+using System.Collections.Generic;
+using System.IO;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Emby.Server.Implementations.Data
+{
+ /// <summary>
+ /// Class SQLiteUserRepository
+ /// </summary>
+ public class SqliteUserRepository : BaseSqliteRepository, IUserRepository
+ {
+ private readonly IJsonSerializer _jsonSerializer;
+
+ public SqliteUserRepository(
+ ILoggerFactory loggerFactory,
+ IServerApplicationPaths appPaths,
+ IJsonSerializer jsonSerializer)
+ : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository)))
+ {
+ _jsonSerializer = jsonSerializer;
+
+ DbFilePath = Path.Combine(appPaths.DataPath, "users.db");
+ }
+
+ /// <summary>
+ /// Gets the name of the repository
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => "SQLite";
+
+ /// <summary>
+ /// Opens the connection to the database
+ /// </summary>
+ /// <returns>Task.</returns>
+ public void Initialize()
+ {
+ using (var connection = CreateConnection())
+ {
+ RunDefaultInitialization(connection);
+
+ var localUsersTableExists = TableExists(connection, "LocalUsersv2");
+
+ connection.RunQueries(new[] {
+ "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)",
+ "drop index if exists idx_users"
+ });
+
+ if (!localUsersTableExists && TableExists(connection, "Users"))
+ {
+ TryMigrateToLocalUsersTable(connection);
} - } - } - - private void TryMigrateToLocalUsersTable(ManagedConnection connection) - { - try - { - connection.RunQueries(new[] - { - "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" - }); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating users database"); - } - } - - /// <summary> - /// Save a user in the repo - /// </summary> - public void CreateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = _jsonSerializer.SerializeToBytes(user); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) - { - statement.TryBind("@guid", user.Id.ToGuidBlob()); - statement.TryBind("@data", serialized); - - statement.MoveNext(); - } - - var createdUser = GetUser(user.Id, false); - - if (createdUser == null) - { - throw new ApplicationException("created user should never be null"); - } - - user.InternalId = createdUser.InternalId; - - }, TransactionMode); - } - } - } - - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = _jsonSerializer.SerializeToBytes(user); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) - { - statement.TryBind("@InternalId", user.InternalId); - statement.TryBind("@data", serialized); - statement.MoveNext(); - } - - }, TransactionMode); - } - } - } - - private User GetUser(Guid guid, bool openLock) - { - using (openLock ? WriteLock.Read() : null) - { - using (var connection = CreateConnection(true)) - { - using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) - { - statement.TryBind("@guid", guid); - - foreach (var row in statement.ExecuteQuery()) - { - return GetUser(row); - } - } - } - } - - return null; - } - - private User GetUser(IReadOnlyList<IResultSetValue> row) - { - var id = row[0].ToInt64(); - var guid = row[1].ReadGuidFromBlob(); - - using (var stream = new MemoryStream(row[2].ToBlob())) - { - stream.Position = 0; - var user = _jsonSerializer.DeserializeFromStream<User>(stream); - user.InternalId = id; - user.Id = guid; - return user; - } - } - - /// <summary> - /// Retrieve all users from the database - /// </summary> - /// <returns>IEnumerable{User}.</returns> - public List<User> RetrieveAllUsers() - { - var list = new List<User>(); - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) - { - list.Add(GetUser(row)); - } - } - } - - return list; - } - - /// <summary> - /// Deletes the user. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">user</exception> - public void DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) - { - statement.TryBind("@id", user.InternalId); - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - } -} +
+ RemoveEmptyPasswordHashes();
+ }
+ }
+
+ private void TryMigrateToLocalUsersTable(ManagedConnection connection)
+ {
+ try
+ {
+ connection.RunQueries(new[]
+ {
+ "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users"
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error migrating users database");
+ }
+ }
+
+ private void RemoveEmptyPasswordHashes()
+ {
+ foreach (var user in RetrieveAllUsers())
+ {
+ // If the user password is the sha1 hash of the empty string, remove it
+ if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
+ || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ user.Password = null;
+ var serialized = _jsonSerializer.SerializeToBytes(user);
+
+ using (WriteLock.Write())
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
+ {
+ statement.TryBind("@InternalId", user.InternalId);
+ statement.TryBind("@data", serialized);
+ statement.MoveNext();
+ }
+
+ }, TransactionMode);
+ }
+ }
+
+ }
+
+ /// <summary>
+ /// Save a user in the repo
+ /// </summary>
+ public void CreateUser(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ var serialized = _jsonSerializer.SerializeToBytes(user);
+
+ using (WriteLock.Write())
+ {
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
+ {
+ statement.TryBind("@guid", user.Id.ToGuidBlob());
+ statement.TryBind("@data", serialized);
+
+ statement.MoveNext();
+ }
+
+ var createdUser = GetUser(user.Id, false);
+
+ if (createdUser == null)
+ {
+ throw new ApplicationException("created user should never be null");
+ }
+
+ user.InternalId = createdUser.InternalId;
+
+ }, TransactionMode);
+ }
+ }
+ }
+
+ public void UpdateUser(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ var serialized = _jsonSerializer.SerializeToBytes(user);
+
+ using (WriteLock.Write())
+ {
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
+ {
+ statement.TryBind("@InternalId", user.InternalId);
+ statement.TryBind("@data", serialized);
+ statement.MoveNext();
+ }
+
+ }, TransactionMode);
+ }
+ }
+ }
+
+ private User GetUser(Guid guid, bool openLock)
+ {
+ using (openLock ? WriteLock.Read() : null)
+ {
+ using (var connection = CreateConnection(true))
+ {
+ using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
+ {
+ statement.TryBind("@guid", guid);
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ return GetUser(row);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private User GetUser(IReadOnlyList<IResultSetValue> row)
+ {
+ var id = row[0].ToInt64();
+ var guid = row[1].ReadGuidFromBlob();
+
+ using (var stream = new MemoryStream(row[2].ToBlob()))
+ {
+ stream.Position = 0;
+ var user = _jsonSerializer.DeserializeFromStream<User>(stream);
+ user.InternalId = id;
+ user.Id = guid;
+ return user;
+ }
+ }
+
+ /// <summary>
+ /// Retrieve all users from the database
+ /// </summary>
+ /// <returns>IEnumerable{User}.</returns>
+ public List<User> RetrieveAllUsers()
+ {
+ var list = new List<User>();
+
+ using (WriteLock.Read())
+ {
+ using (var connection = CreateConnection(true))
+ {
+ foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
+ {
+ list.Add(GetUser(row));
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Deletes the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="ArgumentNullException">user</exception>
+ public void DeleteUser(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ using (WriteLock.Write())
+ {
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
+ {
+ statement.TryBind("@id", user.InternalId);
+ statement.MoveNext();
+ }
+ }, TransactionMode);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 4013ac0c8..2ac3ef424 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -1,104 +1,212 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser - { - private readonly ICryptoProvider _cryptographyProvider; - public DefaultAuthenticationProvider(ICryptoProvider crypto) - { - _cryptographyProvider = crypto; - } - - public string Name => "Default"; - - public bool IsEnabled => true; - - public Task<ProviderAuthenticationResult> Authenticate(string username, string password) - { - throw new NotImplementedException(); - } - - public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) - { - if (resolvedUser == null) - { - throw new Exception("Invalid username or password"); +using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Cryptography;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser
+ {
+ private readonly ICryptoProvider _cryptographyProvider;
+ public DefaultAuthenticationProvider(ICryptoProvider crypto)
+ {
+ _cryptographyProvider = crypto;
+ }
+
+ 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); - - if (!success) + //As long as jellyfin supports passwordless users, we need this little block here to accomodate + if (IsPasswordEmpty(resolvedUser, password)) { - throw new Exception("Invalid username or password"); + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); } - - return Task.FromResult(new ProviderAuthenticationResult +
+ 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)
+ {
+ throw new Exception("Invalid username or password");
+ }
+
+ return Task.FromResult(new ProviderAuthenticationResult
+ {
+ Username = username
+ });
+ }
+
+ //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))
{ - Username = username - }); - } - - public Task<bool> HasPassword(User user) - { - var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); - return Task.FromResult(hasConfiguredPassword); - } - - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); - } - - public Task ChangePassword(User user, string newPassword) + 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 password)
{ - string newPasswordHash = null; - - if (newPassword != null) + if (string.IsNullOrEmpty(user.Password)) { - newPasswordHash = GetHashedString(user, newPassword); + return string.IsNullOrEmpty(password); } - if (string.IsNullOrWhiteSpace(newPasswordHash)) + return false;
+ }
+
+ public Task ChangePassword(User user, string newPassword)
+ {
+ ConvertPasswordFormat(user); + //This is needed to support changing a no password user to a password user + if (string.IsNullOrEmpty(user.Password)) { - throw new ArgumentNullException(nameof(newPasswordHash)); + 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; } - user.Password = newPasswordHash; - - return Task.CompletedTask; - } - - public string GetPasswordHash(User user) - { - return string.IsNullOrEmpty(user.Password) - ? GetEmptyHashedString(user) - : user.Password; - } - - public string GetEmptyHashedString(User user) - { - return GetHashedString(user, string.Empty); - } - - /// <summary> - /// Gets the hashed string. - /// </summary> - public string GetHashedString(User user, string str) - { - var salt = user.Salt; - if (salt != null) - { - // return BCrypt.HashPassword(str, salt); + PasswordHash passwordHash = new PasswordHash(user.Password);
+ if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
+ {
+ 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(passwordHash.Hash))
+ {
+ throw new ArgumentNullException(nameof(passwordHash.Hash));
+ }
+
+ user.Password = passwordHash.ToString();
+
+ return Task.CompletedTask;
+ }
+
+ public string GetPasswordHash(User user)
+ {
+ return user.Password;
+ }
+
+ public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
+ {
+ passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
+ return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
+ }
+
+ /// <summary>
+ /// Gets the hashed string.
+ /// </summary>
+ public string GetHashedString(User user, string str)
+ {
+ PasswordHash passwordHash;
+ if (String.IsNullOrEmpty(user.Password))
+ {
+ passwordHash = new PasswordHash(_cryptographyProvider);
+ }
+ else
+ {
+ 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)));
+ }
+
+
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index dfef8e997..0f188ca75 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Events; @@ -74,7 +75,7 @@ namespace Emby.Server.Implementations.Library private readonly Func<IDtoService> _dtoServiceFactory; private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; - + private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; @@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library } } - public bool IsValidUsername(string username) + public static bool IsValidUsername(string username) { - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - foreach (var currentChar in username) - { - if (!IsValidUsernameCharacter(currentChar)) - { - return false; - } - } - return true; + //This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + return Regex.IsMatch(username, "^[\\w-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) { - return !char.Equals(i, '<') && !char.Equals(i, '>'); + return IsValidUsername(i.ToString()); } public string MakeValidUsername(string username) @@ -475,13 +471,13 @@ namespace Emby.Server.Implementations.Library private string GetLocalPasswordHash(User user) { return string.IsNullOrEmpty(user.EasyPassword) - ? _defaultAuthenticationProvider.GetEmptyHashedString(user) + ? null : user.EasyPassword; } private bool IsPasswordEmpty(User user, string passwordHash) { - return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + return string.IsNullOrEmpty(passwordHash); } /// <summary> @@ -688,7 +684,7 @@ namespace Emby.Server.Implementations.Library if (!IsValidUsername(name)) { - throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); } if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) |
