0

TLDR: Client encrypts image, sends it to server, server tries to decrypt and throws exception but still saves 98% of the image normally with some junk appearing on the bottom-right part of the image.

Important Notes:

  1. Key and IV are static at the moment for testing purposes but even when I use my randomized ones the rest of the messages work 100% of the time
  2. I encrypt the whole byte array, split it up and send it, join the pieces in the server and then try to decrypt the image.
  3. I've tried using Write mode on Decryption, there all messages fail

So this is a bit of a baffling problem I've tried to solve the past few days. I have a client and a server communicating via UDP and I encrypt/decrypt the data for security purposes. It all works fine for all the messages I send/receive except for Images (binary data). At first I had some encoding being done but I since removed that because I found out that it can alter the binary data and produce junk. I converted both my Encrypt and Decrypt functions to accept and return only byte[] so the image never changes format. So I encrypt the Image, send it over to the server, the server tries to decrypt the image and immediately throws an exception. Funnily, if I catch that exception and the function returns, the image is written normally with the difference of a weird line at the bottom-right corner that usually has different colors (its always at the bottom right so its probably junk data from the encryption). So it seems that the decryption works until the very end and then it fails leaving the extra bytes at the end or something along those lines ? (I tried encrypting on client and not decrypting on the server to see if the encryption was maybe failing but that produced complete junk when written to disk, so encryption should be working)

I don't think I forgot any other information but please do let me know if you'd like to know something more, anyway here is the code:

CLIENT SIDE Encryption

public static byte[] Encrypt(byte[] plainText, byte[] iv)
{
    byte[] encrypted;
    
    // Create a new AesManaged.    
    using (AesManaged aes = new AesManaged())
    {
        // Create encryptor    
        aes.Padding = PaddingMode.PKCS7;

        aes.Key = new byte[16];
        aes.IV = new byte[16];

        aes.Mode = CipherMode.CBC;

        //aes.Key = StringToBytes(pinNumber);
        //aes.IV = iv;
        ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

        // Create MemoryStream    
        using (MemoryStream ms = new MemoryStream())
        {
            // Create crypto stream using the CryptoStream class. This class is the key to encryption    
            // and encrypts and decrypts data from any given stream. In this case, we will pass a memory stream    
            // to encrypt    
            using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
            {
                cs.Write(plainText, 0, plainText.Length);
            }
            encrypted = ms.ToArray();
        }
    }
    // Return encrypted data    
    return encrypted;
}

Client Side Image

int counter = 0;
    byte[] iv = SecureHelper.GenerateIV();
    Debug.Log("Phone Unencrypted: " + pngBytes.Length);
    byte[] encrypted = SecureHelper.Encrypt(pngBytes, iv);
    Debug.Log("Phone Encrypted: " + encrypted.Length);

    MessageSender.SendImageLength(encrypted.Length, WebcamUI.Singleton.scanToggle.isOn); // If the toggle is turned on, the method will return true and
                                                                                        // a scan will take place
    byte[][] imageByteArrays = new byte[(encrypted.Length / Message.MaxMessageSize) + 1][];

    for (int i = 0; i < imageByteArrays.Length; i++)
    {
        if (counter < encrypted.Length)
        {
            imageByteArrays[i] = encrypted.Skip(counter).Take(1224).ToArray();
            MessageSender.SendImage(imageByteArrays[i], i, iv);

            counter += 1224;
        }
        prefabSlider.value = (counter * 100f) / encrypted.Length;
        yield return new WaitForEndOfFrame();
    }

Server Side Decryption:

public static byte[] Decrypt(ushort fromClientId, byte[] cipherText, byte[] iv)
{
    byte[] plaintext = new byte[cipherText.Length];
    int readBytes = 0;
    try
    {
        using (AesManaged aes = new AesManaged())
        {
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;

            aes.Key = new byte[16];
            aes.IV = new byte[16];

            //aes.Key = StringToBytes(GetPin(fromClientId));
            //aes.IV = iv;

            // Create a decryptor    
            ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
            // Create the streams used for decryption.    
            using (MemoryStream ms = new MemoryStream(cipherText))
            {
                using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                {
                    // Read crypto stream    
                    readBytes = cs.Read(plaintext, 0, plaintext.Length);
                }
                plaintext = plaintext.Take(readBytes).ToArray();
                Debug.Log("Did take");
            }
        }
    }
    catch (Exception e)
    {
        Debug.LogError("Errot at SecureHelper(Decrypt) " + e.Message + "\n" + e.StackTrace);
    }
    // Create AesManaged    
    
    return plaintext;
}

Server Side Image:

public void ReconstructAndWritePicture(string path, string currentCompany, ushort fromClientId)
{
    string fullPath = path + "/" + currentCompany + "/";
    Debug.Log("Size: " + imgLength);
    int counter = 0;
    byte[] wholeImage = new byte[imgLength];

    for (int i = 0; i < imgParts.Length; i++)
    {
        for (int j = 0; j < imgParts[i].Length; j++)
        {
            wholeImage[j + counter] = imgParts[i][j];
        }
        counter += imgParts[i].Length;
    }
    Debug.Log("Phone Encrypted: " + wholeImage.Length);

    byte[] img = SecureHelper.Decrypt(fromClientId, wholeImage, iv);
    if (img == null)
    {
        return;
    }
    Debug.Log("Phone Unencrypted: " + img.Length);

    string imgName = currentCompany + Companies.Singleton.samplesPerCompany[currentCompany].ToString() + ".jpg";
    FileInfo file = new FileInfo(fullPath);
    if (!file.Directory.Exists)
    {
        file.Directory.Create();
    }

    File.WriteAllBytes(fullPath + imgName, img);

Thank you in advance for taking the time to check this out!

UPDATE:

I copy pasted the Decrypt method from the server to the client and it worked flawlessly. So now I think I should look into whether all the data is sent from the Server to the Client and if I am piecing it back together correctly (but for unencrypted images it worked so that's weird).

MikeyMike
  • 1
  • 2
  • 1
    Your problem is quite likely related to your incorrect usage of the Stream.Read method. (See here for a similar issue regarding Stream.Read in quite similar circumstances: https://stackoverflow.com/a/73530782/19858830). There might be perhaps more issues in your code (like maybe the cipherText data passed into the Decrypt method not being whole, or something else), but the incorrect usage of Stream.Read immediately caught my eye... –  Sep 29 '22 at 10:53
  • 1
    I would highly recommend setting up a unit test. Generate some random data, encrypt and decrypt said data, check that the decrypted data is *identical*. That way you can debug the encryption/decryption code separate from anything that has to do with images, and probably do that much faster than checking if an image looks correct. – JonasH Sep 29 '22 at 11:03
  • 1
    To decrypt the data, I would create a memory stream for the target, and use `Stream.CopyTo`, that should avoid potential problems reading lengths etc. See [convert stream into byte[]](https://stackoverflow.com/questions/1080442/how-do-i-convert-a-stream-into-a-byte-in-c) – JonasH Sep 29 '22 at 11:19
  • Check the number of bytes transmitted against number of byte receive to see if you get the same number. If the number of bytes are different the error is not your encryption/decryption but the UDP transmission. – jdweng Sep 29 '22 at 11:46
  • @MySkullCaveIsADarkPlace The data should be complete because without encryption the image is saved fine, i am also printing the lengths and they match up in both sides. This usage of stream.read is the one that has produced the best results( found it here: https://stackoverflow.com/a/54475586/20090073) but I will try the link you sent me. – MikeyMike Sep 29 '22 at 12:09
  • @JonasH I am already sending 3 messages before I get to the image part and it works every time so I assume that it is working as expected and the problem lies somewhere with the decrypting (because 98% gets decrypted but the remaining 2% is depicted as junk). The sizes are the same between client and server as well. I will try what you mentioned in your following comment about Stream.CopyTo – MikeyMike Sep 29 '22 at 12:11
  • @MikeyMike, "_The data should be complete [...] This usage of stream.read is the one that has produced the best results_" Well, it's quite likely not. But if you insist and you like keep using Stream.Read incorrectly, well... good luck to you! ¯\\_(ツ)_/¯ –  Sep 29 '22 at 12:11
  • @MySkullCaveIsADarkPlace I am only mentioning it because the Image would be unreadable by Windows if the data was incomplete, it makes more sense that the decryption fails along the way (unless i'm mistaken, fairly new at encryption). Either way, I said I will try your way, thank you for taking the time to help – MikeyMike Sep 29 '22 at 12:14
  • "_because the Image would be unreadable by Windows if the data was incomplete_" No, that's an entirely wrong assumption... Cut a jpeg file data in half (i.e., truncate a jpeg file) and see what you get... –  Sep 29 '22 at 12:16
  • Which .NET version? If you are on .NET 6+, you may be affected by this [breaking change](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/partial-byte-reads-in-streams) which has changed the behavior of `Read()` and may return less data. [Here](https://stackoverflow.com/a/69911546/9014097) you can find solutions (`CopyTo()` is one approach). Even if you are not on .NET 6+, a fix would make the code more resilient. In any case you should analyze `Encyrpt()`/`Decrypt()` independently from the rest. If this works, you can look for the bug in the rest of the code. – Topaco Sep 29 '22 at 12:20
  • Sure, but if you write a unit test you 1) actually *confirm* that the encryption portion is broken. 2) can reproduce the problem much faster and easier. 3) Can verify the fix with a good degree of confidence. – JonasH Sep 29 '22 at 12:22
  • @Topaco, i find it quite interesting that despite Stream.Read's API specification never having guaranteed to read as much data as requested (even if the stream would not reach its end), Microsoft felt the need to point out the changed behavior of certain Stream.Read implementations despite the changed behavior still being fully in line with the long-established API specification for Stream.Read. Makes me wonder how much code is out there in the wild that is using Stream.Read improperly... –  Sep 29 '22 at 12:29
  • @Topaco I am on Net 4.0. I tried the link however but sadly now the decryption fails for all messages – MikeyMike Sep 29 '22 at 12:50
  • @MySkullCaveIsADarkPlace I also tried implementing your change but it gave me the exact same result, I implemented the fix that the user from the link you attached used to see if it was getting there but it is failing at cs.Read() already without getting to the if (readBytes < cipherText.Length). The error is the same "Padding is invalid ..." – MikeyMike Sep 29 '22 at 12:52
  • @MySkullCaveIsADarkPlace - The source code (e.g. from .NET Framework 4.8) confirms what is described as old behavior in *Breaking Changes in .NET 6* (i.e. returning as many bytes as specified). So it's rather a mistake in the specs. – Topaco Sep 29 '22 at 18:17
  • @Topaco, no. You got that backwards. The spec never guaranteed it, that's not a mistake in the spec. Not guaranteeing that Stream.Read is returning the requested amount of data does not mean that some Stream.Read implementation is forbidden to return the amount of requested data. It's just that the spec tells you, and always told you, to **not rely** on that Stream.Read will always return the requested amount of data even when the stream is not yet exhausted. That's quite different from what you seem to try claiming here... –  Sep 29 '22 at 20:07
  • @MySkullCaveIsADarkPlace - It seems to me that the `CryptoStream#Read()` implementation has been intentionally implemented to return the specified number of bytes. MS itself apparently sees it this way, otherwise why would a breaking change have been announced in .NET 6? If so, the documentation for `CryptoStream#Read()` in older versions is at least misleading. Why you refer the statement of a spec mistake generally to `Stream#Read()`, only you know. – Topaco Sep 29 '22 at 23:36
  • @Topaco `CryptoStream.Read()` documentation could perhaps be misinterpreted, yes. But it still is/was not wrong. The spec never was and still is not in conflict with the Read() behavior of CryptoStream and vice versa. There is no mistake, there was no mistake in the spec. –  Sep 30 '22 at 06:13
  • To come back to the actual issue: The `Encrypt()` and `Decrypt()` methods run fine on my machine with .NET Framework 4.0 (as opposed to .NET 6). Online it is the same ([here](https://dotnetfiddle.net/1zJZ4Y) for .NET Framework 4.7.2, [here](https://dotnetfiddle.net/IqAeVh) for .NET Core 6). You should check this in your environment for .NET Framework 4.0. If it also works, the issue is in other parts of your code. – Topaco Sep 30 '22 at 07:36
  • @MySkullCaveIsADarkPlace - Well, this discussion is going nowhere, your opinion seems to be that the `CryptoStream#Read()` specification adequately describes the implementation for versions < .NET 6, I don't see it that way. – Topaco Sep 30 '22 at 07:48
  • @Topaca, you get it backwards, again. The implementation has to adhere to the specification. It's not the specification that follows an implementation, it's the implementation that has to follow the specification. Talking about opinions, man, you got nerves... –  Sep 30 '22 at 09:10
  • @Topaco, why are you still assuming the spec is in error? This is bizarre. Both the old as well as the new implementations of Read() by CryptoStream adhere to the API spec and don't violate it. The Read() method is part of the stream API, it is not an CryptoStream-exclusive API defined/specified by CryptoStream. If you call stating this as being presumptuous or polemic, you got not only nerves, you got balls, i'll give you that... –  Sep 30 '22 at 12:13
  • @Topaco, is that a backhanded way of suggesting that i should rather accept your subjective view instead of what the actual specification says, or do i just interpret too much into it? I have to admit, it's not really convincing. I mean, if you really want to go to town with that rhetoric device: Is it bizarre to rule out a mistake in your argument one hundred percent as well? –  Sep 30 '22 at 12:43

0 Answers0