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:
Bond_009 2021-11-10 22:34:54 +01:00
parent 4c88bf3fe3
commit 5265b3eee7
9 changed files with 88 additions and 113 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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