using System.Collections.ObjectModel;
using System.Data;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace OpenIddict.Extensions;
///
/// Exposes common helpers used by the OpenIddict assemblies.
///
internal static class OpenIddictHelpers
{
///
/// Generates a sequence of non-overlapping adjacent buffers over the source sequence.
///
/// The source sequence element type.
/// The source sequence.
/// The number of elements for allocated buffers.
/// A sequence of buffers containing source sequence elements.
public static IEnumerable> Buffer(this IEnumerable source, int count)
{
List? buffer = null;
foreach (var element in source)
{
buffer ??= [];
buffer.Add(element);
if (buffer.Count == count)
{
yield return buffer;
buffer = null;
}
}
if (buffer is not null)
{
yield return buffer;
}
}
///
/// Finds the first base type that matches the specified generic type definition.
///
/// The type to introspect.
/// The generic type definition.
/// A instance if the base type was found, otherwise.
public static Type? FindGenericBaseType(Type type, Type definition)
=> FindGenericBaseTypes(type, definition).FirstOrDefault();
///
/// Finds all the base types that matches the specified generic type definition.
///
/// The type to introspect.
/// The generic type definition.
/// A instance if the base type was found, otherwise.
public static IEnumerable FindGenericBaseTypes(Type type, Type definition)
{
if (type is null)
{
throw new ArgumentNullException(nameof(type));
}
if (definition is null)
{
throw new ArgumentNullException(nameof(definition));
}
if (!definition.IsGenericTypeDefinition)
{
throw new ArgumentException(SR.GetResourceString(SR.ID0263), nameof(definition));
}
if (definition.IsInterface)
{
foreach (var contract in type.GetInterfaces())
{
if (!contract.IsGenericType && !contract.IsConstructedGenericType)
{
continue;
}
if (contract.GetGenericTypeDefinition() == definition)
{
yield return contract;
}
}
}
else
{
for (var candidate = type; candidate is not null; candidate = candidate.BaseType)
{
if (!candidate.IsGenericType && !candidate.IsConstructedGenericType)
{
continue;
}
if (candidate.GetGenericTypeDefinition() == definition)
{
yield return candidate;
}
}
}
}
#if !SUPPORTS_TASK_WAIT_ASYNC
///
/// Waits until the specified task returns a result or the cancellation token is signaled.
///
/// The task.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
/// The specified is signaled.
public static async Task WaitAsync(this Task task, CancellationToken cancellationToken)
{
var source = new TaskCompletionSource(TaskCreationOptions.None);
using (cancellationToken.Register(static state => ((TaskCompletionSource) state!).SetResult(true), source))
{
if (await Task.WhenAny(task, source.Task) == source.Task)
{
throw new OperationCanceledException(cancellationToken);
}
await task;
}
}
///
/// Waits until the specified task returns a result or the cancellation token is signaled.
///
/// The task.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
/// The specified is signaled.
public static async Task WaitAsync(this Task task, CancellationToken cancellationToken)
{
var source = new TaskCompletionSource(TaskCreationOptions.None);
using (cancellationToken.Register(static state => ((TaskCompletionSource) state!).SetResult(true), source))
{
if (await Task.WhenAny(task, source.Task) == source.Task)
{
throw new OperationCanceledException(cancellationToken);
}
return await task;
}
}
#endif
///
/// 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;
}
}
#if !SUPPORTS_TOHASHSET_LINQ_EXTENSION
///
/// Creates a new instance and imports the elements present in the specified source.
///
/// The type of the elements present in the collection.
/// The source collection.
/// The comparer to use.
/// A new instance and imports the elements present in the specified source.
/// The is .
public static HashSet ToHashSet(this IEnumerable source, IEqualityComparer? comparer)
=> new(source ?? throw new ArgumentNullException(nameof(source)), comparer);
#endif
///
/// 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)
{
if (left is null)
{
throw new ArgumentNullException(nameof(left));
}
if (right is null)
{
throw new ArgumentNullException(nameof(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);
}
///
/// 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)
{
if (uri is null)
{
throw new ArgumentNullException(nameof(uri));
}
var builder = new StringBuilder(uri.Query);
if (builder.Length > 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)
{
if (uri is null)
{
throw new ArgumentNullException(nameof(uri));
}
if (parameters is null)
{
throw new ArgumentNullException(nameof(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 > 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 > 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)
{
if (query is null)
{
throw new ArgumentNullException(nameof(query));
}
return query.TrimStart(Separators.QuestionMark[0])
.Split(new[] { Separators.Ampersand[0], Separators.Semicolon[0] }, StringSplitOptions.RemoveEmptyEntries)
.Select(parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(pair => pair.Key)
.ToDictionary(pair => pair.Key!, pair => new StringValues(pair.Select(parts => parts.Value).ToArray()));
}
#if SUPPORTS_ECDSA
///
/// Creates a new key.
///
/// A new key.
///
/// The implementation resolved from is not valid.
///
public static ECDsa CreateEcdsaKey()
=> CryptoConfig.CreateFromName("OpenIddict ECDSA Cryptographic Provider") switch
{
ECDsa result => result,
null => ECDsa.Create(),
var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName))
};
///
/// 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 = CryptoConfig.CreateFromName("OpenIddict ECDSA Cryptographic Provider") 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;
}
#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 = CryptoConfig.CreateFromName("OpenIddict RSA Cryptographic Provider") 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;
}
///
/// 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 = CryptoConfig.CreateFromName("OpenIddict HMAC SHA-256 Cryptographic Provider", [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();
}
}
///
/// 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 = CryptoConfig.CreateFromName("OpenIddict SHA-256 Cryptographic Provider") 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();
}
}
///
/// 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 = CryptoConfig.CreateFromName("OpenIddict SHA-384 Cryptographic Provider") 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();
}
}
///
/// 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 = CryptoConfig.CreateFromName("OpenIddict SHA-512 Cryptographic Provider") 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();
}
}
///
/// 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 = CryptoConfig.CreateFromName("OpenIddict RNG Cryptographic Provider") 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)
{
return RandomNumberGenerator.GetBytes(size / 8);
}
#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;
}
///
/// Creates a new containing characters
/// randomly selected in the specified .
///
/// The characters allowed to be included in the .
/// The desired length of the .
/// A new containing random data.
///
/// The implementation resolved from is not valid.
///
public static string CreateRandomString(ReadOnlySpan charset, int length)
{
var algorithm = CryptoConfig.CreateFromName("OpenIddict RNG Cryptographic Provider") switch
{
RandomNumberGenerator result => result,
null => null,
var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName))
};
try
{
var buffer = new char[length];
for (var index = 0; index < buffer.Length; index++)
{
// Pick a character in the specified charset by generating a random index.
buffer[index] = charset[index: algorithm switch
{
#if SUPPORTS_INTEGER32_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 new string(buffer);
}
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;
}
}
///
/// 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
}
///
/// Converts the specified hex-encoded to a byte array.
///
/// The hexadecimal string.
/// The byte array.
public static byte[] ConvertFromHexadecimalString(string value)
{
#if SUPPORTS_HEXADECIMAL_STRING_CONVERSION
return Convert.FromHexString(value);
#else
if ((uint) value.Length % 2 is not 0)
{
throw new FormatException(SR.GetResourceString(SR.ID0413));
}
var array = new byte[value.Length / 2];
for (var index = 0; index < value.Length; index += 2)
{
array[index / 2] = Convert.ToByte(value.Substring(index, 2), 16);
}
return array;
#endif
}
#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).
using var generator = CryptoConfig.CreateFromName("OpenIddict PBKDF2 Cryptographic Provider",
args: [secret, salt, iterations, algorithm]) switch
{
Rfc2898DeriveBytes result => result,
#pragma warning disable CA5379
null => new Rfc2898DeriveBytes(secret, salt, iterations, algorithm),
#pragma warning restore CA5379
var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName))
};
return generator.GetBytes(length);
}
#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
}