diff --git a/eng/Versions.props b/eng/Versions.props index 8583ed33..a1a6de8e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,6 +8,7 @@ 3.0.0-preview8.19405.7 1.0.0-preview8.19405.3 + 1.8.5 4.4.0 3.0.2 4.4.0 diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 4ce5175c..6938406a 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -52,7 +52,7 @@ namespace Mvc.Server services.AddOpenIddict() - // Register the OpenIddict core services. + // Register the OpenIddict core components. .AddCore(options => { // Configure OpenIddict to use the Entity Framework Core stores and models. @@ -60,7 +60,7 @@ namespace Mvc.Server .UseDbContext(); }) - // Register the OpenIddict server handler. + // Register the OpenIddict server components. .AddServer(options => { // Enable the authorization, logout, token and userinfo endpoints. diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 89d63edd..4ef72521 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -10,15 +10,22 @@ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.CompilerServices; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; -using CryptoHelper; using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +#if !SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +#endif + namespace OpenIddict.Core { /// @@ -1131,7 +1138,72 @@ namespace OpenIddict.Core throw new ArgumentException("The secret cannot be null or empty.", nameof(secret)); } - return new ValueTask(Crypto.HashPassword(secret)); + // Note: the PRF, iteration count, salt length and key length currently all match the default values + // used by CryptoHelper and ASP.NET Core Identity but this may change in the future, if necessary. + + var salt = new byte[128 / 8]; + +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(salt); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(salt); +#endif + + var hash = HashSecret(secret, salt, HashAlgorithmName.SHA256, iterations: 10_000, length: 256 / 8); + + return new ValueTask( +#if SUPPORTS_BASE64_SPAN_CONVERSION + Convert.ToBase64String(hash) +#else + Convert.ToBase64String(hash.ToArray()) +#endif + ); + + // Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x), + // which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that + // secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa). + + static ReadOnlySpan HashSecret(string secret, ReadOnlySpan salt, + HashAlgorithmName algorithm, int iterations, int length) + { + var key = DeriveKey(secret, salt, algorithm, iterations, length); + var payload = new Span(new byte[13 + salt.Length + key.Length]); + + // Write the format marker. + payload[0] = 0x01; + + // Write the hashing algorithm version. + WriteNetworkByteOrder(payload, 1, algorithm switch + { + { Name: nameof(SHA1) } => (uint) 0, + { Name: nameof(SHA256) } => (uint) 1, + { Name: nameof(SHA512) } => (uint) 2, + _ => throw new InvalidOperationException("The specified HMAC algorithm is not valid.") + }); + + // Write the iteration count of the algorithm. + WriteNetworkByteOrder(payload, 5, (uint) iterations); + + // Write the size of the salt. + WriteNetworkByteOrder(payload, 9, (uint) salt.Length); + + // Write the salt. + salt.CopyTo(payload.Slice(13)); + + // Write the subkey. + key.CopyTo(payload.Slice(13 + salt.Length)); + + return payload; + } + + static void WriteNetworkByteOrder(Span buffer, int offset, uint value) + { + buffer[offset + 0] = (byte) (value >> 24); + buffer[offset + 1] = (byte) (value >> 16); + buffer[offset + 2] = (byte) (value >> 8); + buffer[offset + 3] = (byte) (value >> 0); + } } /// @@ -1160,7 +1232,7 @@ namespace OpenIddict.Core try { - return new ValueTask(Crypto.VerifyHashedPassword(comparand, secret)); + return new ValueTask(VerifyHashedSecret(comparand, secret)); } catch (Exception exception) @@ -1170,6 +1242,113 @@ namespace OpenIddict.Core return new ValueTask(false); } + + // Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x), + // which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that + // secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa). + + static bool VerifyHashedSecret(string hash, string secret) + { + var payload = new ReadOnlySpan(Convert.FromBase64String(hash)); + if (payload.Length == 0) + { + return false; + } + + // Verify the hashing format version. + if (payload[0] != 0x01) + { + return false; + } + + // Read the hashing algorithm version. + var algorithm = (int) ReadNetworkByteOrder(payload, 1) switch + { + 0 => HashAlgorithmName.SHA1, + 1 => HashAlgorithmName.SHA256, + 2 => HashAlgorithmName.SHA512, + _ => throw new InvalidOperationException("The specified hash algorithm is not valid.") + }; + + // Read the iteration count of the algorithm. + var iterations = (int) ReadNetworkByteOrder(payload, 5); + + // Read the size of the salt and ensure it's more than 128 bits. + var saltLength = (int) ReadNetworkByteOrder(payload, 9); + if (saltLength < 128 / 8) + { + return false; + } + + // Read the salt. + var salt = payload.Slice(13, saltLength); + + // Ensure the derived key length is more than 128 bits. + var keyLength = payload.Length - 13 - salt.Length; + if (keyLength < 128 / 8) + { + return false; + } + + return FixedTimeEquals( + left: payload.Slice(13 + salt.Length, keyLength), + right: DeriveKey(secret, salt, algorithm, iterations, keyLength)); + } + + static uint ReadNetworkByteOrder(ReadOnlySpan buffer, int offset) => + ((uint) buffer[offset + 0] << 24) | + ((uint) buffer[offset + 1] << 16) | + ((uint) buffer[offset + 2] << 8) | + ((uint) buffer[offset + 3]); + } + + private static ReadOnlySpan DeriveKey(string secret, ReadOnlySpan salt, + HashAlgorithmName algorithm, int iterations, int length) + { +#if SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM + using var generator = new Rfc2898DeriveBytes(secret, salt.ToArray(), iterations, algorithm); + return generator.GetBytes(length); +#else + var generator = new Pkcs5S2ParametersGenerator(algorithm switch + { + { Name: nameof(SHA1) } => (IDigest) new Sha1Digest(), + { Name: nameof(SHA256) } => new Sha256Digest(), + { Name: nameof(SHA512) } => new Sha512Digest(), + _ => throw new InvalidOperationException("The specified hash algorithm is not valid.") + }); + + generator.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(secret.ToCharArray()), salt.ToArray(), iterations); + + var key = (KeyParameter) generator.GenerateDerivedMacParameters(length * 8); + return key.GetKey(); +#endif + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) + { +#if SUPPORTS_TIME_CONSTANT_COMPARISONS + return CryptographicOperations.FixedTimeEquals(left, right); +#else + // Note: these null checks can be theoretically considered as early checks + // (which would defeat the purpose of a time-constant comparison method), + // but the expected string length is the only information an attacker + // could get at this stage, which is not critical where this method is used. + + if (left.Length != right.Length) + { + return false; + } + + var result = true; + + for (var index = 0; index < left.Length; index++) + { + result &= left[index] == right[index]; + } + + return result; +#endif } ValueTask IOpenIddictApplicationManager.CountAsync(CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/OpenIddict.Core.csproj b/src/OpenIddict.Core/OpenIddict.Core.csproj index b76172cb..89b00f84 100644 --- a/src/OpenIddict.Core/OpenIddict.Core.csproj +++ b/src/OpenIddict.Core/OpenIddict.Core.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1 + net472;netstandard2.0;netstandard2.1 @@ -14,7 +14,6 @@ - @@ -22,6 +21,20 @@ + + + + + + $(DefineConstants);SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM + + + + $(DefineConstants);SUPPORTS_BASE64_SPAN_CONVERSION + $(DefineConstants);SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + $(DefineConstants);SUPPORTS_TIME_CONSTANT_COMPARISONS + +