// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

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 unsafe sealed 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);
        }
    }
}
