Abp vNext Secret
使用Abp vNext 6.0
abp大概有两个secret,AbpUsers
和OpenIddictApplications
AbpUsers
abp的用户管理IdentityUserManager
其实是直接套的aspnetcore的UserManager
,继承完就没怎么改了,所以看源码要看aspnetcore的源码
我大概调试到最底下是NetCorePbkdf2Provider
这个地方的HMACSHA256
internal sealed class NetCorePbkdf2Provider : IPbkdf2Provider
{
public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
{
Debug.Assert(password != null);
Debug.Assert(salt != null);
Debug.Assert(iterationCount > 0);
Debug.Assert(numBytesRequested > 0);
HashAlgorithmName algorithmName;
switch (prf)
{
case KeyDerivationPrf.HMACSHA1:
algorithmName = HashAlgorithmName.SHA1;
break;
case KeyDerivationPrf.HMACSHA256:
algorithmName = HashAlgorithmName.SHA256;
break;
case KeyDerivationPrf.HMACSHA512:
algorithmName = HashAlgorithmName.SHA512;
break;
default:
throw new ArgumentOutOfRangeException(nameof(prf));
}
return Rfc2898DeriveBytes.Pbkdf2(password, salt, iterationCount, algorithmName, numBytesRequested);
}
}
Rfc2898DeriveBytes.Pbkdf2
的源码在runtime里
/// <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="ArgumentNullException">
/// <paramref name="password" /> or <paramref name="salt" /> is <see langword="null" />.
/// </exception>
/// <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>
/// <exception cref="EncoderFallbackException">
/// <paramref name="password" /> contains text that cannot be converted to UTF-8.
/// </exception>
/// <remarks>
/// The <paramref name="password" /> will be converted to bytes using the UTF-8 encoding. For
/// other encodings, convert the password string to bytes using the appropriate <see cref="System.Text.Encoding" />
/// and use <see cref="Pbkdf2(byte[], byte[], int, HashAlgorithmName, int)" />.
/// </remarks>
public static byte[] Pbkdf2(
string password,
byte[] salt,
int iterations,
HashAlgorithmName hashAlgorithm,
int outputLength)
{
ArgumentNullException.ThrowIfNull(password);
ArgumentNullException.ThrowIfNull(salt);
return Pbkdf2(password.AsSpan(), new ReadOnlySpan<byte>(salt), iterations, hashAlgorithm, outputLength);
}
/// <summary>
/// Creates a PBKDF2 derived key from password bytes.
/// </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="ArgumentNullException">
/// <paramref name="password" /> or <paramref name="salt" /> is <see langword="null" />.
/// </exception>
/// <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(
byte[] password,
byte[] salt,
int iterations,
HashAlgorithmName hashAlgorithm,
int outputLength)
{
ArgumentNullException.ThrowIfNull(password);
ArgumentNullException.ThrowIfNull(salt);
return Pbkdf2(new ReadOnlySpan<byte>(password), new ReadOnlySpan<byte>(salt), iterations, hashAlgorithm, outputLength);
}
/// <summary>
/// Creates a PBKDF2 derived key from password bytes.
/// </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<byte> password,
ReadOnlySpan<byte> salt,
int iterations,
HashAlgorithmName hashAlgorithm,
int outputLength)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(iterations);
ArgumentOutOfRangeException.ThrowIfNegative(outputLength);
ValidateHashAlgorithm(hashAlgorithm);
byte[] result = new byte[outputLength];
Pbkdf2Core(password, salt, result, iterations, hashAlgorithm);
return result;
}
private static void Pbkdf2Core(
ReadOnlySpan<char> password,
ReadOnlySpan<byte> salt,
Span<byte> destination,
int iterations,
HashAlgorithmName hashAlgorithm)
{
Debug.Assert(hashAlgorithm.Name is not null);
Debug.Assert(iterations > 0);
if (destination.IsEmpty)
{
return;
}
const int MaxPasswordStackSize = 256;
byte[]? rentedPasswordBuffer = null;
int maxEncodedSize = s_throwingUtf8Encoding.GetMaxByteCount(password.Length);
Span<byte> passwordBuffer = maxEncodedSize > MaxPasswordStackSize ?
(rentedPasswordBuffer = CryptoPool.Rent(maxEncodedSize)) :
stackalloc byte[MaxPasswordStackSize];
int passwordBytesWritten = s_throwingUtf8Encoding.GetBytes(password, passwordBuffer);
Span<byte> passwordBytes = passwordBuffer.Slice(0, passwordBytesWritten);
try
{
Pbkdf2Implementation.Fill(passwordBytes, salt, iterations, hashAlgorithm, destination);
}
finally
{
CryptographicOperations.ZeroMemory(passwordBytes);
}
if (rentedPasswordBuffer is not null)
{
CryptoPool.Return(rentedPasswordBuffer, clearSize: 0); // manually cleared above.
}
}
上面一串下来就是Pbkdf2Implementation
,这个在源码里面是根据操作系统来的,这个是windows的
public static unsafe void Fill(
ReadOnlySpan<byte> password,
ReadOnlySpan<byte> salt,
int iterations,
HashAlgorithmName hashAlgorithmName,
Span<byte> destination)
{
Debug.Assert(!destination.IsEmpty);
Debug.Assert(iterations >= 0);
Debug.Assert(hashAlgorithmName.Name is not null);
if (s_useKeyDerivation)
{
FillKeyDerivation(password, salt, iterations, hashAlgorithmName.Name, destination);
}
else
{
FillDeriveKeyPBKDF2(password, salt, iterations, hashAlgorithmName.Name, destination);
}
}
private static unsafe void FillKeyDerivation(
ReadOnlySpan<byte> password,
ReadOnlySpan<byte> salt,
int iterations,
string hashAlgorithmName,
Span<byte> destination)
{
SafeBCryptKeyHandle keyHandle;
int hashBlockSizeBytes = GetHashBlockSize(hashAlgorithmName);
// stackalloc 0 to let compiler know this cannot escape.
scoped Span<byte> clearSpan;
scoped ReadOnlySpan<byte> symmetricKeyMaterial;
int symmetricKeyMaterialLength;
if (password.IsEmpty)
{
// CNG won't accept a null pointer for the password.
symmetricKeyMaterial = stackalloc byte[1];
symmetricKeyMaterialLength = 0;
clearSpan = default;
}
else if (password.Length <= hashBlockSizeBytes)
{
// Password is small enough to use as-is.
symmetricKeyMaterial = password;
symmetricKeyMaterialLength = password.Length;
clearSpan = default;
}
else
{
// RFC 2104: "The key for HMAC can be of any length (keys longer than B bytes are
// first hashed using H).
// We denote by B the byte-length of such
// blocks (B=64 for all the above mentioned examples of hash functions)
//
// Windows' PBKDF2 will do this up to a point. To ensure we accept arbitrary inputs for
// PBKDF2, we do the hashing ourselves.
Span<byte> hashBuffer = stackalloc byte[512 / 8]; // 64 bytes is SHA512, the largest digest handled.
int hashBufferSize;
switch (hashAlgorithmName)
{
case HashAlgorithmNames.SHA1:
case HashAlgorithmNames.SHA256:
case HashAlgorithmNames.SHA384:
case HashAlgorithmNames.SHA512:
hashBufferSize = HashProviderDispenser.OneShotHashProvider.HashData(hashAlgorithmName, password, hashBuffer);
break;
case HashAlgorithmNames.SHA3_256:
case HashAlgorithmNames.SHA3_384:
case HashAlgorithmNames.SHA3_512:
if (!HashProviderDispenser.HashSupported(hashAlgorithmName))
{
throw new PlatformNotSupportedException();
}
hashBufferSize = HashProviderDispenser.OneShotHashProvider.HashData(hashAlgorithmName, password, hashBuffer);
break;
default:
Debug.Fail($"Unexpected hash algorithm '{hashAlgorithmName}'");
throw new CryptographicException();
}
clearSpan = hashBuffer.Slice(0, hashBufferSize);
symmetricKeyMaterial = clearSpan;
symmetricKeyMaterialLength = hashBufferSize;
}
Debug.Assert(symmetricKeyMaterial.Length > 0);
NTSTATUS generateKeyStatus;
if (Interop.BCrypt.PseudoHandlesSupported)
{
fixed (byte* pSymmetricKeyMaterial = symmetricKeyMaterial)
{
generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey(
(nuint)BCryptAlgPseudoHandle.BCRYPT_PBKDF2_ALG_HANDLE,
out keyHandle,
pbKeyObject: IntPtr.Zero,
cbKeyObject: 0,
pSymmetricKeyMaterial,
symmetricKeyMaterialLength,
dwFlags: 0);
}
}
else
{
if (s_pbkdf2AlgorithmHandle is null)
{
NTSTATUS openStatus = Interop.BCrypt.BCryptOpenAlgorithmProvider(
out SafeBCryptAlgorithmHandle pbkdf2AlgorithmHandle,
Internal.NativeCrypto.BCryptNative.AlgorithmName.Pbkdf2,
null,
BCryptOpenAlgorithmProviderFlags.None);
if (openStatus != NTSTATUS.STATUS_SUCCESS)
{
pbkdf2AlgorithmHandle.Dispose();
CryptographicOperations.ZeroMemory(clearSpan);
throw Interop.BCrypt.CreateCryptographicException(openStatus);
}
// This might race, and that's okay. Worst case the algorithm is opened
// more than once, and the ones that lost will get cleaned up during collection.
Interlocked.CompareExchange(ref s_pbkdf2AlgorithmHandle, pbkdf2AlgorithmHandle, null);
}
fixed (byte* pSymmetricKeyMaterial = symmetricKeyMaterial)
{
generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey(
s_pbkdf2AlgorithmHandle,
out keyHandle,
pbKeyObject: IntPtr.Zero,
cbKeyObject: 0,
pSymmetricKeyMaterial,
symmetricKeyMaterialLength,
dwFlags: 0);
}
}
CryptographicOperations.ZeroMemory(clearSpan);
if (generateKeyStatus != NTSTATUS.STATUS_SUCCESS)
{
keyHandle.Dispose();
throw Interop.BCrypt.CreateCryptographicException(generateKeyStatus);
}
Debug.Assert(!keyHandle.IsInvalid);
ulong kdfIterations = (ulong)iterations; // Previously asserted to be positive.
using (keyHandle)
fixed (char* pHashAlgorithmName = hashAlgorithmName)
fixed (byte* pSalt = salt)
fixed (byte* pDestination = destination)
{
Span<BCryptBuffer> buffers = stackalloc BCryptBuffer[3];
buffers[0].BufferType = CngBufferDescriptors.KDF_ITERATION_COUNT;
buffers[0].pvBuffer = (IntPtr)(&kdfIterations);
buffers[0].cbBuffer = sizeof(ulong);
buffers[1].BufferType = CngBufferDescriptors.KDF_SALT;
buffers[1].pvBuffer = (IntPtr)pSalt;
buffers[1].cbBuffer = salt.Length;
buffers[2].BufferType = CngBufferDescriptors.KDF_HASH_ALGORITHM;
buffers[2].pvBuffer = (IntPtr)pHashAlgorithmName;
// C# spec: "A char* value produced by fixing a string instance always points to a null-terminated string"
buffers[2].cbBuffer = checked((hashAlgorithmName.Length + 1) * sizeof(char)); // Add null terminator.
fixed (BCryptBuffer* pBuffers = buffers)
{
Interop.BCrypt.BCryptBufferDesc bufferDesc;
bufferDesc.ulVersion = Interop.BCrypt.BCRYPTBUFFER_VERSION;
bufferDesc.cBuffers = buffers.Length;
bufferDesc.pBuffers = (IntPtr)pBuffers;
NTSTATUS deriveStatus = Interop.BCrypt.BCryptKeyDerivation(
keyHandle,
&bufferDesc,
pDestination,
destination.Length,
out uint resultLength,
dwFlags: 0);
if (deriveStatus != NTSTATUS.STATUS_SUCCESS)
{
throw Interop.BCrypt.CreateCryptographicException(deriveStatus);
}
if (destination.Length != resultLength)
{
Debug.Fail("PBKDF2 resultLength != destination.Length");
throw new CryptographicException();
}
}
}
}
总的流程就是前端传密码给后端,后端再通过数据库保存的密文对明文进行hash转换,再用明文的hash比较数据库保存的密文
还真复杂,所以还是用.net或abp自带的函数来操作比较好,不过还是得封装一层,这样才比较符合使用习惯
OpenIddictApplications
根据abp的源码,只能知道是用内置IOpenIddictApplicationManager
来操作数据,hash操作估计也在里面,再根据AbpUsers
的看来,这个OpenIddictApplicationManager
估计就在OpenIddict的源码里
所以接下来的代码是OpenIddict的源码
根据OpenIddict官方所说,这个加密应该是PBKDF2 with HMAC-SHA256
,那就跟AbpUsers
是一样的了
https://github.com/openiddict/openiddict-core/issues/418
ClientSecret操作大概就在这俩函数里,CreateAsync
一看就是创建数据的,ObfuscateClientSecretAsync
函数是加密secret的
/// <summary>
/// Creates a new application.
/// Note: the default implementation automatically hashes the client
/// secret before storing it in the database, for security reasons.
/// </summary>
/// <param name="application">The application to create.</param>
/// <param name="secret">The client secret associated with the application, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async ValueTask CreateAsync(TApplication application, string? secret, CancellationToken cancellationToken = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (!string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken)))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0206), nameof(application));
}
// If no client type was specified, assume it's a confidential application if a secret was
// provided or a JSON Web Key Set was attached and contains at least one RSA/ECDSA signing key.
var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
if (!string.IsNullOrEmpty(secret))
{
await Store.SetClientTypeAsync(application, ClientTypes.Confidential, cancellationToken);
}
else
{
var set = await Store.GetJsonWebKeySetAsync(application, cancellationToken);
if (set is not null && set.Keys.Any(static key =>
key.Kty is JsonWebAlgorithmsKeyTypes.EllipticCurve or JsonWebAlgorithmsKeyTypes.RSA &&
key.Use is JsonWebKeyUseNames.Sig or null))
{
await Store.SetClientTypeAsync(application, ClientTypes.Confidential, cancellationToken);
}
else
{
await Store.SetClientTypeAsync(application, ClientTypes.Public, cancellationToken);
}
}
}
// If a client secret was provided, obfuscate it.
if (!string.IsNullOrEmpty(secret))
{
secret = await ObfuscateClientSecretAsync(secret, cancellationToken);
await Store.SetClientSecretAsync(application, secret, cancellationToken);
}
var results = await GetValidationResultsAsync(application, cancellationToken);
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0207));
builder.AppendLine();
foreach (var result in results)
{
builder.AppendLine(result.ErrorMessage);
}
throw new ValidationException(builder.ToString(), results);
}
await Store.CreateAsync(application, cancellationToken);
if (!Options.CurrentValue.DisableEntityCaching)
{
await Cache.AddAsync(application, cancellationToken);
}
async Task<ImmutableArray<ValidationResult>> GetValidationResultsAsync(
TApplication application, CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder<ValidationResult>();
await foreach (var result in ValidateAsync(application, cancellationToken))
{
builder.Add(result);
}
return builder.ToImmutable();
}
}
/// <summary>
/// Obfuscates the specified client secret so it can be safely stored in a database.
/// By default, this method returns a complex hashed representation computed using PBKDF2.
/// </summary>
/// <param name="secret">The client secret.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
protected virtual ValueTask<string> ObfuscateClientSecretAsync(string secret, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(secret))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0216), nameof(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 = OpenIddictHelpers.CreateRandomArray(size: 128);
var hash = HashSecret(secret, salt, HashAlgorithmName.SHA256, iterations: 10_000, length: 256 / 8);
return new(Convert.ToBase64String(hash));
// 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 byte[] HashSecret(string secret, byte[] salt, HashAlgorithmName algorithm, int iterations, int length)
{
var key = DeriveKey(secret, salt, algorithm, iterations, length);
var payload = new byte[13 + salt.Length + key.Length];
// Write the format marker.
payload[0] = 0x01;
// Write the hashing algorithm version.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), algorithm switch
{
var name when name == HashAlgorithmName.SHA1 => 0,
var name when name == HashAlgorithmName.SHA256 => 1,
var name when name == HashAlgorithmName.SHA512 => 2,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217))
});
// Write the iteration count of the algorithm.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(5, sizeof(uint)), (uint) iterations);
// Write the size of the salt.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(9, sizeof(uint)), (uint) salt.Length);
// Write the salt.
salt.CopyTo(payload.AsSpan(13));
// Write the subkey.
key.CopyTo(payload.AsSpan(13 + salt.Length));
return payload;
}
}
至于这个secret,有一个验证函数VerifyHashedSecret
,这个在OpenIddictServerHandlers
有用到,在OpenIddictApplicationManager
里面,似乎是在GrantType之前验证application的
/// <summary>
/// Validates the client_secret associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The secret that should be compared to the client_secret stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
/// </returns>
public virtual async ValueTask<bool> ValidateClientSecretAsync(
TApplication application, string secret, CancellationToken cancellationToken = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(secret))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0216), nameof(secret));
}
if (await HasClientTypeAsync(application, ClientTypes.Public, cancellationToken))
{
Logger.LogWarning(SR.GetResourceString(SR.ID6159));
return false;
}
var value = await Store.GetClientSecretAsync(application, cancellationToken);
if (string.IsNullOrEmpty(value))
{
Logger.LogInformation(SR.GetResourceString(SR.ID6160), await GetClientIdAsync(application, cancellationToken));
return false;
}
if (!await ValidateClientSecretAsync(secret, value, cancellationToken))
{
Logger.LogInformation(SR.GetResourceString(SR.ID6161), await GetClientIdAsync(application, cancellationToken));
return false;
}
return true;
}
/// <summary>
/// Validates the specified value to ensure it corresponds to the client secret.
/// Note: when overriding this method, using a time-constant comparer is strongly recommended.
/// </summary>
/// <param name="secret">The client secret to compare to the value stored in the database.</param>
/// <param name="comparand">The value stored in the database, which is usually a hashed representation of the secret.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the specified value was valid.
/// </returns>
protected virtual ValueTask<bool> ValidateClientSecretAsync(
string secret, string comparand, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(secret))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0216), nameof(secret));
}
if (string.IsNullOrEmpty(comparand))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand));
}
try
{
return new(VerifyHashedSecret(comparand, secret));
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
Logger.LogWarning(exception, SR.GetResourceString(SR.ID6163));
return new(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 is 0)
{
return false;
}
// Verify the hashing format version.
if (payload[0] is not 0x01)
{
return false;
}
// Read the hashing algorithm version.
var algorithm = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(1, sizeof(uint))) switch
{
0 => HashAlgorithmName.SHA1,
1 => HashAlgorithmName.SHA256,
2 => HashAlgorithmName.SHA512,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217))
};
// Read the iteration count of the algorithm.
var iterations = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(5, sizeof(uint)));
// Read the size of the salt and ensure it's more than 128 bits.
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint)));
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 OpenIddictHelpers.FixedTimeEquals(
left: payload.Slice(13 + salt.Length, keyLength),
right: DeriveKey(secret, salt.ToArray(), algorithm, iterations, keyLength));
}
}
private static byte[] DeriveKey(string secret, byte[] salt, HashAlgorithmName algorithm, int iterations, int length)
{
#if SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
return OpenIddictHelpers.DeriveKey(secret, salt, algorithm, iterations, length);
#else
var generator = new Pkcs5S2ParametersGenerator(algorithm switch
{
var name when name == HashAlgorithmName.SHA1 => new Sha1Digest(),
var name when name == HashAlgorithmName.SHA256 => new Sha256Digest(),
var name when name == HashAlgorithmName.SHA512 => new Sha512Digest(),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217))
});
generator.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(secret.ToCharArray()), salt, iterations);
var key = (KeyParameter) generator.GenerateDerivedMacParameters(length * 8);
return key.GetKey();
#endif
}
这个流程和AbpUsers的差不多,都是前端传给后端,后端再通过数据库保存的密文对明文进行hash转换,再用明文的hash比较数据库保存的密文
我是没感觉OpenIddictApplications这个ClientSecret有啥用,感觉跟用户名和密码一样,官方文档里说是内部用的,就是说内网操作需要验证咯,那确实
DeriveKey
DeriveKey
函数似乎才是具体的算法函数,ObfuscateClientSecretAsync
和VerifyHashedSecret
都有调用这个
private static byte[] DeriveKey(string secret, byte[] salt, HashAlgorithmName algorithm, int iterations, int length)
{
#if SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
return OpenIddictHelpers.DeriveKey(secret, salt, algorithm, iterations, length);
#else
var generator = new Pkcs5S2ParametersGenerator(algorithm switch
{
var name when name == HashAlgorithmName.SHA1 => new Sha1Digest(),
var name when name == HashAlgorithmName.SHA256 => new Sha256Digest(),
var name when name == HashAlgorithmName.SHA512 => new Sha512Digest(),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217))
});
generator.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(secret.ToCharArray()), salt, iterations);
var key = (KeyParameter) generator.GenerateDerivedMacParameters(length * 8);
return key.GetKey();
#endif
}