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 Microsoft.AspNetCore.Cryptography;
using Microsoft.AspNetCore.Cryptography.Cng;
using Microsoft.AspNetCore.Cryptography.SafeHandles;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.Cng.Internal;
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 : CngAuthenticatedEncryptorBase
{
    // 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();
    }
 
    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;
    }
 
    protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
    {
        // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC
        if (cbCiphertext < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes))
        {
            throw Error.CryptCommon_PayloadInvalid();
        }
 
        // Assumption: pbCipherText := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
 
        var cbEncryptedData = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes));
 
        // Calculate offsets
        byte* pbKeyModifier = pbCiphertext;
        byte* pbIV = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
        byte* pbEncryptedData = &pbIV[_symmetricAlgorithmBlockSizeInBytes];
        byte* pbActualHmac = &pbEncryptedData[cbEncryptedData];
 
        // 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: cbAdditionalAuthenticatedData,
                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 + cbEncryptedData, pbActualHmac))
                {
                    throw Error.CryptCommon_PayloadInvalid();
                }
            }
 
            // If the integrity check succeeded, decrypt the payload.
            using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
            {
                return DoCbcDecrypt(decryptionSubkeyHandle, pbIV, pbEncryptedData, cbEncryptedData);
            }
        }
        finally
        {
            // Buffer contains sensitive key material; delete.
            UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys);
        }
    }
 
    public override void Dispose()
    {
        _sp800_108_ctr_hmac_provider.Dispose();
 
        // We don't want to dispose of the underlying algorithm instances because they
        // might be reused.
    }
 
    // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size.
    private byte[] DoCbcDecrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* pbInput, uint cbInput)
    {
        // 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, figure out how large an output buffer we require.
        // Ideally we'd be able to transform the last block ourselves and strip
        // off the padding before creating the return value array, but we don't
        // know the actual padding scheme being used under the covers (we can't
        // assume PKCS#7). So unfortunately we're stuck with the temporary buffer.
        // (Querying the output size won't mutate the IV.)
        uint dwEstimatedDecryptedByteCount;
        var ntstatus = UnsafeNativeMethods.BCryptDecrypt(
            hKey: symmetricKeyHandle,
            pbInput: pbInput,
            cbInput: cbInput,
            pPaddingInfo: null,
            pbIV: pbClonedIV,
            cbIV: _symmetricAlgorithmBlockSizeInBytes,
            pbOutput: null,
            cbOutput: 0,
            pcbResult: out dwEstimatedDecryptedByteCount,
            dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
        UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
 
        var decryptedPayload = new byte[dwEstimatedDecryptedByteCount];
        uint dwActualDecryptedByteCount;
        fixed (byte* pbDecryptedPayload = decryptedPayload)
        {
            byte dummy;
 
            // Perform the actual decryption.
            ntstatus = UnsafeNativeMethods.BCryptDecrypt(
                hKey: symmetricKeyHandle,
                pbInput: pbInput,
                cbInput: cbInput,
                pPaddingInfo: null,
                pbIV: pbClonedIV,
                cbIV: _symmetricAlgorithmBlockSizeInBytes,
                pbOutput: (pbDecryptedPayload != null) ? pbDecryptedPayload : &dummy, // CLR won't pin zero-length arrays
                cbOutput: dwEstimatedDecryptedByteCount,
                pcbResult: out dwActualDecryptedByteCount,
                dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
            UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
        }
 
        // Decryption finished!
        CryptoUtil.Assert(dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount, "dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount");
        if (dwActualDecryptedByteCount == dwEstimatedDecryptedByteCount)
        {
            // payload takes up the entire buffer
            return decryptedPayload;
        }
        else
        {
            // payload takes up only a partial buffer
            var resizedDecryptedPayload = new byte[dwActualDecryptedByteCount];
            Buffer.BlockCopy(decryptedPayload, 0, resizedDecryptedPayload, 0, resizedDecryptedPayload.Length);
            return resizedDecryptedPayload;
        }
    }
 
    // '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");
    }
 
    protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer)
    {
        // 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
            _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
                pbLabel: pbAdditionalAuthenticatedData,
                cbLabel: cbAdditionalAuthenticatedData,
                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))
            {
                // We can't assume PKCS#7 padding (maybe the underlying provider is really using CTS),
                // so we need to query the padded output size before we can allocate the return value array.
                var cbOutputCiphertext = GetCbcEncryptedOutputSizeWithPadding(symmetricKeyHandle, pbPlaintext, cbPlaintext);
 
                // Allocate return value array and start copying some data
                var retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext + _hmacAlgorithmDigestLengthInBytes + cbPostBuffer)];
                fixed (byte* pbRetVal = retVal)
                {
                    // Calculate offsets
                    byte* pbOutputKeyModifier = &pbRetVal[cbPreBuffer];
                    byte* pbOutputIV = &pbOutputKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
                    byte* pbOutputCiphertext = &pbOutputIV[_symmetricAlgorithmBlockSizeInBytes];
                    byte* pbOutputHmac = &pbOutputCiphertext[cbOutputCiphertext];
 
                    UnsafeBufferUtil.BlockCopy(from: pbKeyModifierAndIV, to: pbOutputKeyModifier, byteCount: cbKeyModifierAndIV);
 
                    // retVal will eventually contain { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer }
                    // At this point, retVal := { preBuffer | keyModifier | iv | _____ | _____ | postBuffer }
 
                    DoCbcEncrypt(
                        symmetricKeyHandle: symmetricKeyHandle,
                        pbIV: pbIV,
                        pbInput: pbPlaintext,
                        cbInput: cbPlaintext,
                        pbOutput: pbOutputCiphertext,
                        cbOutput: cbOutputCiphertext);
 
                    // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | _____ | postBuffer }
 
                    // Compute the HMAC over the IV and the ciphertext (prevents IV tampering).
                    // The HMAC is already implicitly computed over the key modifier since the key
                    // modifier is used as input to the KDF.
                    using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
                    {
                        hashHandle.HashData(
                            pbInput: pbOutputIV,
                            cbInput: checked(_symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext),
                            pbHashDigest: pbOutputHmac,
                            cbHashDigest: _hmacAlgorithmDigestLengthInBytes);
                    }
 
                    // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer }
                    // And we're done!
                    return retVal;
                }
            }
        }
        finally
        {
            // Buffer contains sensitive material; delete it.
            UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys);
        }
    }
 
    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.
        uint dwResult;
        var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
            hKey: symmetricKeyHandle,
            pbInput: pbInput,
            cbInput: cbInput,
            pPaddingInfo: null,
            pbIV: pbIV,
            cbIV: _symmetricAlgorithmBlockSizeInBytes,
            pbOutput: null,
            cbOutput: 0,
            pcbResult: out 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);
    }
}