Abp vNext Secret

发布时间 2023-12-08 09:30:47作者: .NET好耶

Abp vNext Secret

使用Abp vNext 6.0
abp大概有两个secret,AbpUsersOpenIddictApplications

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函数似乎才是具体的算法函数,ObfuscateClientSecretAsyncVerifyHashedSecret都有调用这个

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
}

Abp vNext Secret结束