/* * 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.Collections.ObjectModel; using System.Data; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Primitives; namespace OpenIddict.Extensions; /// /// Exposes common helpers used by the OpenIddict assemblies. /// internal static class OpenIddictHelpers { /// /// Determines whether the specified array contains at least one value present in the specified set. /// /// The type of the elements. /// The array. /// The set. /// /// if the specified array contains at least one /// value present in the specified set, otherwise. /// public static bool IncludesAnyFromSet(IReadOnlyList array, ISet set) { ArgumentNullException.ThrowIfNull(set); for (var index = 0; index < array.Count; index++) { var value = array[index]; if (set.Contains(value)) { return true; } } return false; } /// /// Determines whether the specified is considered fatal. /// /// The exception. /// /// if the exception is considered fatal, otherwise. /// public static bool IsFatal(Exception exception) { RuntimeHelpers.EnsureSufficientExecutionStack(); return exception switch { ThreadAbortException => true, OutOfMemoryException and not InsufficientMemoryException => true, AggregateException { InnerExceptions: var exceptions } => IsAnyFatal(exceptions), Exception { InnerException: Exception inner } => IsFatal(inner), _ => false }; static bool IsAnyFatal(ReadOnlyCollection exceptions) { for (var index = 0; index < exceptions.Count; index++) { if (IsFatal(exceptions[index])) { return true; } } return false; } } /// /// Computes an absolute URI from the specified and URIs. /// Note: if the URI is already absolute, it is directly returned. /// /// The left part. /// The right part. /// An absolute URI from the specified and . /// is not an absolute URI. [return: NotNullIfNotNull(nameof(right))] public static Uri? CreateAbsoluteUri(Uri? left, string? right) => CreateAbsoluteUri(left, !string.IsNullOrEmpty(right) ? new Uri(right, UriKind.RelativeOrAbsolute) : null); /// /// Computes an absolute URI from the specified and URIs. /// Note: if the URI is already absolute, it is directly returned. /// /// The left part. /// The right part. /// An absolute URI from the specified and . /// is not an absolute URI. [return: NotNullIfNotNull(nameof(right))] public static Uri? CreateAbsoluteUri(Uri? left, Uri? right) { if (right is null) { return null; } if (right.IsAbsoluteUri) { return right; } if (left is not { IsAbsoluteUri: true }) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left)); } // Ensure the left part ends with a trailing slash, as it is necessary // for Uri's constructor to include the last path segment in the base URI. left = left.AbsolutePath switch { null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri, [.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri, ['/'] or _ => left }; return new Uri(left, right); } /// /// Determines whether the URI is a base of the URI. /// /// The left part. /// The right part. /// if is base of /// , otherwise. /// or /// is . /// is not an absolute URI. public static bool IsBaseOf(Uri left, Uri right) { ArgumentNullException.ThrowIfNull(left); ArgumentNullException.ThrowIfNull(right); if (left is not { IsAbsoluteUri: true }) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left)); } // Ensure the left part ends with a trailing slash, as it is necessary // for Uri's constructor to include the last path segment in the base URI. left = left.AbsolutePath switch { null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri, [.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri, ['/'] or _ => left }; return left.IsBaseOf(right); } /// /// Determines whether the specified represents an implicit file URI. /// /// The URI. /// /// if represents /// an implicit file URI, otherwise. /// /// is . public static bool IsImplicitFileUri(Uri uri) { ArgumentNullException.ThrowIfNull(uri); return uri.IsAbsoluteUri && uri.IsFile && !uri.OriginalString.StartsWith(uri.Scheme, StringComparison.OrdinalIgnoreCase); } /// /// Adds a query string parameter to the specified . /// /// The URI to which the query string parameter will be appended. /// The name of the query string parameter to append. /// The value of the query string parameter to append. /// The final instance, with the specified parameter appended. public static Uri AddQueryStringParameter(Uri uri, string name, string? value) { ArgumentNullException.ThrowIfNull(uri); var builder = new StringBuilder(uri.Query); if (builder.Length is > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(name)); if (!string.IsNullOrEmpty(value)) { builder.Append('='); builder.Append(Uri.EscapeDataString(value)); } return new UriBuilder(uri) { Query = builder.ToString() }.Uri; } /// /// Adds query string parameters to the specified . /// /// The URI to which the query string parameters will be appended. /// The query string parameters to append. /// The final instance, with the specified parameters appended. /// is . /// is . public static Uri AddQueryStringParameters(Uri uri, IReadOnlyDictionary parameters) { ArgumentNullException.ThrowIfNull(uri); ArgumentNullException.ThrowIfNull(parameters); if (parameters.Count is 0) { return uri; } var builder = new StringBuilder(uri.Query); foreach (var parameter in parameters) { // If the parameter doesn't include any string value, // only append the parameter key to the query string. if (parameter.Value.Count is 0) { if (builder.Length is > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(parameter.Key)); } // Otherwise, iterate the string values and create // a new "name=value" pair for each iterated value. else { foreach (var value in parameter.Value) { if (builder.Length is > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(parameter.Key)); if (!string.IsNullOrEmpty(value)) { builder.Append('='); builder.Append(Uri.EscapeDataString(value)); } } } } return new UriBuilder(uri) { Query = builder.ToString() }.Uri; } /// /// Extracts the parameters from the specified query string. /// /// The query string, which may start with a '?'. /// The parameters extracted from the specified query string. /// is . public static IReadOnlyDictionary ParseQuery(string query) { ArgumentNullException.ThrowIfNull(query); return query.TrimStart(Separators.QuestionMark[0]) .Split([Separators.Ampersand[0], Separators.Semicolon[0]], StringSplitOptions.RemoveEmptyEntries) .Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries)) .Select(static parts => ( Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null, Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null)) .Where(static pair => !string.IsNullOrEmpty(pair.Key)) .GroupBy(static pair => pair.Key) .ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)])); } /// /// Extracts the parameters from the specified fragment. /// /// The fragment string, which may start with a '#'. /// The parameters extracted from the specified fragment. /// is . public static IReadOnlyDictionary ParseFragment(string fragment) { ArgumentNullException.ThrowIfNull(fragment); return fragment.TrimStart(Separators.Hash[0]) .Split([Separators.Ampersand[0], Separators.Semicolon[0]], StringSplitOptions.RemoveEmptyEntries) .Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries)) .Select(static parts => ( Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null, Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null)) .Where(static pair => !string.IsNullOrEmpty(pair.Key)) .GroupBy(static pair => pair.Key) .ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)])); } /// /// Extracts the parameters from the specified stream. /// /// The stream containing the formurl-encoded data. /// The encoding used to decode the data. /// The that can be used to abort the operation. /// The parameters extracted from the specified stream. /// is . public static async ValueTask> ParseFormAsync( Stream stream, Encoding encoding, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(encoding); var reader = new FormReader(stream, encoding); return await reader.ReadFormAsync(cancellationToken); } #if SUPPORTS_ECDSA /// /// Creates a new key. /// /// A new key. /// /// The implementation resolved from is not valid. /// public static ECDsa CreateEcdsaKey() { return GetAlgorithmFromConfig() switch { ECDsa result => result, null => ECDsa.Create(), var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict ECDSA Cryptographic Provider"); } /// /// Creates a new key. /// /// The EC curve to use to create the key. /// A new key. /// /// The implementation resolved from is not valid. /// public static ECDsa CreateEcdsaKey(ECCurve curve) { var algorithm = GetAlgorithmFromConfig() switch { ECDsa result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // If no custom algorithm was registered, use either the static Create() API // on platforms that support it or create a default instance provided by the BCL. if (algorithm is null) { return ECDsa.Create(curve); } try { algorithm.GenerateKey(curve); } catch { algorithm.Dispose(); throw; } return algorithm; [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict ECDSA Cryptographic Provider"); } #endif /// /// Creates a new key. /// /// The key size to use to create the key. /// A new key. /// /// The implementation resolved from is not valid. /// public static RSA CreateRsaKey(int size) { var algorithm = GetAlgorithmFromConfig() switch { RSA result => result, #if SUPPORTS_RSA_KEY_CREATION_WITH_SPECIFIED_SIZE // Note: on .NET Framework >= 4.7.2, the new RSA.Create(int keySizeInBits) uses // CryptoConfig.CreateFromName("RSAPSS") internally, which returns by default // a RSACng instance instead of a RSACryptoServiceProvider based on CryptoAPI. null => RSA.Create(size), #else // Note: while a RSACng object could be manually instantiated and returned on // .NET Framework < 4.7.2, the static RSA.Create() factory (which returns a // RSACryptoServiceProvider instance by default) is always preferred to RSACng // as this type is known to have compatibility issues on .NET Framework < 4.6.2. // // Developers who prefer using a CNG-based implementation on .NET Framework 4.6.1 // can do so by tweaking machine.config or by using CryptoConfig.AddAlgorithm(). null => RSA.Create(), #endif var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // Note: on .NET Framework, the RSA.Create() overload uses CryptoConfig.CreateFromName() // and always returns a RSACryptoServiceProvider instance unless the default name mapping was // explicitly overridden in machine.config or via CryptoConfig.AddAlgorithm(). Unfortunately, // RSACryptoServiceProvider still uses 1024-bit keys by default and doesn't support changing // the key size via RSACryptoServiceProvider.KeySize (setting it has no effect on the object). // // To ensure the key size matches the requested size, this method replaces the instance by a // new RSACryptoServiceProvider using the constructor allowing to override the default key size. try { if (algorithm.KeySize != size) { if (algorithm is RSACryptoServiceProvider) { algorithm.Dispose(); algorithm = new RSACryptoServiceProvider(size); } else { algorithm.KeySize = size; } if (algorithm.KeySize != size) { throw new CryptographicException(SR.FormatID0059(algorithm.GetType().FullName)); } } } catch { algorithm.Dispose(); throw; } return algorithm; [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict RSA Cryptographic Provider"); } /// /// Computes the SHA-256 message authentication code (HMAC) of the specified array. /// /// The cryptographic key. /// The data to hash. /// The SHA-256 message authentication code (HMAC) of the specified array. /// /// The implementation resolved from is not valid. /// public static byte[] ComputeSha256MessageAuthenticationCode(byte[] key, byte[] data) { var algorithm = GetAlgorithmFromConfig(key) switch { HMACSHA256 result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // If no custom algorithm was registered, use either the static/one-shot HashData() API // on platforms that support it or create a default instance provided by the BCL. if (algorithm is null) { #if SUPPORTS_ONE_SHOT_HASHING_METHODS return HMACSHA256.HashData(key, data); #else algorithm = new HMACSHA256(key); #endif } try { return algorithm.ComputeHash(data); } finally { algorithm.Dispose(); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig(byte[] key) => CryptoConfig.CreateFromName("OpenIddict HMAC SHA-256 Cryptographic Provider", [key]); } /// /// Computes the SHA-256 hash of the specified array. /// /// The data to hash. /// The SHA-256 hash of the specified array. /// /// The implementation resolved from is not valid. /// public static byte[] ComputeSha256Hash(byte[] data) { var algorithm = GetAlgorithmFromConfig() switch { SHA256 result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // If no custom algorithm was registered, use either the static/one-shot HashData() API // on platforms that support it or create a default instance provided by the BCL. if (algorithm is null) { #if SUPPORTS_ONE_SHOT_HASHING_METHODS return SHA256.HashData(data); #else algorithm = SHA256.Create(); #endif } try { return algorithm.ComputeHash(data); } finally { algorithm.Dispose(); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict SHA-256 Cryptographic Provider"); } /// /// Computes the SHA-384 hash of the specified array. /// /// The data to hash. /// The SHA-384 hash of the specified array. /// /// The implementation resolved from is not valid. /// public static byte[] ComputeSha384Hash(byte[] data) { var algorithm = GetAlgorithmFromConfig() switch { SHA384 result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // If no custom algorithm was registered, use either the static/one-shot HashData() API // on platforms that support it or create a default instance provided by the BCL. if (algorithm is null) { #if SUPPORTS_ONE_SHOT_HASHING_METHODS return SHA384.HashData(data); #else algorithm = SHA384.Create(); #endif } try { return algorithm.ComputeHash(data); } finally { algorithm.Dispose(); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict SHA-384 Cryptographic Provider"); } /// /// Computes the SHA-512 hash of the specified array. /// /// The data to hash. /// The SHA-512 hash of the specified array. /// /// The implementation resolved from is not valid. /// public static byte[] ComputeSha512Hash(byte[] data) { var algorithm = GetAlgorithmFromConfig() switch { SHA512 result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // If no custom algorithm was registered, use either the static/one-shot HashData() API // on platforms that support it or create a default instance provided by the BCL. if (algorithm is null) { #if SUPPORTS_ONE_SHOT_HASHING_METHODS return SHA512.HashData(data); #else algorithm = SHA512.Create(); #endif } try { return algorithm.ComputeHash(data); } finally { algorithm.Dispose(); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict SHA-512 Cryptographic Provider"); } /// /// Creates a new array of containing random data. /// /// The desired entropy, in bits. /// A new array of containing random data. /// /// The implementation resolved from is not valid. /// public static byte[] CreateRandomArray(int size) { var algorithm = GetAlgorithmFromConfig() switch { RandomNumberGenerator result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // If no custom random number generator was registered, use either the static GetBytes() or // Fill() APIs on platforms that support them or create a default instance provided by the BCL. #if SUPPORTS_ONE_SHOT_RANDOM_NUMBER_GENERATOR_METHODS if (algorithm is null) { var array = new byte[size / 8]; algorithm.GetBytes(array); return array; } #endif var array = new byte[size / 8]; #if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS if (algorithm is null) { RandomNumberGenerator.Fill(array); return array; } #else algorithm ??= RandomNumberGenerator.Create(); #endif try { algorithm.GetBytes(array); } finally { algorithm.Dispose(); } return array; [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict RNG Cryptographic Provider"); } /// /// Creates a new containing characters /// randomly selected in the specified . /// /// The characters allowed to be included in the . /// The number of characters. /// A new containing random data. /// /// The implementation resolved from is not valid. /// public static string CreateRandomString(ReadOnlySpan charset, int count) { var algorithm = GetAlgorithmFromConfig() switch { RandomNumberGenerator result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; try { var builder = new StringBuilder(); for (var index = 0; index < count; index++) { // Pick a character in the specified charset by generating a random index. builder.Append(charset[index: algorithm switch { #if SUPPORTS_INT32_RANDOM_NUMBER_GENERATOR_METHODS // If no custom random number generator was registered, use // the static GetInt32() API on platforms that support it. null => RandomNumberGenerator.GetInt32(0, charset.Length), #endif // Otherwise, create a default implementation if necessary // and use the local function that achieves the same result. _ => GetInt32(algorithm ??= RandomNumberGenerator.Create(), 0..charset.Length) }]); } return builder.ToString(); } finally { algorithm?.Dispose(); } static int GetInt32(RandomNumberGenerator algorithm, Range range) { // Note: the logic used here is directly taken from the official implementation // of the RandomNumberGenerator.GetInt32() method introduced in .NET Core 3.0. // // See https://github.com/dotnet/corefx/pull/31243 for more information. var count = (uint) range.End.Value - (uint) range.Start.Value - 1; if (count is 0) { return range.Start.Value; } var mask = count; mask |= mask >> 1; mask |= mask >> 2; mask |= mask >> 4; mask |= mask >> 8; mask |= mask >> 16; var buffer = new byte[sizeof(uint)]; uint value; do { algorithm.GetBytes(buffer); value = mask & BitConverter.ToUInt32(buffer, 0); } while (value > count); return (int) value + range.Start.Value; } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig() => CryptoConfig.CreateFromName("OpenIddict RNG Cryptographic Provider"); } /// /// Determines 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. /// /// if and have the same values /// for and the same contents, otherwise. /// #if !SUPPORTS_TIME_CONSTANT_COMPARISONS [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] #endif public static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) { #if SUPPORTS_TIME_CONSTANT_COMPARISONS return CryptographicOperations.FixedTimeEquals(left, right); #else // 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 } /// /// Removes the characters that are not part of /// from the specified string. /// /// /// Note: if no character is present in , all characters are considered valid. /// /// The original string. /// The list of allowed characters. /// The original string with the disallowed characters removed. /// is . public static string? RemoveDisallowedCharacters(string? value, IReadOnlyCollection charset) { ArgumentNullException.ThrowIfNull(charset); if (charset.Count is 0 || string.IsNullOrEmpty(value)) { return value; } var builder = new StringBuilder(); var enumerator = StringInfo.GetTextElementEnumerator(value); while (enumerator.MoveNext()) { var element = enumerator.GetTextElement(); if (charset.Contains(element)) { builder.Append(element); } } return builder.ToString(); } #if SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM /// /// Creates a derived key based on the specified using PBKDF2. /// /// The secret from which the derived key is created. /// The salt. /// The hash algorithm to use. /// The number of iterations to use. /// The desired length of the derived key. /// A derived key based on the specified . /// /// The implementation resolved from is not valid. /// public static byte[] DeriveKey(string secret, byte[] salt, HashAlgorithmName algorithm, int iterations, int length) { // Warning: the type and order of the arguments specified here MUST exactly match the parameters used with // Rfc2898DeriveBytes(string password, byte[] salt, int iterations, HashAlgorithmName hashAlgorithm). var generator = GetAlgorithmFromConfig(secret, salt, iterations, algorithm) switch { Rfc2898DeriveBytes result => result, null => null, var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) }; // If no custom generator was registered, use either the static/one-shot Pbkdf2() API // on platforms that support it or create an instance using the dedicated constructor. if (generator is null) { #if SUPPORTS_ONE_SHOT_KEY_DERIVATION_METHODS return Rfc2898DeriveBytes.Pbkdf2(secret, salt, iterations, algorithm, length); #else #pragma warning disable CA5379 generator = new Rfc2898DeriveBytes(secret, salt, iterations, algorithm); #pragma warning restore CA5379 #endif } try { return generator.GetBytes(length); } finally { generator.Dispose(); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The default implementation is always used when no custom algorithm was registered.")] static object? GetAlgorithmFromConfig(string secret, byte[] salt, int iterations, HashAlgorithmName algorithm) => CryptoConfig.CreateFromName("OpenIddict PBKDF2 Cryptographic Provider", [secret, salt, iterations, algorithm]); } #endif #if SUPPORTS_ECDSA /// /// Determines whether the specified represent a specific EC curve. /// /// The . /// The . /// /// if is identical to /// the specified , otherwise. /// public static bool IsEcCurve(ECParameters parameters, ECCurve curve) { Debug.Assert(parameters.Curve.Oid is not null, SR.GetResourceString(SR.ID4011)); Debug.Assert(curve.Oid is not null, SR.GetResourceString(SR.ID4011)); // Warning: on .NET Framework 4.x and .NET Core 2.1, exported ECParameters generally have // a null OID value attached. To work around this limitation, both the raw OID values and // the friendly names are compared to determine whether the curve is of the specified type. if (!string.IsNullOrEmpty(parameters.Curve.Oid.Value) && !string.IsNullOrEmpty(curve.Oid.Value)) { return string.Equals(parameters.Curve.Oid.Value, curve.Oid.Value, StringComparison.Ordinal); } if (!string.IsNullOrEmpty(parameters.Curve.Oid.FriendlyName) && !string.IsNullOrEmpty(curve.Oid.FriendlyName)) { return string.Equals(parameters.Curve.Oid.FriendlyName, curve.Oid.FriendlyName, StringComparison.Ordinal); } Debug.Fail(SR.GetResourceString(SR.ID4012)); return false; } #endif /// /// Determines whether the specified represents a null, undefined or empty JSON node. /// /// The . /// /// if the JSON node is null, undefined or empty otherwise. /// public static bool IsNullOrEmpty(JsonElement element) { switch (element.ValueKind) { case JsonValueKind.Undefined or JsonValueKind.Null: return true; case JsonValueKind.String: return string.IsNullOrEmpty(element.GetString()); case JsonValueKind.Array: return element.GetArrayLength() is 0; case JsonValueKind.Object: #if SUPPORTS_JSON_ELEMENT_PROPERTY_COUNT return element.GetPropertyCount() is 0; #else using (var enumerator = element.EnumerateObject()) { return !enumerator.MoveNext(); } #endif default: return false; } } /// /// Determines whether the specified represents a null or empty JSON node. /// /// The . /// /// if the JSON node is null or empty, otherwise. /// public static bool IsNullOrEmpty([NotNullWhen(false)] JsonNode? node) => node switch { null => true, JsonArray value => value.Count is 0, JsonObject value => value.Count is 0, JsonValue value when value.TryGetValue(out string? result) => string.IsNullOrEmpty(result), JsonValue value when value.TryGetValue(out JsonElement element) => IsNullOrEmpty(element), // If the JSON node cannot be mapped to a primitive type, convert it to // a JsonElement instance and infer the corresponding claim value type. JsonNode value => IsNullOrEmpty(value.Deserialize(OpenIddictSerializer.Default.JsonElement)) }; /// /// Determines whether the specified is a certificate authority. /// /// The . /// /// if the certificate is a certificate authority, otherwise. /// public static bool IsCertificateAuthority(X509Certificate2 certificate) { ArgumentNullException.ThrowIfNull(certificate); return certificate.Extensions.OfType() .Any(static extension => extension.CertificateAuthority); } /// /// Determines whether the specified has the specified extended key usage. /// /// The . /// The extended key usage. /// /// if the certificate has the specified extended key usage, otherwise. /// public static bool HasExtendedKeyUsage(X509Certificate2 certificate, string usage) { for (var index = 0; index < certificate.Extensions.Count; index++) { if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension && HasOid(extension.EnhancedKeyUsages, usage)) { return true; } } return false; static bool HasOid(OidCollection collection, string value) { for (var index = 0; index < collection.Count; index++) { if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal)) { return true; } } return false; } } /// /// Determines whether the specified has the specified key usage. /// /// The . /// The . /// /// if the certificate has the specified key usage, otherwise. /// public static bool HasKeyUsage(X509Certificate2 certificate, X509KeyUsageFlags usage) { ArgumentNullException.ThrowIfNull(certificate); for (var index = 0; index < certificate.Extensions.Count; index++) { if (certificate.Extensions[index] is X509KeyUsageExtension extension && extension.KeyUsages.HasFlag(usage)) { return true; } } return false; } /// /// Determines whether the specified is self-issued. /// /// The . /// /// if the certificate is self-issued, otherwise. /// public static bool IsSelfIssuedCertificate(X509Certificate2 certificate) { ArgumentNullException.ThrowIfNull(certificate); return certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData); } /// /// Determines whether the items contained in /// are of the specified . /// /// The . /// The expected . /// /// if the array doesn't contain any value or if all the items /// are of the specified , otherwise. /// public static bool ValidateArrayElements(JsonElement element, JsonValueKind kind) { if (element.ValueKind is not JsonValueKind.Array) { throw new ArgumentOutOfRangeException(nameof(element)); } foreach (var item in element.EnumerateArray()) { if (item.ValueKind != kind) { return false; } } return true; } /// /// Determines whether the items contained in /// are of the specified . /// /// The . /// The expected . /// /// if the object doesn't contain any value or if all the items /// are of the specified , otherwise. /// public static bool ValidateObjectElements(JsonElement element, JsonValueKind kind) { if (element.ValueKind is not JsonValueKind.Object) { throw new ArgumentOutOfRangeException(nameof(element)); } foreach (var property in element.EnumerateObject()) { if (property.Value.ValueKind != kind) { return false; } } return true; } /// /// Note: this implementation was taken from ASP.NET Core. /// private class FormReader { public const int DefaultValueCountLimit = 1024; public const int DefaultKeyLengthLimit = 1024 * 2; public const int DefaultValueLengthLimit = 1024 * 1024 * 4; private readonly TextReader _reader; private readonly char[] _buffer; private readonly StringBuilder _builder = new(); private int _bufferOffset; private int _bufferCount; private string? _currentKey; private string? _currentValue; private bool _endOfStream; public FormReader(Stream stream, Encoding encoding) { _buffer = new char[8192]; _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); } public int ValueCountLimit { get; set; } = DefaultValueCountLimit; public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; public KeyValuePair? ReadNextPair() { ReadNextPairImpl(); if (ReadSucceeded()) { return KeyValuePair.Create(_currentKey, _currentValue); } return null; } private void ReadNextPairImpl() { StartReadNextPair(); while (!_endOfStream) { // Empty if (_bufferCount == 0) { Buffer(); } if (TryReadNextPair()) { break; } } } public async Task?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) { await ReadNextPairAsyncImpl(cancellationToken); if (ReadSucceeded()) { return KeyValuePair.Create(_currentKey, _currentValue); } return null; } private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken()) { StartReadNextPair(); while (!_endOfStream) { if (_bufferCount == 0) { await BufferAsync(cancellationToken); } if (TryReadNextPair()) { break; } } } private void StartReadNextPair() { _currentKey = null; _currentValue = null; } private bool TryReadNextPair() { if (_currentKey == null) { if (!TryReadWord('=', KeyLengthLimit, out _currentKey)) { return false; } if (_bufferCount == 0) { return false; } } if (_currentValue == null) { if (!TryReadWord('&', ValueLengthLimit, out _currentValue)) { return false; } } return true; } private bool TryReadWord(char separator, int limit, [NotNullWhen(true)] out string? value) { do { if (ReadChar(separator, limit, out value)) { return true; } } while (_bufferCount > 0); return false; } private bool ReadChar(char separator, int limit, [NotNullWhen(true)] out string? word) { if (_bufferCount == 0) { word = BuildWord(); return true; } var c = _buffer[_bufferOffset++]; _bufferCount--; if (c == separator) { word = BuildWord(); return true; } if (_builder.Length >= limit) { throw new InvalidDataException($"Form key or value length limit {limit} exceeded."); } _builder.Append(c); word = null; return false; } private string BuildWord() { _builder.Replace('+', ' '); var result = _builder.ToString(); _builder.Clear(); return Uri.UnescapeDataString(result); } private void Buffer() { _bufferOffset = 0; _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); _endOfStream = _bufferCount == 0; } private async Task BufferAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); _bufferOffset = 0; _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); _endOfStream = _bufferCount == 0; } public Dictionary ReadForm() { var accumulator = new KeyValueAccumulator(); while (!_endOfStream) { ReadNextPairImpl(); Append(ref accumulator); } return accumulator.GetResults(); } public async Task> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()) { var accumulator = new KeyValueAccumulator(); while (!_endOfStream) { await ReadNextPairAsyncImpl(cancellationToken); Append(ref accumulator); } return accumulator.GetResults(); } [MemberNotNullWhen(true, nameof(_currentKey), nameof(_currentValue))] private bool ReadSucceeded() { return _currentKey != null && _currentValue != null; } private void Append(ref KeyValueAccumulator accumulator) { if (ReadSucceeded()) { accumulator.Append(_currentKey, _currentValue); if (accumulator.ValueCount > ValueCountLimit) { throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); } } } } /// /// Note: this implementation was taken from ASP.NET Core. /// private struct KeyValueAccumulator { private Dictionary _accumulator; private Dictionary> _expandingAccumulator; public void Append(string key, string value) { if (_accumulator == null) { _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); } StringValues values; if (_accumulator.TryGetValue(key, out values)) { if (values.Count == 0) { _expandingAccumulator[key].Add(value); } else if (values.Count == 1) { _accumulator[key] = new string[] { values[0]!, value }; } else { _accumulator[key] = default(StringValues); if (_expandingAccumulator == null) { _expandingAccumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); } var list = new List(8); var array = values.ToArray(); list.Add(array[0]!); list.Add(array[1]!); list.Add(value); _expandingAccumulator[key] = list; } } else { _accumulator[key] = new StringValues(value); } ValueCount++; } public bool HasValues => ValueCount > 0; public int KeyCount => _accumulator?.Count ?? 0; public int ValueCount { get; private set; } public Dictionary GetResults() { if (_expandingAccumulator != null) { foreach (var entry in _expandingAccumulator) { _accumulator[entry.Key] = new StringValues([.. entry.Value]); } } return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); } } }