File: Managed\AesGcmAuthenticatedEncryptor.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.
 
#if NETCOREAPP
using System;
using System.Buffers;
using System.IO;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.SP800_108;
 
namespace Microsoft.AspNetCore.DataProtection.Managed;
 
// An encryptor that uses AesGcm to do encryption
internal sealed unsafe class AesGcmAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, ISpanAuthenticatedEncryptor, IDisposable
{
    // Having a key modifier ensures with overwhelming probability that no two encryption operations
    // will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's
    // ability to mount a key-dependent chosen ciphertext attack. See also the class-level comment
    //  on CngGcmAuthenticatedEncryptor for how this is used to overcome GCM's IV limitations.
    private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
 
    private const int NONCE_SIZE_IN_BYTES = 96 / 8; // GCM has a fixed 96-bit IV
    private const int TAG_SIZE_IN_BYTES = 128 / 8; // we're hardcoding a 128-bit authentication tag size
 
    // See CngGcmAuthenticatedEncryptor.CreateContextHeader for how these were precomputed
 
    // 128 "00-01-00-00-00-10-00-00-00-0C-00-00-00-10-00-00-00-10-95-7C-50-FF-69-2E-38-8B-9A-D5-C7-68-9E-4B-9E-2B"
    private static readonly byte[] AES_128_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x95, 0x7C, 0x50, 0xFF, 0x69, 0x2E, 0x38, 0x8B, 0x9A, 0xD5, 0xC7, 0x68, 0x9E, 0x4B, 0x9E, 0x2B };
 
    // 192 "00-01-00-00-00-18-00-00-00-0C-00-00-00-10-00-00-00-10-0D-AA-01-3A-95-0A-DA-2B-79-8F-5F-F2-72-FA-D3-63"
    private static readonly byte[] AES_192_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x0D, 0xAA, 0x01, 0x3A, 0x95, 0x0A, 0xDA, 0x2B, 0x79, 0x8F, 0x5F, 0xF2, 0x72, 0xFA, 0xD3, 0x63 };
 
    // 256 00-01-00-00-00-20-00-00-00-0C-00-00-00-10-00-00-00-10-E7-DC-CE-66-DF-85-5A-32-3A-6B-B7-BD-7A-59-BE-45
    private static readonly byte[] AES_256_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0xE7, 0xDC, 0xCE, 0x66, 0xDF, 0x85, 0x5A, 0x32, 0x3A, 0x6B, 0xB7, 0xBD, 0x7A, 0x59, 0xBE, 0x45 };
 
    private readonly byte[] _contextHeader;
 
    private readonly Secret _keyDerivationKey;
    private readonly int _derivedkeySizeInBytes;
    private readonly IManagedGenRandom _genRandom;
 
    public AesGcmAuthenticatedEncryptor(ISecret keyDerivationKey, int derivedKeySizeInBytes, IManagedGenRandom? genRandom = null)
    {
        _keyDerivationKey = new Secret(keyDerivationKey);
        _derivedkeySizeInBytes = derivedKeySizeInBytes;
 
        switch (_derivedkeySizeInBytes)
        {
            case 16:
                _contextHeader = AES_128_GCM_Header;
                break;
            case 24:
                _contextHeader = AES_192_GCM_Header;
                break;
            case 32:
                _contextHeader = AES_256_GCM_Header;
                break;
            default:
                throw CryptoUtil.Fail("Unexpected AES key size in bytes only support 16, 24, 32."); // should never happen
        }
 
        _genRandom = genRandom ?? ManagedGenRandomImpl.Instance;
    }
 
    public void Decrypt<TWriter>(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination)
        where TWriter : IBufferWriter<byte>, allows ref struct
    {
        try
        {
            // Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag
            if (ciphertext.Length < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)
            {
                throw Error.CryptCommon_PayloadInvalid();
            }
 
            var plaintextBytes = ciphertext.Length - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES);
 
            // Calculate offsets in the ciphertext
            var keyModifierOffset = 0;
            var nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
            var encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES;
            var tagOffset = encryptedDataOffset + plaintextBytes;
 
            // Extract spans for each component
            var keyModifier = ciphertext.Slice(keyModifierOffset, KEY_MODIFIER_SIZE_IN_BYTES);
            var nonce = ciphertext.Slice(nonceOffset, NONCE_SIZE_IN_BYTES);
            var encrypted = ciphertext.Slice(encryptedDataOffset, plaintextBytes);
            var tag = ciphertext.Slice(tagOffset, TAG_SIZE_IN_BYTES);
 
            // Get buffer from writer with the plaintext size
            var buffer = destination.GetSpan(plaintextBytes);
 
            // Get the plaintext destination
            var plaintext = buffer.Slice(0, plaintextBytes);
 
            // Decrypt the KDK and use it to restore the original encryption key
            // We pin all unencrypted keys to limit their exposure via GC relocation
            Span<byte> decryptedKdk = _keyDerivationKey.Length <= 256
                ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length)
                : new byte[_keyDerivationKey.Length];
 
            Span<byte> derivedKey = _derivedkeySizeInBytes <= 256
                ? stackalloc byte[256].Slice(0, _derivedkeySizeInBytes)
                : new byte[_derivedkeySizeInBytes];
 
            fixed (byte* decryptedKdkUnsafe = decryptedKdk)
            fixed (byte* derivedKeyUnsafe = derivedKey)
            {
                try
                {
                    _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length);
                    ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
                        kdk: decryptedKdk,
                        label: additionalAuthenticatedData,
                        contextHeader: _contextHeader,
                        contextData: keyModifier,
                        operationSubkey: derivedKey,
                        validationSubkey: Span<byte>.Empty /* filling in derivedKey only */);
 
                    // Perform the decryption operation directly into destination
                    using var aes = new AesGcm(derivedKey, TAG_SIZE_IN_BYTES);
                    aes.Decrypt(nonce, encrypted, tag, plaintext);
 
                    // Advance the writer by the number of bytes written
                    destination.Advance(plaintextBytes);
                }
                finally
                {
                    // delete since these contain secret material
                    decryptedKdk.Clear();
                    derivedKey.Clear();
                }
            }
        }
        catch (Exception ex) when (ex.RequiresHomogenization())
        {
            // Homogenize all exceptions to CryptographicException.
            throw Error.CryptCommon_GenericError(ex);
        }
    }
 
    public byte[] Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
    {
        ciphertext.Validate();
        additionalAuthenticatedData.Validate();
 
        var outputSize = ciphertext.Count - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES);
        if (outputSize < 0)
        {
            throw Error.CryptCommon_PayloadInvalid();
        }
 
        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);
            }
        }
    }
 
    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 = checked(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Count + TAG_SIZE_IN_BYTES);
        var outputSize = (int)(preBufferSize + size + postBufferSize);
 
        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);
 
            CryptoUtil.Assert(refPooledBuffer.WrittenSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize");
            return refPooledBuffer.WrittenSpan.ToArray();
        }
        finally
        {
            refPooledBuffer.Dispose();
            if (rentedBuffer is not null)
            {
                ArrayPool<byte>.Shared.Return(rentedBuffer, clearArray: true);
            }
        }
    }
 
    public void Encrypt<TWriter>(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination)
        where TWriter : IBufferWriter<byte>, allows ref struct
    {
        try
        {
            // Calculate total required size: keyModifier + nonce + plaintext + tag
            // In GCM, ciphertext length equals plaintext length
            var totalRequiredSize = checked(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Length + TAG_SIZE_IN_BYTES);
 
            // Get buffer from writer with the required total size
            var buffer = destination.GetSpan(totalRequiredSize);
 
            // Generate random key modifier and nonce
            var keyModifier = _genRandom.GenRandom(KEY_MODIFIER_SIZE_IN_BYTES);
            var nonceBytes = _genRandom.GenRandom(NONCE_SIZE_IN_BYTES);
 
            // Copy keyModifier and nonce to buffer
            keyModifier.CopyTo(buffer.Slice(0, KEY_MODIFIER_SIZE_IN_BYTES));
            nonceBytes.CopyTo(buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES, NONCE_SIZE_IN_BYTES));
 
            // At this point, buffer := { keyModifier | nonce | _____ | _____ }
 
            // Use the KDF to generate a new symmetric block cipher key
            // We'll need a temporary buffer to hold the symmetric encryption subkey
            Span<byte> decryptedKdk = _keyDerivationKey.Length <= 256
                ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length)
                : new byte[_keyDerivationKey.Length];
 
            Span<byte> derivedKey = _derivedkeySizeInBytes <= 256
                ? stackalloc byte[256].Slice(0, _derivedkeySizeInBytes)
                : new byte[_derivedkeySizeInBytes];
 
            fixed (byte* decryptedKdkUnsafe = decryptedKdk)
            fixed (byte* __unused__2 = derivedKey)
            {
                try
                {
                    _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length);
                    ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
                        kdk: decryptedKdk,
                        label: additionalAuthenticatedData,
                        contextHeader: _contextHeader,
                        contextData: keyModifier,
                        operationSubkey: derivedKey,
                        validationSubkey: Span<byte>.Empty /* filling in derivedKey only */);
 
                    // Perform GCM encryption. Buffer expected structure:
                    // { keyModifier | nonce | encryptedData | authenticationTag }
                    var nonce = buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES, NONCE_SIZE_IN_BYTES);
                    var encrypted = buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES, plaintext.Length);
                    var tag = buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Length, TAG_SIZE_IN_BYTES);
 
                    using var aes = new AesGcm(derivedKey, TAG_SIZE_IN_BYTES);
                    aes.Encrypt(nonce, plaintext, encrypted, tag);
 
                    // At this point, buffer := { keyModifier | nonce | encryptedData | authenticationTag }
                    // And we're done!
                    destination.Advance(totalRequiredSize);
                }
                finally
                {
                    // delete since these contain secret material
                    decryptedKdk.Clear();
                    derivedKey.Clear();
                }
            }
        }
        catch (Exception ex) when (ex.RequiresHomogenization())
        {
            // Homogenize all exceptions to CryptographicException.
            throw Error.CryptCommon_GenericError(ex);
        }
    }
    public void Dispose()
    {
        _keyDerivationKey.Dispose();
    }
}
#endif