File: Cng\CbcAuthenticatedEncryptor.cs
Web Access
Project: src\src\DataProtection\DataProtection\src\Microsoft.AspNetCore.DataProtection.csproj (Microsoft.AspNetCore.DataProtection)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Cryptography;
using Microsoft.AspNetCore.Cryptography.Cng;
using Microsoft.AspNetCore.Cryptography.SafeHandles;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.SP800_108;
 
namespace Microsoft.AspNetCore.DataProtection.Cng;
 
// An encryptor which does Encrypt(CBC) + HMAC using the Windows CNG (BCrypt*) APIs.
// The payloads produced by this encryptor should be compatible with the payloads
// produced by the managed Encrypt(CBC) + HMAC encryptor.
internal sealed unsafe class CbcAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, IDisposable
#if NET
    , ISpanAuthenticatedEncryptor
#endif
{
    // Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single
    // key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block
    // encryption operations, which a high-traffic web server might perform in mere hours.
    // AES and other 128-bit block ciphers are less susceptible to this due to the larger IV
    // space, but unfortunately some organizations require older 64-bit block ciphers. To address
    // the collision issue, we'll feed 128 bits of entropy to the KDF when performing subkey
    // generation. This creates >= 192 bits total entropy for each operation, so we shouldn't
    // expect a collision until >= 2^96 operations. Even 2^80 operations still maintains a <= 2^-32
    // probability of collision, and this is acceptable for the expected KDK lifetime.
    private const uint KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
 
    private readonly byte[] _contextHeader;
    private readonly IBCryptGenRandom _genRandom;
    private readonly BCryptAlgorithmHandle _hmacAlgorithmHandle;
    private readonly uint _hmacAlgorithmDigestLengthInBytes;
    private readonly uint _hmacAlgorithmSubkeyLengthInBytes;
    private readonly ISP800_108_CTR_HMACSHA512Provider _sp800_108_ctr_hmac_provider;
    private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle;
    private readonly uint _symmetricAlgorithmBlockSizeInBytes;
    private readonly uint _symmetricAlgorithmSubkeyLengthInBytes;
 
    public CbcAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, BCryptAlgorithmHandle hmacAlgorithmHandle, IBCryptGenRandom? genRandom = null)
    {
        _genRandom = genRandom ?? BCryptGenRandomImpl.Instance;
        _sp800_108_ctr_hmac_provider = SP800_108_CTR_HMACSHA512Util.CreateProvider(keyDerivationKey);
        _symmetricAlgorithmHandle = symmetricAlgorithmHandle;
        _symmetricAlgorithmBlockSizeInBytes = symmetricAlgorithmHandle.GetCipherBlockLength();
        _symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes;
        _hmacAlgorithmHandle = hmacAlgorithmHandle;
        _hmacAlgorithmDigestLengthInBytes = hmacAlgorithmHandle.GetHashDigestLength();
        _hmacAlgorithmSubkeyLengthInBytes = _hmacAlgorithmDigestLengthInBytes; // for simplicity we'll generate HMAC subkeys with a length equal to the digest length
 
        // Argument checking on the algorithms and lengths passed in to us
        AlgorithmAssert.IsAllowableSymmetricAlgorithmBlockSize(checked(_symmetricAlgorithmBlockSizeInBytes * 8));
        AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked(_symmetricAlgorithmSubkeyLengthInBytes * 8));
        AlgorithmAssert.IsAllowableValidationAlgorithmDigestSize(checked(_hmacAlgorithmDigestLengthInBytes * 8));
 
        _contextHeader = CreateContextHeader();
    }
 
    public void Decrypt<TWriter>(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination) where TWriter : IBufferWriter<byte>
#if NET
        , allows ref struct
#endif
    {
        // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC
        if (ciphertext.Length < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes))
        {
            throw Error.CryptCommon_PayloadInvalid();
        }
 
        var cbEncryptedDataLength = checked(ciphertext.Length - (int)(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes));
 
        // Assumption: ciphertext := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
        fixed (byte* pbCiphertext = ciphertext)
        fixed (byte* pbAdditionalAuthenticatedData = additionalAuthenticatedData)
        {
            // Calculate offsets
            byte* pbKeyModifier = pbCiphertext;
            byte* pbIV = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
            byte* pbEncryptedData = &pbIV[_symmetricAlgorithmBlockSizeInBytes];
            byte* pbActualHmac = &pbEncryptedData[cbEncryptedDataLength];
 
            // Use the KDF to recreate the symmetric encryption and HMAC subkeys
            // We'll need a temporary buffer to hold them
            var cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes);
            byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)];
            try
            {
                _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
                    pbLabel: pbAdditionalAuthenticatedData,
                    cbLabel: (uint)additionalAuthenticatedData.Length,
                    contextHeader: _contextHeader,
                    pbContext: pbKeyModifier,
                    cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
                    pbDerivedKey: pbTempSubkeys,
                    cbDerivedKey: cbTempSubkeys);
 
                // Calculate offsets
                byte* pbSymmetricEncryptionSubkey = pbTempSubkeys;
                byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes];
 
                // First, perform an explicit integrity check over (iv | encryptedPayload) to ensure the
                // data hasn't been tampered with. The integrity check is also implicitly performed over
                // keyModifier since that value was provided to the KDF earlier.
                using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
                {
                    if (!ValidateHash(hashHandle, pbIV, _symmetricAlgorithmBlockSizeInBytes + (uint)cbEncryptedDataLength, pbActualHmac))
                    {
                        throw Error.CryptCommon_PayloadInvalid();
                    }
                }
 
                // If the integrity check succeeded, decrypt the payload.
                using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
                {
                    // BCryptDecrypt mutates the provided IV; we need to clone it to prevent mutation of the original value
                    byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
                    UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes);
 
                    // First, query the output size needed
                    var ntstatus = UnsafeNativeMethods.BCryptDecrypt(
                        hKey: decryptionSubkeyHandle,
                        pbInput: pbEncryptedData,
                        cbInput: (uint)cbEncryptedDataLength,
                        pPaddingInfo: null,
                        pbIV: pbClonedIV,
                        cbIV: _symmetricAlgorithmBlockSizeInBytes,
                        pbOutput: null,  // NULL output = size query only
                        cbOutput: 0,
                        pcbResult: out var dwRequiredSize,
                        dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
                    UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
                    // Get buffer from writer with the required size
                    var buffer = destination.GetSpan(checked((int)dwRequiredSize));
 
                    // Clone IV again for the actual decryption call
                    byte* pbClonedIV2 = stackalloc byte[(int)_symmetricAlgorithmBlockSizeInBytes];
                    UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV2, byteCount: _symmetricAlgorithmBlockSizeInBytes);
 
                    // Perform the actual decryption
                    fixed (byte* pbBuffer = buffer)
                    {
                        byte dummy;
                        ntstatus = UnsafeNativeMethods.BCryptDecrypt(
                            hKey: decryptionSubkeyHandle,
                            pbInput: pbEncryptedData,
                            cbInput: (uint)cbEncryptedDataLength,
                            pPaddingInfo: null,
                            pbIV: pbClonedIV2,
                            cbIV: _symmetricAlgorithmBlockSizeInBytes,
                            pbOutput: (buffer.Length > 0) ? pbBuffer : &dummy,
                            cbOutput: (uint)buffer.Length,
                            pcbResult: out var dwActualDecryptedByteCount,
                            dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
 
                        UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
                        // Advance the writer by the number of bytes actually written
                        destination.Advance(checked((int)dwActualDecryptedByteCount));
                    }
                }
            }
            finally
            {
                // Buffer contains sensitive key material; delete.
                UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys);
            }
        }
    }
 
    public byte[] Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
    {
        ciphertext.Validate();
        additionalAuthenticatedData.Validate();
 
        var outputSize = ciphertext.Count - (int)(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes);
        if (outputSize <= 0)
        {
            throw Error.CryptCommon_PayloadInvalid();
        }
 
#if NET
        byte[]? rentedBuffer = null;
        var buffer = outputSize < 256
            ? stackalloc byte[255]
            : (rentedBuffer = ArrayPool<byte>.Shared.Rent(outputSize));
 
        var refPooledBuffer = new RefPooledArrayBufferWriter<byte>(buffer);
        try
        {
            Decrypt(ciphertext, additionalAuthenticatedData, ref refPooledBuffer);
            return refPooledBuffer.WrittenSpan.ToArray();
        }
        finally
        {
            refPooledBuffer.Dispose();
            if (rentedBuffer is not null)
            {
                ArrayPool<byte>.Shared.Return(rentedBuffer, clearArray: true);
            }
        }
 
#else
        var pooledArrayBuffer = new PooledArrayBufferWriter<byte>(outputSize);
        try
        {
            Decrypt(ciphertext, additionalAuthenticatedData, ref pooledArrayBuffer);
            return pooledArrayBuffer.WrittenSpan.ToArray();
        }
        finally
        {
            pooledArrayBuffer.Dispose();
        }
#endif
    }
 
    // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size.
    private void DoCbcEncrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* pbInput, uint cbInput, byte* pbOutput, uint cbOutput)
    {
        // BCryptEncrypt mutates the provided IV; we need to clone it to prevent mutation of the original value
        byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
        UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes);
 
        uint dwEncryptedBytes;
        var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
            hKey: symmetricKeyHandle,
            pbInput: pbInput,
            cbInput: cbInput,
            pPaddingInfo: null,
            pbIV: pbClonedIV,
            cbIV: _symmetricAlgorithmBlockSizeInBytes,
            pbOutput: pbOutput,
            cbOutput: cbOutput,
            pcbResult: out dwEncryptedBytes,
            dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
        UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
        // Need to make sure we didn't underrun the buffer - means caller passed a bad value
        CryptoUtil.Assert(dwEncryptedBytes == cbOutput, "dwEncryptedBytes == cbOutput");
    }
 
    public int GetEncryptedSize(int plainTextLength)
    {
        uint paddedCiphertextLength = GetCbcEncryptedOutputSizeWithPadding((uint)plainTextLength);
        return checked((int)(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + paddedCiphertextLength + _hmacAlgorithmDigestLengthInBytes));
    }
 
    public void Encrypt<TWriter>(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination) where TWriter : IBufferWriter<byte>
#if NET
        , allows ref struct
#endif
    {
        // This buffer will be used to hold the symmetric encryption and HMAC subkeys
        // used in the generation of this payload.
        var cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes);
        byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)];
 
        try
        {
            // Randomly generate the key modifier and IV.
            var cbKeyModifierAndIV = checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes);
            byte* pbKeyModifierAndIV = stackalloc byte[checked((int)cbKeyModifierAndIV)];
            _genRandom.GenRandom(pbKeyModifierAndIV, cbKeyModifierAndIV);
 
            // Calculate offsets
            byte* pbKeyModifier = pbKeyModifierAndIV;
            byte* pbIV = &pbKeyModifierAndIV[KEY_MODIFIER_SIZE_IN_BYTES];
 
            // Use the KDF to generate a new symmetric encryption and HMAC subkey
            fixed (byte* pbAdditionalAuthenticatedData = additionalAuthenticatedData)
            {
                _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
                    pbLabel: pbAdditionalAuthenticatedData,
                    cbLabel: (uint)additionalAuthenticatedData.Length,
                    contextHeader: _contextHeader,
                    pbContext: pbKeyModifier,
                    cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
                    pbDerivedKey: pbTempSubkeys,
                    cbDerivedKey: cbTempSubkeys);
            }
 
            // Calculate offsets
            byte* pbSymmetricEncryptionSubkey = pbTempSubkeys;
            byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes];
 
            using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
            {
                // Query the padded ciphertext output size
                byte dummy;
                fixed (byte* pbPlaintextArray = plaintext)
                {
                    var pbPlaintext = (pbPlaintextArray != null) ? pbPlaintextArray : &dummy;
 
                    // First, query the size needed for ciphertext
                    var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
                        hKey: symmetricKeyHandle,
                        pbInput: pbPlaintext,
                        cbInput: (uint)plaintext.Length,
                        pPaddingInfo: null,
                        pbIV: pbIV,
                        cbIV: _symmetricAlgorithmBlockSizeInBytes,
                        pbOutput: null,  // NULL output = size query only
                        cbOutput: 0,
                        pcbResult: out var dwCiphertextSize,
                        dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
                    UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
                    // Calculate total required size
                    var totalRequiredSize = checked((int)(cbKeyModifierAndIV + dwCiphertextSize + _hmacAlgorithmDigestLengthInBytes));
 
                    // Get buffer from writer with the required total size
                    var buffer = destination.GetSpan(totalRequiredSize);
 
                    fixed (byte* pbBuffer = buffer)
                    {
                        // Calculate offsets in destination buffer
                        byte* pbOutputKeyModifier = pbBuffer;
                        byte* pbOutputIV = &pbOutputKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
                        byte* pbOutputCiphertext = &pbOutputIV[_symmetricAlgorithmBlockSizeInBytes];
                        byte* pbOutputHmac = &pbOutputCiphertext[dwCiphertextSize];
 
                        // Copy key modifier and IV to output
                        Unsafe.CopyBlock(pbOutputKeyModifier, pbKeyModifierAndIV, cbKeyModifierAndIV);
 
                        // Clone IV for encryption (BCryptEncrypt mutates it)
                        byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
                        UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes);
 
                        // Perform encryption
                        ntstatus = UnsafeNativeMethods.BCryptEncrypt(
                            hKey: symmetricKeyHandle,
                            pbInput: pbPlaintext,
                            cbInput: (uint)plaintext.Length,
                            pPaddingInfo: null,
                            pbIV: pbClonedIV,
                            cbIV: _symmetricAlgorithmBlockSizeInBytes,
                            pbOutput: pbOutputCiphertext,
                            cbOutput: dwCiphertextSize,
                            pcbResult: out var dwActualCiphertextSize,
                            dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
                        UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
                        CryptoUtil.Assert(dwActualCiphertextSize == dwCiphertextSize, "dwActualCiphertextSize == dwCiphertextSize");
 
                        // Calculate HMAC over (IV | ciphertext)
                        using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
                        {
                            hashHandle.HashData(
                                pbInput: pbOutputIV,
                                cbInput: checked(_symmetricAlgorithmBlockSizeInBytes + dwCiphertextSize),
                                pbHashDigest: pbOutputHmac,
                                cbHashDigest: _hmacAlgorithmDigestLengthInBytes);
                        }
 
                        // Advance the writer by the total bytes written
                        destination.Advance(totalRequiredSize);
                    }
                }
            }
        }
        finally
        {
            // Buffer contains sensitive material; delete it.
            UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys);
        }
    }
 
    public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData)
        => Encrypt(plaintext, additionalAuthenticatedData, 0, 0);
 
    public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize)
    {
        plaintext.Validate();
        additionalAuthenticatedData.Validate();
 
        var size = GetEncryptedSize(plaintext.Count);
        var outputSize = (int)(preBufferSize + size + postBufferSize);
 
#if NET
        byte[] rentedBuffer = null!;
        var buffer = outputSize < 256
            ? stackalloc byte[255]
            : (rentedBuffer = ArrayPool<byte>.Shared.Rent(outputSize));
 
        var refPooledBuffer = new RefPooledArrayBufferWriter<byte>(buffer);
        try
        {
            // arrays are pooled. and they MAY contain non-zeros in the pre-buffer and post-buffer regions.
            // we could clean them up, but it's not strictly necessary - the important part is that output array
            // has those pre/post buffer regions, which will be used by the caller.
            refPooledBuffer.Advance((int)preBufferSize);
            Encrypt(plaintext, additionalAuthenticatedData, ref refPooledBuffer);
            refPooledBuffer.Advance((int)postBufferSize);
 
            var resultSpan = refPooledBuffer.WrittenSpan.ToArray();
            CryptoUtil.Assert(resultSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize");
            return resultSpan;
        }
        finally
        {
            refPooledBuffer.Dispose();
            if (rentedBuffer is not null)
            {
                ArrayPool<byte>.Shared.Return(rentedBuffer, clearArray: true);
            }
        }
#else
        var pooledArrayBuffer = new PooledArrayBufferWriter<byte>(outputSize);
        try
        {
            pooledArrayBuffer.Advance(preBufferSize);
            Encrypt(plaintext, additionalAuthenticatedData, ref pooledArrayBuffer);
            pooledArrayBuffer.Advance(postBufferSize);
 
            var resultSpan = pooledArrayBuffer.WrittenSpan.ToArray();
            CryptoUtil.Assert(resultSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize");
 
            return resultSpan;
        }
        finally
        {
            pooledArrayBuffer.Dispose();
        }
#endif
    }
 
    /// <summary>
    /// Should be used only for expected encrypt/decrypt size calculation,
    /// use the other overload <see cref="GetCbcEncryptedOutputSizeWithPadding(BCryptKeyHandle, byte*, uint)"/>
    /// for the actual encryption algorithm
    /// </summary>
    private uint GetCbcEncryptedOutputSizeWithPadding(uint cbInput)
    {
        // Create a temporary key with dummy data for size calculation only
        // The actual key material doesn't matter for size calculation
        byte* pbDummyKey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)];
        // Leave pbDummyKey uninitialized (all zeros) - BCrypt doesn't care for size queries
 
        using var tempKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbDummyKey, _symmetricAlgorithmSubkeyLengthInBytes);
 
        // Use uninitialized IV and input data - only the lengths matter
        byte* pbDummyIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
        byte* pbDummyInput = stackalloc byte[checked((int)cbInput)];
 
        var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
            hKey: tempKeyHandle,
            pbInput: pbDummyInput,
            cbInput: cbInput,
            pPaddingInfo: null,
            pbIV: pbDummyIV,
            cbIV: _symmetricAlgorithmBlockSizeInBytes,
            pbOutput: null,  // NULL output = size query only
            cbOutput: 0,
            pcbResult: out var dwResult,
            dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
        UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
        return dwResult;
    }
 
    private uint GetCbcEncryptedOutputSizeWithPadding(BCryptKeyHandle symmetricKeyHandle, byte* pbInput, uint cbInput)
    {
        // ok for this memory to remain uninitialized since nobody depends on it
        byte* pbIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
 
        // Calling BCryptEncrypt with a null output pointer will cause it to return the total number
        // of bytes required for the output buffer.
        var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
            hKey: symmetricKeyHandle,
            pbInput: pbInput,
            cbInput: cbInput,
            pPaddingInfo: null,
            pbIV: pbIV,
            cbIV: _symmetricAlgorithmBlockSizeInBytes,
            pbOutput: null,
            cbOutput: 0,
            pcbResult: out var dwResult,
            dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
        UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
        return dwResult;
    }
 
    // 'pbExpectedDigest' must point to a '_hmacAlgorithmDigestLengthInBytes'-length buffer
    private bool ValidateHash(BCryptHashHandle hashHandle, byte* pbInput, uint cbInput, byte* pbExpectedDigest)
    {
        byte* pbActualDigest = stackalloc byte[checked((int)_hmacAlgorithmDigestLengthInBytes)];
        hashHandle.HashData(pbInput, cbInput, pbActualDigest, _hmacAlgorithmDigestLengthInBytes);
        return CryptoUtil.TimeConstantBuffersAreEqual(pbExpectedDigest, pbActualDigest, _hmacAlgorithmDigestLengthInBytes);
    }
 
    private byte[] CreateContextHeader()
    {
        var retVal = new byte[checked(
            1 /* KDF alg */
            + 1 /* chaining mode */
            + sizeof(uint) /* sym alg key size */
            + sizeof(uint) /* sym alg block size */
            + sizeof(uint) /* hmac alg key size */
            + sizeof(uint) /* hmac alg digest size */
            + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */
            + _hmacAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)];
 
        fixed (byte* pbRetVal = retVal)
        {
            byte* ptr = pbRetVal;
 
            // First is the two-byte header
            *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF
            *(ptr++) = 0; // 0x00 = CBC encryption + HMAC authentication
 
            // Next is information about the symmetric algorithm (key size followed by block size)
            BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes);
            BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmBlockSizeInBytes);
 
            // Next is information about the HMAC algorithm (key size followed by digest size)
            BitHelpers.WriteTo(ref ptr, _hmacAlgorithmSubkeyLengthInBytes);
            BitHelpers.WriteTo(ref ptr, _hmacAlgorithmDigestLengthInBytes);
 
            // See the design document for an explanation of the following code.
            var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes];
            fixed (byte* pbTempKeys = tempKeys)
            {
                byte dummy;
 
                // Derive temporary keys for encryption + HMAC.
                using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider())
                {
                    provider.DeriveKey(
                        pbLabel: &dummy,
                        cbLabel: 0,
                        pbContext: &dummy,
                        cbContext: 0,
                        pbDerivedKey: pbTempKeys,
                        cbDerivedKey: (uint)tempKeys.Length);
                }
 
                // At this point, tempKeys := { K_E || K_H }.
                byte* pbSymmetricEncryptionSubkey = pbTempKeys;
                byte* pbHmacSubkey = &pbTempKeys[_symmetricAlgorithmSubkeyLengthInBytes];
 
                // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer.
                using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
                {
                    fixed (byte* pbIV = new byte[_symmetricAlgorithmBlockSizeInBytes] /* will be zero-initialized */)
                    {
                        DoCbcEncrypt(
                            symmetricKeyHandle: symmetricKeyHandle,
                            pbIV: pbIV,
                            pbInput: &dummy,
                            cbInput: 0,
                            pbOutput: ptr,
                            cbOutput: _symmetricAlgorithmBlockSizeInBytes);
                    }
                }
                ptr += _symmetricAlgorithmBlockSizeInBytes;
 
                // MAC a zero-length input string and copy the digest to the return buffer.
                using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
                {
                    hashHandle.HashData(
                        pbInput: &dummy,
                        cbInput: 0,
                        pbHashDigest: ptr,
                        cbHashDigest: _hmacAlgorithmDigestLengthInBytes);
                }
 
                ptr += _hmacAlgorithmDigestLengthInBytes;
                CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length");
            }
        }
 
        // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || hmacAlgKeySize || hmacAlgDigestSize || E("") || MAC("") }.
        return retVal;
    }
 
    public void Dispose()
    {
        _sp800_108_ctr_hmac_provider.Dispose();
 
        // We don't want to dispose of the underlying algorithm instances because they
        // might be reused.
    }
}