/*
* 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;
///
/// Exposes common polyfills used by the OpenIddict assemblies.
///
internal static class OpenIddictPolyfills
{
extension(CryptographicOperations)
{
#if !NET
///
/// Determine the equality of two byte sequences in an amount of time which depends on
/// the length of the sequences, but not the values.
///
/// The first buffer to compare.
/// The second buffer to compare.
///
/// true if and have the same
/// values for and the same contents, false
/// otherwise.
///
///
/// 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 false only if
/// and have different lengths.
///
/// Fixed-time behavior is guaranteed in all other cases, including if
/// and reference the same address.
///
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan 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
}
extension(HMACSHA256)
{
#if !NET
///
/// Computes the HMAC of data using the SHA256 algorithm.
///
/// The HMAC key.
/// The data to HMAC.
/// The HMAC of the data.
///
/// or is .
///
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
///
/// Indicates whether the current application is running on Android.
///
public static bool IsAndroid() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID"));
///
/// 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.
///
public static bool IsAndroidVersionAtLeast(int major, int minor = 0, int build = 0, int revision = 0)
=> IsAndroid() && IsOSVersionAtLeast(major, minor, build, revision);
///
/// Indicates whether the current application is running on iOS or MacCatalyst.
///
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOS() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS"));
///
/// 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.
///
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOSVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsIOS() && IsOSVersionAtLeast(major, minor, build, 0);
///
/// Indicates whether the current application is running on Linux.
///
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
///
/// Indicates whether the current application is running on Mac Catalyst.
///
public static bool IsMacCatalyst() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("MACCATALYST"));
///
/// 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.
///
public static bool IsMacCatalystVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsMacCatalyst() && IsOSVersionAtLeast(major, minor, build, 0);
///
/// Indicates whether the current application is running on macOS.
///
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
///
/// 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.
///
public static bool IsMacOSVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsMacOS() && IsOSVersionAtLeast(major, minor, build, 0);
///
/// Indicates whether the current application is running on Windows.
///
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
///
/// Check for the Windows version (returned by 'RtlGetVersion') with a >= version
/// comparison. Used to guard APIs that were added in the given Windows release.
///
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
///
/// Creates a PBKDF2 derived key from a password.
///
/// The password used to derive the key.
/// The key salt used to derive the key.
/// The number of iterations for the operation.
/// The hash algorithm to use to derive the key.
/// The size of key to derive.
///
/// is not zero or a positive value.
/// -or-
/// is not a positive value.
///
///
/// has a
/// that is empty or .
///
///
/// is an unsupported hash algorithm. Supported algorithms
/// are , ,
/// , and .
///
public static byte[] Pbkdf2(
ReadOnlySpan password,
ReadOnlySpan 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(ValueTask)
{
#if !NET
///
/// Gets a task that has already completed successfully.
///
public static ValueTask 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 (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]);
}
}
clone.ExtraStore.AddRange(policy.ExtraStore);
return clone;
}
#endif
}
}