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
pull/6818/head
Bond_009 3 years ago
parent 4c88bf3fe3
commit 5265b3eee7

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography;
using static MediaBrowser.Common.Cryptography.Constants;
using static MediaBrowser.Model.Cryptography.Constants;
namespace Emby.Server.Implementations.Cryptography
{
@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
/// </summary>
public class CryptographyProvider : ICryptoProvider
{
// 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
// TODO: remove when not needed for backwards compat
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
};
/// <inheritdoc />
public string DefaultHashMethod => "PBKDF2";
public string DefaultHashMethod => "PBKDF2-SHA512";
/// <inheritdoc />
public IEnumerable<string> GetSupportedHashMethods()
=> _supportedHashMethods;
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
{
// downgrading for now as we need this library to be dotnetstandard compliant
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
if (method != DefaultHashMethod)
{
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
}
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
return r.GetBytes(32);
byte[] salt = GenerateSalt();
return new PasswordHash(
DefaultHashMethod,
Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
DefaultIterations,
HashAlgorithmName.SHA512,
DefaultOutputLength),
salt,
new Dictionary<string, string>
{
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
});
}
/// <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 (salt.Length == 0)
if (!_supportedHashMethods.Contains(hash.Id))
{
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(salt, 0, salted, bytes.Length, salt.Length);
return h.ComputeHash(salted);
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
}
/// <inheritdoc />
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
/// <inheritdoc />
public byte[] GenerateSalt()
=> GenerateSalt(DefaultSaltLength);
/// <inheritdoc />
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.Headers;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
@ -648,7 +649,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken)
{
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
// Schedules Direct requires the hex to be lowercase
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();

@ -1,9 +1,6 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Cryptography;
using MediaBrowser.Controller.Authentication;
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
if (resolvedUser.Password != null)
if (resolvedUser.Password == null)
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(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}");
}
throw new AuthenticationException("Invalid username or password");
}
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
success = _cryptographyProvider.Verify(readyHash, password);
if (!success)
{
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
{
Username = username

@ -5,7 +5,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@ -13,7 +12,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Events.Users;
using MediaBrowser.Common;
using MediaBrowser.Common.Cryptography;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
@ -818,11 +816,7 @@ namespace Jellyfin.Server.Implementations.Users
{
// Check easy password
var passwordHash = PasswordHash.Parse(user.EasyPassword);
var hash = _cryptoProvider.ComputeHash(
passwordHash.Id,
Encoding.UTF8.GetBytes(password),
passwordHash.Salt.ToArray());
success = passwordHash.Hash.SequenceEqual(hash);
success = _cryptoProvider.Verify(passwordHash, password);
}
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>
/// Class containing global constants for Jellyfin Cryptography.
@ -8,11 +8,16 @@ namespace MediaBrowser.Common.Cryptography
/// <summary>
/// The default length for new salts.
/// </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>
/// The default amount of iterations for hashing passwords.
/// </summary>
public const int DefaultIterations = 1000;
public const int DefaultIterations = 120000;
}
}

@ -1,6 +1,6 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System;
namespace MediaBrowser.Model.Cryptography
{
@ -8,11 +8,14 @@ namespace MediaBrowser.Model.Cryptography
{
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);
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
bool Verify(PasswordHash hash, ReadOnlySpan<char> password);
byte[] GenerateSalt();

@ -4,7 +4,7 @@ using System;
using System.Collections.Generic;
using System.Text;
namespace MediaBrowser.Common.Cryptography
namespace MediaBrowser.Model.Cryptography
{
// Defined from this hash storage spec
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Common.Cryptography;
using MediaBrowser.Model.Cryptography;
using Xunit;
namespace Jellyfin.Common.Tests.Cryptography
namespace Jellyfin.Model.Tests.Cryptography
{
public static class PasswordHashTests
{
Loading…
Cancel
Save