Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

385 lines
15 KiB

/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace OpenIddict.Extensions;
/// <summary>
/// Exposes common polyfills used by the OpenIddict assemblies.
/// </summary>
internal static class OpenIddictPolyfills
{
extension(Convert)
{
#if !NET
/// <summary>Converts the specified string, which encodes binary data as hex characters, to an equivalent 8-bit unsigned integer array.</summary>
/// <param name="s">The string to convert.</param>
/// <returns>An array of 8-bit unsigned integers that is equivalent to <paramref name="s"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="s"/> is <code>null</code>.</exception>
/// <exception cref="FormatException">The length of <paramref name="s"/>, is not zero or a multiple of 2.</exception>
/// <exception cref="FormatException">The format of <paramref name="s"/> is invalid. <paramref name="s"/> contains a non-hex character.</exception>
public static byte[] FromHexString(string s)
{
if ((uint) s.Length % 2 is not 0)
{
throw new FormatException(SR.GetResourceString(SR.ID0413));
}
var array = new byte[s.Length / 2];
for (var index = 0; index < s.Length; index += 2)
{
array[index / 2] = Convert.ToByte(s.Substring(index, 2), 16);
}
return array;
}
#endif
}
extension(HMACSHA256)
{
#if !NET
/// <summary>
/// Computes the HMAC of data using the SHA256 algorithm.
/// </summary>
/// <param name="key">The HMAC key.</param>
/// <param name="source">The data to HMAC.</param>
/// <returns>The HMAC of the data.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="key" /> or <paramref name="source" /> is <see langword="null" />.
/// </exception>
public static byte[] HashData(byte[] key, byte[] source)
{
ArgumentNullException.ThrowIfNull(key);
ArgumentNullException.ThrowIfNull(source);
using var algorithm = new HMACSHA256(key);
return algorithm.ComputeHash(source);
}
#endif
}
extension(OperatingSystem)
{
#if !NET
/// <summary>
/// Indicates whether the current application is running on Android.
/// </summary>
public static bool IsAndroid() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID"));
/// <summary>
/// Check for the Android API level (returned by 'ro.build.version.sdk') with a >=
/// version comparison. Used to guard APIs that were added in the given Android release.
/// </summary>
public static bool IsAndroidVersionAtLeast(int major, int minor = 0, int build = 0, int revision = 0)
=> IsAndroid() && IsOSVersionAtLeast(major, minor, build, revision);
/// <summary>
/// Indicates whether the current application is running on iOS or MacCatalyst.
/// </summary>
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOS() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS"));
/// <summary>
/// Check for the iOS/MacCatalyst version (returned by 'libobjc.get_operatingSystemVersion')
/// with a >= version comparison. Used to guard APIs that were added in the given iOS release.
/// </summary>
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOSVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsIOS() && IsOSVersionAtLeast(major, minor, build, 0);
/// <summary>
/// Indicates whether the current application is running on Linux.
/// </summary>
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
/// <summary>
/// Indicates whether the current application is running on Mac Catalyst.
/// </summary>
public static bool IsMacCatalyst() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("MACCATALYST"));
/// <summary>
/// Check for the Mac Catalyst version (iOS version as presented in Apple documentation) with a >=
/// version comparison. Used to guard APIs that were added in the given Mac Catalyst release.
/// </summary>
public static bool IsMacCatalystVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsMacCatalyst() && IsOSVersionAtLeast(major, minor, build, 0);
/// <summary>
/// Indicates whether the current application is running on macOS.
/// </summary>
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
/// <summary>
/// Check for the macOS version (returned by 'libobjc.get_operatingSystemVersion') with a >=
/// version comparison. Used to guard APIs that were added in the given macOS release.
/// </summary>
public static bool IsMacOSVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsMacOS() && IsOSVersionAtLeast(major, minor, build, 0);
/// <summary>
/// Indicates whether the current application is running on Windows.
/// </summary>
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
/// <summary>
/// Check for the Windows version (returned by 'RtlGetVersion') with a >= version
/// comparison. Used to guard APIs that were added in the given Windows release.
/// </summary>
public static bool IsWindowsVersionAtLeast(int major, int minor = 0, int build = 0, int revision = 0)
{
if (Environment.OSVersion.Platform is PlatformID.Win32NT &&
Environment.OSVersion.Version >= new Version(major, minor, build, revision))
{
return true;
}
// Note: on older versions of .NET, Environment.OSVersion.Version is known to be affected by
// the compatibility shims used by Windows 10+ when the application doesn't have a manifest
// that explicitly indicates it's compatible with Windows 10 and higher. To avoid that, a
// second pass using RuntimeInformation.OSDescription (that calls NtDll.RtlGetVersion() under
// the hood) is made. Note: no version is returned on UWP due to the missing Win32 API.
return RuntimeInformation.OSDescription.StartsWith("Microsoft Windows ", StringComparison.OrdinalIgnoreCase) &&
RuntimeInformation.OSDescription["Microsoft Windows ".Length..] is string value &&
Version.TryParse(value, out Version? version) && version >= new Version(major, minor, build, revision);
}
#endif
}
extension(Rfc2898DeriveBytes)
{
#if !NET
/// <summary>
/// Creates a PBKDF2 derived key from a password.
/// </summary>
/// <param name="password">The password used to derive the key.</param>
/// <param name="salt">The key salt used to derive the key.</param>
/// <param name="iterations">The number of iterations for the operation.</param>
/// <param name="hashAlgorithm">The hash algorithm to use to derive the key.</param>
/// <param name="outputLength">The size of key to derive.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// <para><paramref name="outputLength" /> is not zero or a positive value.</para>
/// <para>-or-</para>
/// <para><paramref name="iterations" /> is not a positive value.</para>
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="hashAlgorithm" /> has a <see cref="HashAlgorithmName.Name" />
/// that is empty or <see langword="null" />.
/// </exception>
/// <exception cref="CryptographicException">
/// <paramref name="hashAlgorithm" /> is an unsupported hash algorithm. Supported algorithms
/// are <see cref="HashAlgorithmName.SHA1" />, <see cref="HashAlgorithmName.SHA256" />,
/// <see cref="HashAlgorithmName.SHA384" />, and <see cref="HashAlgorithmName.SHA512" />.
/// </exception>
public static byte[] Pbkdf2(
ReadOnlySpan<char> password,
ReadOnlySpan<byte> salt,
int iterations,
HashAlgorithmName hashAlgorithm,
int outputLength)
{
ArgumentOutOfRangeException.ThrowIfNegative(outputLength);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(iterations);
using var algorithm = new Rfc2898DeriveBytes(password.ToString(), salt.ToArray(), iterations, hashAlgorithm);
return algorithm.GetBytes(outputLength);
}
#endif
}
extension(SHA384)
{
#if !NET
/// <summary>
/// Computes the hash of data using the SHA384 algorithm.
/// </summary>
/// <param name="source">The data to hash.</param>
/// <returns>The hash of the data.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="source" /> is <see langword="null" />.
/// </exception>
public static byte[] HashData(byte[] source)
{
ArgumentNullException.ThrowIfNull(source);
using var algorithm = SHA384.Create();
return algorithm.ComputeHash(source);
}
#endif
}
extension<TResult>(ValueTask<TResult>)
{
#if !NET
/// <summary>
/// Gets a task that has already completed successfully.
/// </summary>
public static ValueTask<TResult> CompletedTask => default;
#endif
}
#if !NET
static bool IsOSVersionAtLeast(int major, int minor, int build, int revision)
{
Version current = Environment.OSVersion.Version;
if (current.Major != major)
{
return current.Major > major;
}
if (current.Minor != minor)
{
return current.Minor > minor;
}
int currentBuild = current.Build < 0 ? 0 : current.Build;
build = build < 0 ? 0 : build;
if (currentBuild != build)
{
return currentBuild > build;
}
int currentRevision = current.Revision < 0 ? 0 : current.Revision;
revision = revision < 0 ? 0 : revision;
return currentRevision >= revision;
}
#endif
extension(X509ChainPolicy policy)
{
#if !NET
public X509ChainPolicy Clone()
{
var clone = new X509ChainPolicy
{
RevocationMode = policy.RevocationMode,
RevocationFlag = policy.RevocationFlag,
UrlRetrievalTimeout = policy.UrlRetrievalTimeout,
VerificationFlags = policy.VerificationFlags,
VerificationTime = policy.VerificationTime,
#if NET
DisableCertificateDownloads = policy.DisableCertificateDownloads,
TrustMode = policy.TrustMode,
VerificationTimeIgnored = policy.VerificationTimeIgnored
#endif
};
if (policy.ApplicationPolicy.Count is > 0)
{
for (var index = 0; index < policy.ApplicationPolicy.Count; index++)
{
clone.ApplicationPolicy.Add(policy.ApplicationPolicy[index]);
}
}
if (policy.CertificatePolicy.Count is > 0)
{
for (var index = 0; index < policy.CertificatePolicy.Count; index++)
{
clone.CertificatePolicy.Add(policy.CertificatePolicy[index]);
}
}
#if NET
clone.CustomTrustStore.AddRange(policy.CustomTrustStore);
#endif
clone.ExtraStore.AddRange(policy.ExtraStore);
return clone;
}
#endif
}
}
/// <summary>
/// Exposes common polyfills used by the OpenIddict assemblies.
/// </summary>
internal static class OpenIddictPolyfills_SHA512
{
extension(SHA512)
{
#if !NET
/// <summary>
/// Computes the hash of data using the SHA512 algorithm.
/// </summary>
/// <param name="source">The data to hash.</param>
/// <returns>The hash of the data.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="source" /> is <see langword="null" />.
/// </exception>
public static byte[] HashData(byte[] source)
{
ArgumentNullException.ThrowIfNull(source);
using var algorithm = SHA512.Create();
return algorithm.ComputeHash(source);
}
#endif
}
}
#if !NET
internal static class CryptographicOperations
{
/// <summary>
/// Determine the equality of two byte sequences in an amount of time which depends on
/// the length of the sequences, but not the values.
/// </summary>
/// <param name="left">The first buffer to compare.</param>
/// <param name="right">The second buffer to compare.</param>
/// <returns>
/// <c>true</c> if <paramref name="left"/> and <paramref name="right"/> have the same
/// values for <see cref="ReadOnlySpan{T}.Length"/> and the same contents, <c>false</c>
/// otherwise.
/// </returns>
/// <remarks>
/// This method compares two buffers' contents for equality in a manner which does not
/// leak timing information, making it ideal for use within cryptographic routines.
/// This method will short-circuit and return <c>false</c> only if <paramref name="left"/>
/// and <paramref name="right"/> have different lengths.
///
/// Fixed-time behavior is guaranteed in all other cases, including if <paramref name="left"/>
/// and <paramref name="right"/> reference the same address.
/// </remarks>
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static bool FixedTimeEquals(ReadOnlySpan<byte> left, ReadOnlySpan<byte> right)
{
// Note: the logic used here is directly taken from the official implementation of
// the CryptographicOperations.FixedTimeEquals() method introduced in .NET Core 2.1.
//
// See https://github.com/dotnet/corefx/pull/27103 for more information.
// 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 length = left.Length;
var accumulator = 0;
for (var index = 0; index < length; index++)
{
accumulator |= left[index] - right[index];
}
return accumulator is 0;
}
}
#endif