Browse Source

Remove the CryptoHelper reference from OpenIddict.Core

pull/790/head
Kévin Chalet 7 years ago
committed by GitHub
parent
commit
189aadaaf0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      eng/Versions.props
  2. 4
      samples/Mvc.Server/Startup.cs
  3. 185
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  4. 17
      src/OpenIddict.Core/OpenIddict.Core.csproj

1
eng/Versions.props

@ -8,6 +8,7 @@
<PropertyGroup> <PropertyGroup>
<AspNetCoreVersion>3.0.0-preview8.19405.7</AspNetCoreVersion> <AspNetCoreVersion>3.0.0-preview8.19405.7</AspNetCoreVersion>
<BclAsyncInterfacesVersion>1.0.0-preview8.19405.3</BclAsyncInterfacesVersion> <BclAsyncInterfacesVersion>1.0.0-preview8.19405.3</BclAsyncInterfacesVersion>
<BouncyCastleVersion>1.8.5</BouncyCastleVersion>
<CoreFxVersion>4.4.0</CoreFxVersion> <CoreFxVersion>4.4.0</CoreFxVersion>
<CryptoHelperVersion>3.0.2</CryptoHelperVersion> <CryptoHelperVersion>3.0.2</CryptoHelperVersion>
<DataAnnotationsVersion>4.4.0</DataAnnotationsVersion> <DataAnnotationsVersion>4.4.0</DataAnnotationsVersion>

4
samples/Mvc.Server/Startup.cs

@ -52,7 +52,7 @@ namespace Mvc.Server
services.AddOpenIddict() services.AddOpenIddict()
// Register the OpenIddict core services. // Register the OpenIddict core components.
.AddCore(options => .AddCore(options =>
{ {
// Configure OpenIddict to use the Entity Framework Core stores and models. // Configure OpenIddict to use the Entity Framework Core stores and models.
@ -60,7 +60,7 @@ namespace Mvc.Server
.UseDbContext<ApplicationDbContext>(); .UseDbContext<ApplicationDbContext>();
}) })
// Register the OpenIddict server handler. // Register the OpenIddict server components.
.AddServer(options => .AddServer(options =>
{ {
// Enable the authorization, logout, token and userinfo endpoints. // Enable the authorization, logout, token and userinfo endpoints.

185
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -10,15 +10,22 @@ using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoHelper;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; 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 namespace OpenIddict.Core
{ {
/// <summary> /// <summary>
@ -1131,7 +1138,72 @@ namespace OpenIddict.Core
throw new ArgumentException("The secret cannot be null or empty.", nameof(secret)); throw new ArgumentException("The secret cannot be null or empty.", nameof(secret));
} }
return new ValueTask<string>(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<string>(
#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<byte> HashSecret(string secret, ReadOnlySpan<byte> salt,
HashAlgorithmName algorithm, int iterations, int length)
{
var key = DeriveKey(secret, salt, algorithm, iterations, length);
var payload = new Span<byte>(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<byte> 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);
}
} }
/// <summary> /// <summary>
@ -1160,7 +1232,7 @@ namespace OpenIddict.Core
try try
{ {
return new ValueTask<bool>(Crypto.VerifyHashedPassword(comparand, secret)); return new ValueTask<bool>(VerifyHashedSecret(comparand, secret));
} }
catch (Exception exception) catch (Exception exception)
@ -1170,6 +1242,113 @@ namespace OpenIddict.Core
return new ValueTask<bool>(false); return new ValueTask<bool>(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<byte>(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<byte> 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<byte> DeriveKey(string secret, ReadOnlySpan<byte> 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<byte> left, ReadOnlySpan<byte> 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<long> IOpenIddictApplicationManager.CountAsync(CancellationToken cancellationToken) ValueTask<long> IOpenIddictApplicationManager.CountAsync(CancellationToken cancellationToken)

17
src/OpenIddict.Core/OpenIddict.Core.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>net472;netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@ -14,7 +14,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptoHelper" Version="$(CryptoHelperVersion)" />
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" /> <PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(ExtensionsVersion)" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(ExtensionsVersion)" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="$(ExtensionsVersion)" />
@ -22,6 +21,20 @@
<PackageReference Include="System.Linq.Async" Version="$(LinqAsyncVersion)" /> <PackageReference Include="System.Linq.Async" Version="$(LinqAsyncVersion)" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Portable.BouncyCastle" Version="$(BouncyCastleVersion)" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net472' Or '$(TargetFramework)' == 'netstandard2.1' ">
<DefineConstants>$(DefineConstants);SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<DefineConstants>$(DefineConstants);SUPPORTS_BASE64_SPAN_CONVERSION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_TIME_CONSTANT_COMPARISONS</DefineConstants>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\shared\OpenIddict.Extensions\*\*.cs" /> <Compile Include="..\..\shared\OpenIddict.Extensions\*\*.cs" />
</ItemGroup> </ItemGroup>

Loading…
Cancel
Save