Search code examples
.netpostgresqlexceptionencryptionutf-8

Unexpected behavior of AES decryption


The problem: I have implemented some functionality that does decryption at specific events, but SOMETIMES it throws unexpected and unexplained exceptions.

Flow: A master key/iv is fetched then data (from a PostgreSQL database is fetched) and decoded from base64 and then decrypted and then decompressed and decoded again. So summarizing it goes like this:

  1. Fetch Key/Iv
  2. Decode From Base64
  3. Decrypt with AES-256 CBC
  4. Decompress from Gzip (At this point it fails sometimes)
  5. Decode again to UTF-8

This is the code

public async Task<byte[]> PrivateDecrypt(long userId, byte[] data)
{
   var key = await this.GetOrAddPrivateEncryptionKey(userId);
   var keyBytes = key.ToUTF8Bytes();
   var (cipher, ivBytes) = data.ExtractIVPadding();

   var decrypted = await cipher
      .AESDecryptAndDecompressAsync(keyBytes, ivBytes)
      .ConfigureAwait(false);

   return decrypted;
}
public static byte[] FromBase64(this string base64) => Convert.FromBase64String(base64);
public static async Task<byte[]> AESDecryptAndDecompressAsync(this byte[] cipher, byte[] key, byte[] iv)
{
   if (cipher == null || cipher.Length <= 0)
      throw new ArgumentNullException(nameof(cipher), "incorrect data");

   if (key == null || key.Length != 32)
      throw new ArgumentNullException(nameof(key), "incorrect key");

   if (iv == null || iv.Length != 16)
      throw new ArgumentNullException(nameof(iv), "incorrect iv");

   using var aesAlg = Aes.Create();

   aesAlg.Mode = CipherMode.CBC;
   aesAlg.KeySize = 256;

   aesAlg.Key = key;
   aesAlg.IV = iv;
   aesAlg.Padding = PaddingMode.PKCS7;

   using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

   using (var decompressedStream = new MemoryStream())
   {
      using (var encryptedStream = new MemoryStream(cipher))
      {
         using (var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read))
         {
            using (var gzipStream = new GZipStream(cryptoStream, CompressionMode.Decompress))
               await gzipStream.CopyToAsync(decompressedStream).ConfigureAwait(false);

            return decompressedStream.ToArray();
         }           
      }           
   }
}

This is part of the exception stack trace I receive

    System.IO.InvalidDataException: The archive entry was compressed using an unsupported compression method.
   at System.IO.Compression.Inflater.Inflate(FlushCode flushCode)
   at System.IO.Compression.Inflater.ReadInflateOutput(Byte* bufPtr, Int32 length, FlushCode flushCode, Int32& bytesRead)
   at System.IO.Compression.Inflater.InflateVerified(Byte* bufPtr, Int32 length)
   at System.IO.Compression.DeflateStream.CopyToStream.WriteAsyncCore(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)
   at System.Security.Cryptography.CryptoStream.CopyToAsyncInternal(Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
   at System.IO.Compression.DeflateStream.CopyToStream.CopyFromSourceToDestinationAsync()
   at Trading.Tools.Bytes.EncryptionExtensions.AESDecryptAndDecompressAsync(Byte[] cipher, Byte[] key, Byte[] iv) in /src/Trading.Tools/Bytes/EncryptionExtensions.cs:line 207
   at Trading.Web.Commons.Encryption.LocalEncryptionService.PrivateDecrypt(Int64 userId, Byte[] data) in /src/Trading.Web.Commons/Encryption/LocalEncryptionService.cs:line 107

As you can see from the stacktrace the problem lies in the decompression layer AFTER the decryption.

Hypothesis:

  1. This code I can say for sure that it works
  2. This exception that I encounter happens quite rarely
  3. Data in database is correct, otherwise it would never work

My thoughts:

  1. I believe that due to probabilistic nature of the problem then this can be a race condition. There are 2 possibilities I see:
  • Either the database doesn't return me the full string and it escapes some character by mistake, but then how does decryption work ?
  • Either decryption returns weird results sometimes which leads me to think that even if there are separate instances there is still a shared state somewhere ?

Additional Info:

  1. I'm using EFCore 8.0.2 for fetching from database
  2. I'm using .NET Core 8.0.3
  3. I'm using PostgreSQL 16.1-bullseye

UPDATE 1: I've updated the code to @Charlieface suggestions, but unfortunately I still face the same issues

UPDATE 2: I replaced all the encryption logic with BouncyCastle Nuget and got rid of the issue. I did not have this problem prior to 8.0.3 so I only can assume it was because of the latest patch


Solution

  • I replaced all the encryption logic with BouncyCastle Nuget and got rid of the issue. I did not have this problem prior to 8.0.3 so I only can assume it was because of the latest patch

    Below is the only change I've made.

    public static async Task<byte[]> AESDecryptAndDecompressAsync(this byte[] cipher, byte[] key, byte[] iv)
    {
       if (cipher == null || cipher.Length <= 0)
          throw new ArgumentNullException(nameof(cipher), "incorrect data");
    
       if (key == null || key.Length != 32)
          throw new ArgumentNullException(nameof(key), "incorrect key");
    
       if (iv == null || iv.Length != 16)
          throw new ArgumentNullException(nameof(iv), "incorrect iv");
    
       var cipherEngine = new PaddedBufferedBlockCipher(new CbcBlockCipher(new AesEngine()), new Pkcs7Padding());
       cipherEngine.Init(false, new ParametersWithIV(new KeyParameter(key), iv));
    
       var decryptedBytes = new byte[cipherEngine.GetOutputSize(cipher.Length)];
       var len = cipherEngine.ProcessBytes(cipher, 0, cipher.Length, decryptedBytes, 0);
       len += cipherEngine.DoFinal(decryptedBytes, len);
    
       var result = new byte[len];
       Array.Copy(decryptedBytes, result, len);
    
       using var decompressedStream = new MemoryStream();
       using (var ms = new MemoryStream(result))
       {
          using (var gzipStream = new GZipStream(ms, CompressionMode.Decompress))
             await gzipStream.CopyToAsync(decompressedStream).ConfigureAwait(false);
       }
    
       return decompressedStream.ToArray();
    }