Intermittent Exception "Padding is invalid and cannot be removed." with ECDH AES

Bill Clark 20 Reputation points
2023-08-11T18:10:03.0133333+00:00

I have a windows client application that creates an encrypted connection to a windows server application, using ECDH AES.

Both applications are .Net Framework 4.8.1, and they are both using the same class (below) to encrypt and decrypt the data.

After the key exchange, there is a brief two-way authentication process, and then the client will start pushing encrypted data to the server. This may continue for a few minutes or for many hours.

Issue: At seemingly random moments (every 2-20 minutes...), the Decrypt method on the server will throw a "Padding is invalid and cannot be removed." exception.

If I ignore the exception, the Decrypt method returns decrypted data with padding (for the data segment that triggered the exception), and then it will work great until the next exception.

The data segments being encrypted and decrypted are quite variable in length, and can contain virtually anything. Repeating the same data feed will yield exceptions at different points (or doesn't fail at the same points), and I haven't been able to detect any consistent properties with data segments that yield exceptions.

My buffering / re-connection scheme between the client and server can handle the exceptions without any data loss, but I would love to resolve the need to frequently re-establish the encrypted connection.

Any thoughts on what could be causing these intermittent exceptions?

<Error DateTime="8/9/2023 11:33:13 AM" Message="Padding is invalid and cannot be removed." Source="System.Core">
    <StackTrace><![CDATA[   at System.Security.Cryptography.CapiSymmetricAlgorithm.DepadBlock(Byte[] block, Int32 offset, Int32 count)
   at System.Security.Cryptography.CapiSymmetricAlgorithm.TransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)
   at System.Security.Cryptography.CryptoStream.FlushFinalBlock()
   at System.Security.Cryptography.CryptoStream.Dispose(Boolean disposing)
   at System.IO.Stream.Close()
   at Speche.eScription.CaptionServer.AESwDH.Decrypt(Byte[] publicKey, Byte[] encryptedMessage, Byte[] iv) in AESwDH.cs:line 117]]></StackTrace>
</Error>

This is occurring at the end of the following using statement (final "}"). The plainText MemoryStream is alway populated (with the padding...) when the exception occurs.

using (var cryptoStream = new CryptoStream(plainText, decryptor, ryptoStreamMode.Write))
{
    try
    {
        cryptoStream.Write(encryptedMessage, 0, encryptedMessage.Length);
    }
    catch (Exception ex)
    {
        AppLog.LogException(ex);
    }
}

The full class:

using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;

namespace CATcast
{
    public class AESwDH : IDisposable
    {
        private Aes aes = null;
        private ECDiffieHellmanCng diffieHellman = null;

        private byte[] publicKey;

        public AESwDH()
        {
            this.aes = new AesCryptoServiceProvider();

            this.diffieHellman = new ECDiffieHellmanCng
            {
                KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash,
                HashAlgorithm = CngAlgorithm.Sha256
            };

            // This is the public key we will send to the other party
            this.publicKey = this.diffieHellman.PublicKey.ToByteArray();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.aes != null)
                    this.aes.Dispose();

                if (this.diffieHellman != null)
                    this.diffieHellman.Dispose();
            }
        }

        public byte[] PublicKey
        {
            get
            {
                return this.publicKey;
            }
        }

        public byte[] IV
        {
            get
            {
                return this.aes.IV;
            }
        }

        public byte[] Encrypt(byte[] publicKey, byte[] txtMessage)
        {
            byte[] encryptedMessage;
            var key = CngKey.Import(publicKey, CngKeyBlobFormat.EccPublicBlob);
            var derivedKey = this.diffieHellman.DeriveKeyMaterial(key); // "Common secret"

            this.aes.Key = derivedKey;

            using (var cipherText = new MemoryStream())
            {
                using (var encryptor = this.aes.CreateEncryptor())
                {
                    using (var cryptoStream = new CryptoStream(cipherText, encryptor, CryptoStreamMode.Write))
                    {
                        try
                        {
                            cryptoStream.Write(txtMessage, 0, txtMessage.Length);
                        }
                        catch(Exception ex)
                        {
                            Util.logException(ex);
                        }                        
                    }
                }

                encryptedMessage = cipherText.ToArray();
            }

            return encryptedMessage;
        }

        public byte[] Decrypt(byte[] publicKey, byte[] encryptedMessage, byte[] iv)
        {
            byte[] decryptedMessage;
            var key = CngKey.Import(publicKey, CngKeyBlobFormat.EccPublicBlob);
            var derivedKey = this.diffieHellman.DeriveKeyMaterial(key);

            this.aes.Key = derivedKey;
            this.aes.IV = iv;

            using (var plainText = new MemoryStream())
            {
                using (var decryptor = this.aes.CreateDecryptor())
                {
                    using (var cryptoStream = new CryptoStream(plainText, decryptor, CryptoStreamMode.Write))
                    {
                        try
                        {
                            cryptoStream.Write(encryptedMessage, 0, encryptedMessage.Length);
                        }
                        catch (Exception ex)
                        {
                            Util.logException(ex);
                        }                        
                    }
                }

                decryptedMessage = plainText.ToArray();
            }

            return decryptedMessage;
        }
    }
}

Developer technologies | .NET | Other
Developer technologies | C#
{count} votes

1 answer

Sort by: Most helpful
  1. Bill Clark 20 Reputation points
    2023-08-24T20:40:27.2066667+00:00

    Problem solved... I was missing the obvious. It wasn't really a problem with the encryption / decryption process. The problem was with the data being fed into the decryption method. On occasion, the server would receive multiple encryption packets, concatenated together. The concatenated packets would cause the decryption failures.

    Resolved by prefixing every encrypted segment with the segment length (4 byte int / Int32), so concatenated segments can be pulled apart and individually fed into the decryption method. Working like a charm.

    4 people found this answer helpful.

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.