File: System\Security\Cryptography\Pbkdf2Implementation.Windows.cs
Web Access
Project: src\src\runtime\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Threading;
using Microsoft.Win32.SafeHandles;
using BCryptAlgPseudoHandle = Interop.BCrypt.BCryptAlgPseudoHandle;
using BCryptBuffer = Interop.BCrypt.BCryptBuffer;
using BCryptOpenAlgorithmProviderFlags = Interop.BCrypt.BCryptOpenAlgorithmProviderFlags;
using CngBufferDescriptors = Interop.BCrypt.CngBufferDescriptors;
using NTSTATUS = Interop.BCrypt.NTSTATUS;

namespace System.Security.Cryptography
{
    internal static partial class Pbkdf2Implementation
    {
        // A cached instance of PBKDF2 for Windows 8, where pseudo handles are not supported.
        private static SafeBCryptAlgorithmHandle? s_pbkdf2AlgorithmHandle;

        public static void Fill(
            ReadOnlySpan<byte> password,
            ReadOnlySpan<byte> salt,
            int iterations,
            HashAlgorithmName hashAlgorithmName,
            Span<byte> destination)
        {
            Debug.Assert(!destination.IsEmpty);
            Debug.Assert(iterations >= 0);

            FillKeyDerivation(password, salt, iterations, hashAlgorithmName, destination);
        }

        private static unsafe void FillKeyDerivation(
            ReadOnlySpan<byte> password,
            ReadOnlySpan<byte> salt,
            int iterations,
            HashAlgorithmName hashAlgorithm,
            Span<byte> destination)
        {
            Debug.Assert(hashAlgorithm.Name is not null);
            string hashAlgorithmName = hashAlgorithm.Name;
            SafeBCryptKeyHandle keyHandle;
            int hashBlockSizeBytes = GetHashBlockSize(hashAlgorithmName);

            scoped Span<byte> clearSpan;
            scoped ReadOnlySpan<byte> symmetricKeyMaterial;
            int symmetricKeyMaterialLength;

            if (password.IsEmpty)
            {
                // CNG won't accept a null pointer for the password.
                symmetricKeyMaterial = [0];
                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.

                if (!CryptographicOperations.TryHashData(hashAlgorithm, password, hashBuffer, out int hashBufferSize))
                {
                    CryptographicOperations.ZeroMemory(hashBuffer);
                    Debug.Fail("Preallocated buffer was too small.");
                    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();
                    }
                }
            }
        }

        private static int GetHashBlockSize(string hashAlgorithmName)
        {
            // Block sizes per NIST FIPS pub 180-4 and FIPS 202.
            switch (hashAlgorithmName)
            {
                case HashAlgorithmNames.SHA1:
                case HashAlgorithmNames.SHA256:
                    return 512 / 8;
                case HashAlgorithmNames.SHA384:
                case HashAlgorithmNames.SHA512:
                    return 1024 / 8;
                case HashAlgorithmNames.SHA3_256:
                    return HMACSHA3_256.BlockSize;
                case HashAlgorithmNames.SHA3_384:
                    return HMACSHA3_384.BlockSize;
                case HashAlgorithmNames.SHA3_512:
                    return HMACSHA3_512.BlockSize;
                default:
                    Debug.Fail($"Unexpected hash algorithm '{hashAlgorithmName}'");
                    throw new CryptographicException();
            }
        }
    }
}