mirror of https://github.com/jellyfin/jellyfin.git
Replace PBKDF2-SHA1 with PBKDF2-SHA512
This also migrates already created passwords on login Source for the number of iterations: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
This commit is contained in:
parent
4c88bf3fe3
commit
5265b3eee7
|
@ -1,9 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
using static MediaBrowser.Common.Cryptography.Constants;
|
using static MediaBrowser.Model.Cryptography.Constants;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Cryptography
|
namespace Emby.Server.Implementations.Cryptography
|
||||||
{
|
{
|
||||||
|
@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CryptographyProvider : ICryptoProvider
|
public class CryptographyProvider : ICryptoProvider
|
||||||
{
|
{
|
||||||
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
// TODO: remove when not needed for backwards compat
|
||||||
// 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
|
|
||||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||||
{
|
{
|
||||||
"MD5",
|
"MD5",
|
||||||
|
@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string DefaultHashMethod => "PBKDF2";
|
public string DefaultHashMethod => "PBKDF2-SHA512";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<string> GetSupportedHashMethods()
|
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
|
||||||
=> _supportedHashMethods;
|
|
||||||
|
|
||||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
|
||||||
{
|
{
|
||||||
// downgrading for now as we need this library to be dotnetstandard compliant
|
byte[] salt = GenerateSalt();
|
||||||
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
return new PasswordHash(
|
||||||
if (method != DefaultHashMethod)
|
DefaultHashMethod,
|
||||||
{
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
password,
|
||||||
}
|
salt,
|
||||||
|
DefaultIterations,
|
||||||
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
|
HashAlgorithmName.SHA512,
|
||||||
return r.GetBytes(32);
|
DefaultOutputLength),
|
||||||
|
salt,
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
|
||||||
{
|
{
|
||||||
if (hashMethod == DefaultHashMethod)
|
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
|
return hash.Hash.SequenceEqual(
|
||||||
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
|
password,
|
||||||
|
hash.Salt,
|
||||||
|
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||||
|
HashAlgorithmName.SHA1,
|
||||||
|
32));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_supportedHashMethods.Contains(hashMethod))
|
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
return hash.Hash.SequenceEqual(
|
||||||
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
|
password,
|
||||||
|
hash.Salt,
|
||||||
|
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||||
|
HashAlgorithmName.SHA512,
|
||||||
|
DefaultOutputLength));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
|
if (!_supportedHashMethods.Contains(hash.Id))
|
||||||
if (salt.Length == 0)
|
|
||||||
{
|
{
|
||||||
return h.ComputeHash(bytes);
|
throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] salted = new byte[bytes.Length + salt.Length];
|
using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(password.ToArray());
|
||||||
|
if (hash.Salt.Length == 0)
|
||||||
|
{
|
||||||
|
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
|
||||||
Array.Copy(bytes, salted, bytes.Length);
|
Array.Copy(bytes, salted, bytes.Length);
|
||||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
|
||||||
return h.ComputeHash(salted);
|
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
|
||||||
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt()
|
public byte[] GenerateSalt()
|
||||||
=> GenerateSalt(DefaultSaltLength);
|
=> GenerateSalt(DefaultSaltLength);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt(int length)
|
public byte[] GenerateSalt(int length)
|
||||||
=> RandomNumberGenerator.GetBytes(length);
|
{
|
||||||
|
var salt = new byte[length];
|
||||||
|
using var rng = RandomNumberGenerator.Create();
|
||||||
|
rng.GetNonZeroBytes(salt);
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -648,7 +649,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
|
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
|
||||||
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
|
var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password));
|
||||||
// TODO: remove ToLower when Convert.ToHexString supports lowercase
|
// TODO: remove ToLower when Convert.ToHexString supports lowercase
|
||||||
// Schedules Direct requires the hex to be lowercase
|
// Schedules Direct requires the hex to be lowercase
|
||||||
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
|
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Cryptography;
|
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
|
|
||||||
|
@ -61,35 +58,25 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the case when the stored password is null, but the user tried to login with a password
|
// Handle the case when the stored password is null, but the user tried to login with a password
|
||||||
if (resolvedUser.Password != null)
|
if (resolvedUser.Password == null)
|
||||||
{
|
{
|
||||||
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
|
throw new AuthenticationException("Invalid username or password");
|
||||||
|
|
||||||
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
|
|
||||||
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
|
|
||||||
|| _cryptographyProvider.DefaultHashMethod == readyHash.Id)
|
|
||||||
{
|
|
||||||
byte[] calculatedHash = _cryptographyProvider.ComputeHash(
|
|
||||||
readyHash.Id,
|
|
||||||
passwordBytes,
|
|
||||||
readyHash.Salt.ToArray());
|
|
||||||
|
|
||||||
if (readyHash.Hash.SequenceEqual(calculatedHash))
|
|
||||||
{
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
|
||||||
|
success = _cryptographyProvider.Verify(readyHash, password);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Invalid username or password");
|
throw new AuthenticationException("Invalid username or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate old hashes to the new default
|
||||||
|
if (!string.Equals(readyHash.Id, _cryptographyProvider.DefaultHashMethod, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ChangePassword(resolvedUser, password);
|
||||||
|
}
|
||||||
|
|
||||||
return Task.FromResult(new ProviderAuthenticationResult
|
return Task.FromResult(new ProviderAuthenticationResult
|
||||||
{
|
{
|
||||||
Username = username
|
Username = username
|
||||||
|
|
|
@ -5,7 +5,6 @@ using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
@ -13,7 +12,6 @@ using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
using Jellyfin.Data.Events.Users;
|
using Jellyfin.Data.Events.Users;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Cryptography;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
|
@ -818,11 +816,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
{
|
{
|
||||||
// Check easy password
|
// Check easy password
|
||||||
var passwordHash = PasswordHash.Parse(user.EasyPassword);
|
var passwordHash = PasswordHash.Parse(user.EasyPassword);
|
||||||
var hash = _cryptoProvider.ComputeHash(
|
success = _cryptoProvider.Verify(passwordHash, password);
|
||||||
passwordHash.Id,
|
|
||||||
Encoding.UTF8.GetBytes(password),
|
|
||||||
passwordHash.Salt.ToArray());
|
|
||||||
success = passwordHash.Hash.SequenceEqual(hash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (authenticationProvider, username, success);
|
return (authenticationProvider, username, success);
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using MediaBrowser.Model.Cryptography;
|
|
||||||
using static MediaBrowser.Common.Cryptography.Constants;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Common.Cryptography
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class containing extension methods for working with Jellyfin cryptography objects.
|
|
||||||
/// </summary>
|
|
||||||
public static class CryptoExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="PasswordHash" /> instance.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cryptoProvider">The <see cref="ICryptoProvider" /> instance used.</param>
|
|
||||||
/// <param name="password">The password that will be hashed.</param>
|
|
||||||
/// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns>
|
|
||||||
public static PasswordHash CreatePasswordHash(this ICryptoProvider cryptoProvider, string password)
|
|
||||||
{
|
|
||||||
byte[] salt = cryptoProvider.GenerateSalt();
|
|
||||||
return new PasswordHash(
|
|
||||||
cryptoProvider.DefaultHashMethod,
|
|
||||||
cryptoProvider.ComputeHashWithDefaultMethod(
|
|
||||||
Encoding.UTF8.GetBytes(password),
|
|
||||||
salt),
|
|
||||||
salt,
|
|
||||||
new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
namespace MediaBrowser.Common.Cryptography
|
namespace MediaBrowser.Model.Cryptography
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class containing global constants for Jellyfin Cryptography.
|
/// Class containing global constants for Jellyfin Cryptography.
|
||||||
|
@ -8,11 +8,16 @@ namespace MediaBrowser.Common.Cryptography
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default length for new salts.
|
/// The default length for new salts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int DefaultSaltLength = 64;
|
public const int DefaultSaltLength = 128 / 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default output length.
|
||||||
|
/// </summary>
|
||||||
|
public const int DefaultOutputLength = 512 / 8;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default amount of iterations for hashing passwords.
|
/// The default amount of iterations for hashing passwords.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int DefaultIterations = 1000;
|
public const int DefaultIterations = 120000;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Cryptography
|
namespace MediaBrowser.Model.Cryptography
|
||||||
{
|
{
|
||||||
|
@ -8,11 +8,14 @@ namespace MediaBrowser.Model.Cryptography
|
||||||
{
|
{
|
||||||
string DefaultHashMethod { get; }
|
string DefaultHashMethod { get; }
|
||||||
|
|
||||||
IEnumerable<string> GetSupportedHashMethods();
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="PasswordHash" /> instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">The password that will be hashed.</param>
|
||||||
|
/// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns>
|
||||||
|
PasswordHash CreatePasswordHash(ReadOnlySpan<char> password);
|
||||||
|
|
||||||
byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt);
|
bool Verify(PasswordHash hash, ReadOnlySpan<char> password);
|
||||||
|
|
||||||
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
|
|
||||||
|
|
||||||
byte[] GenerateSalt();
|
byte[] GenerateSalt();
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace MediaBrowser.Common.Cryptography
|
namespace MediaBrowser.Model.Cryptography
|
||||||
{
|
{
|
||||||
// Defined from this hash storage spec
|
// Defined from this hash storage spec
|
||||||
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
|
@ -1,9 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using MediaBrowser.Common.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Jellyfin.Common.Tests.Cryptography
|
namespace Jellyfin.Model.Tests.Cryptography
|
||||||
{
|
{
|
||||||
public static class PasswordHashTests
|
public static class PasswordHashTests
|
||||||
{
|
{
|
Loading…
Reference in New Issue