-3

I was trying to decrypt a JSON request with a key, hex and textbody but all the codes I found are from 2013 - 2017. I tried using this one

namespace AES {
    using System.IO;
    using System;
    using System.Security.Cryptography;

    class Program {

        public static byte[] FromBase64Url(string base64Url) {
            // Replace '-' with '+' and '_' with '/' for standard Base64
            string base64 = base64Url.Replace('-', '+').Replace('_', '/');

            // Add padding if necessary. Base64 strings should have a length that is a multiple of 4.
            /**/
            int padding = 3 - ((base64.Length + 3) % 4);
            if (padding > 0) {
                base64 += new string('=', padding);
            }
            //*/
            // Convert the Base64 string to a byte array
            return Convert.FromBase64String(base64);
        }

        static void Main(string[] args) {
            var mode = "CFB";
            var message = "HgnDCsJPaosJkvCF8jqPHUdSOVtCO2GeVQWiBCV9ocDRDM2EGUzNYRXy";
            byte[] encrypted;
            var plaintext = "";

            //if (args.Length >0) mode=args[0];
            if (args.Length > 1) message = args[1];

            try {

                using
                var aes = Aes.Create();

                aes.Mode = CipherMode.CFB;

                if (mode == "CBC") aes.Mode = CipherMode.CBC;

                else if (mode == "CFB") aes.Mode = CipherMode.CFB;
                else if (mode == "CTS") aes.Mode = CipherMode.CTS;
                else if (mode == "ECB") aes.Mode = CipherMode.ECB;
                else if (mode == "OFB") aes.Mode = CipherMode.OFB;

                byte[] temp_iv = Convert.FromHexString("76396c566a476c4c50694e376469487a"); // Convert IV from hex to bytes
                byte[] temp_key = System.Text.Encoding.UTF8.GetBytes("ei9dzmx9f3l2CdFliYMb7iwJ7d0ce58d");
                byte[] temp_message = FromBase64Url(message);

                aes.Key = temp_key;
                aes.IV = temp_iv;
                //aes.Padding = PaddingMode.PKCS7;
                aes.Padding = PaddingMode.None;

                ICryptoTransform encryptor = aes.CreateEncryptor();
                ICryptoTransform decryptor = aes.CreateDecryptor();

                using(MemoryStream msEncrypt = new MemoryStream()) {
                    using(CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) {
                        using(StreamWriter swEncrypt = new StreamWriter(csEncrypt)) {
                            swEncrypt.Write(message);
                        }
                        encrypted = msEncrypt.ToArray();
                        msEncrypt.Close();
                        csEncrypt.Close();
                    }
                }

                using(MemoryStream msDecrypt = new MemoryStream(temp_message)) {
                    using(CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) {
                        using(StreamReader plainTextReader = new StreamReader(csDecrypt)) {
                            plaintext = plainTextReader.ReadToEnd();

                            msDecrypt.Close();
                            csDecrypt.Close();
                        }

                    }
                }

                string str1 = "Message: " + message;

                str1 = str1 + "\nIV " + BitConverter.ToString(aes.IV);
                str1 = str1 + "\nKey " + BitConverter.ToString(aes.Key);
                str1 = str1 + "\nMode: " + mode;

                str1 = str1 + "\n\nCipher: " + BitConverter.ToString(encrypted).Replace("-", string.Empty);
                str1 = str1 + "\nCipher: " + Convert.ToBase64String(encrypted);
                str1 = str1 + "\nDecrypted Byte Code: " + BitConverter.ToString(temp_message);
                str1 = str1 + "\nDecrypted: " + plaintext;

                Console.WriteLine("{0}", str1);

            } catch(Exception e) {
                Console.WriteLine("Error: {0}", e);
            }

        }
    }
}

The var message is an encrypted JSON data, I've provided the value there so anyone can run it themselves. It is a Base64String.

This returned Padding is Invalid Error when I didn't set the padding and also returned gibberish when I did. I tried all the encodings and all the Algorithms to decrypt (It was encoded using CFB but just in case I covered all bases)

The Result: Gibberish Decryption from Cryptography C#

14
  • 5
    "TLDR don't use System.Security.Cryptography..." - yes, because the chances that a core library is fundamentally broken is so much higher than you made a mistake somewhere. Plenty of people have used System.Security.Cryptography without incident. Commented Apr 4 at 13:09
  • 1
    You have a simple typo. using (MemoryStream msDecrypt = new MemoryStream(temp_message)) should be using (MemoryStream msDecrypt = new MemoryStream(encrypted)). Make that change and everything works. I am voting to close as a typo Commented Apr 4 at 13:14
  • 1
    Extraordinary claims require extraordinary proof - if System.Security.Cryptography was broken, no web app would work. No HTTPS, no password encryption, no hashing. All .NET developers would know. This very question proves there's nothing wrong - SO is an ASP.NET Core site Commented Apr 4 at 13:20
  • 1
    The CFB mode is not implemented at all, incompletely or incorrectly in almost every .NET version. On .NET 8 (what you are using) the CFB variant CFB128 is supported (with this variant HgnDCs... was generated), but not correct, so that a workaround is required. CFB is a stream cipher mode that does not need padding if implemented correctly. However, the MS implementation requires padding to a multiple of the segment size, which for CFB128 is a full block. As a consequence, the ciphertext must be shortened accordingly after encryption. An alternative that works as intended is C#/BouncyCastle. Commented Apr 4 at 15:13
  • 1
    However, your question is slightly confusing. In the first part of the code, the (Base64 encoded) ciphertext (message = Hgn... ) appears to be encrypted a second time (encrypted), and in the second part the ciphertext seems to be decrypted (plaintext). It is not clear to me what the first part is supposed to do. Perhaps you are looking for a way to generate the ciphertext message = Hgn...? Commented Apr 4 at 15:18

2 Answers 2

3

The CFB mode requires the specification of an additional parameter, namely the segment size, which MS refers to as the feedback size. The segment size specifies the number of encrypted bits per encryption step, see CFB. The segment size is usually appended, e.g. CFB-8 or CFB-128 (CFB-128 is sometimes also referred to as CFB for short).

The CFB mode is not implemented consistently in the various .NET versions. Some versions do not support it at all and some incompletely. Where it is supported, it is not implemented correctly.

.NET 8 (the .NET version you are using) supports CFB-128 (the CFB variant with which the ciphertext HgnDCsJPaosJkvCF8jqPHUdSOVtCO2GeVQWiBCV9ocDRDM2EGUzNYRXy was generated, see CyberChef). However, the .NET implementation is flawed: CFB is a stream cipher mode, so that no padding is required if implemented correctly. In contrast, the MS implementation requires padding to a multiple of the segment size, which for CFB-128 corresponds to a full block (16 bytes for AES). Consequently, the ciphertext must be shortened by the number of padding bytes after encryption.
Since this padding is added before encryption and removed after encryption, meaning that the number of padding bytes is known, no PKCS#7 padding is required, instead a simple padding like zero padding is enough (in particular, it is not necessary to pad a ciphertext that already has the correct length, as PKCS#7 padding does).
Similarly for decryption: the ciphertext must be padded to the required length, and the padding bytes must be removed after decryption.

For the sake of completeness: .NET 8 also supports CFB-8, which is even the default. The segment size for CFB-8 is 8 bits, so that any plaintext length can be encrypted without padding.

CFB-128 encryption/decryption with the native .NET methods:

private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] plaintext)
{
    byte[] ciphertext = null;
    using (Aes aesAlg = Aes.Create())
    {
        aesAlg.Key = key;
        ciphertext = aesAlg.EncryptCfb(plaintext, iv, PaddingMode.Zeros, 128); // pad
    }
    int padding = ciphertext.Length - plaintext.Length;
    return ciphertext[..^padding]; // remove padding
}

private static byte[] AesDecrypt(byte[] key, byte[] iv, byte[] ciphertext)
{
    int paddingSize = (16 - ciphertext.Length % 16) % 16;
    byte[] ciphertextPadded = ciphertext;
    if (paddingSize != 0)
    {
        ciphertextPadded = new byte[ciphertext.Length + paddingSize]; // pad
        Buffer.BlockCopy(ciphertext, 0, ciphertextPadded, 0, ciphertext.Length);
    }           
    byte[] decrypted = null;
    using (Aes aesAlg = Aes.Create())
    {
        aesAlg.Key = key;
        decrypted = aesAlg.DecryptCfb(ciphertextPadded, iv, PaddingMode.None, 128); 
    }
    return decrypted[..^paddingSize]; // remove padding
}

With a correct CFB implementation, padding/unpadding is not necessary. For the .NET implementation, however, a missing padding/unpadding results in exceptions. If you use CFB-8 instead of CFB-128, the padding/unpadding can be ommitted.


For comparison, here is the solution with BouncyCastle:

private static byte[] AesEncrypt_BC(byte[] key, byte[] iv, byte[] plaintext)
{
    var cipher = CipherUtilities.GetCipher("AES/CFB");
    cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv));
    return cipher.DoFinal(plaintext);
}

private static byte[] AesDecrypt_BC(byte[] key, byte[] iv, byte[] ciphertext)
{
    var cipher = CipherUtilities.GetCipher("AES/CFB");
    cipher.Init(false, new ParametersWithIV(new KeyParameter(key), iv));
    return cipher.DoFinal(ciphertext);
}

Here, no padding/unpadding is required, no matter how long the plaintext/ciphertext is, as it should be. Note that CFB means CFB-128, while CFB8 refers to CFB-8.


A solution with streams is:

private static byte[] AesEncrypt_streams(byte[] key, byte[] iv, byte[] plaintext)
{
    byte[] ciphertext = null;
    int padding = 0;
    using (Aes aesAlg = Aes.Create())
    {
        aesAlg.Padding = PaddingMode.Zeros; // must be specified, as PKCS#7 is the default // pad
        aesAlg.Mode = CipherMode.CFB; // must be specified, as CBC is the default
        aesAlg.FeedbackSize = 128; // must be specified, as CFB-8 is the default
        aesAlg.Key = key;
        aesAlg.IV = iv;
        ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
        using (MemoryStream msEncrypt = new MemoryStream())
        {
            using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
            {
                csEncrypt.Write(plaintext);
            }
            ciphertext = msEncrypt.ToArray();
        }
        padding = ciphertext.Length - plaintext.Length;
        return ciphertext[..^padding]; // remove padding
    }
}

private static byte[] AesDecrypt_streams(byte[] key, byte[] iv, byte[] ciphertext)
{
    int paddingSize = (16 - ciphertext.Length % 16) % 16;
    byte[] ciphertextPadded = ciphertext;
    if (paddingSize != 0)
    {
        ciphertextPadded = new byte[ciphertext.Length + paddingSize]; // pad
        Buffer.BlockCopy(ciphertext, 0, ciphertextPadded, 0, ciphertext.Length);
    }

    byte[] decrypted = null;
    using (Aes aesAlg = Aes.Create())
    {
        aesAlg.Padding = PaddingMode.None; // must be specified, as PKCS#7 is the default
        aesAlg.Mode = CipherMode.CFB; // must be specified, as CBC is the default
        aesAlg.FeedbackSize = 128; // must be specified, as CFB-8 is the default
        aesAlg.Key = key;
        aesAlg.IV = iv;
        ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
        using (MemoryStream msDecrypt = new MemoryStream(ciphertextPadded))
        {
            using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
            {
                using (MemoryStream msDecryptForCpy = new MemoryStream())
                {
                    csDecrypt.CopyTo(msDecryptForCpy);
                    decrypted = msDecryptForCpy.ToArray();
                }
            }
        }
        return decrypted[..^paddingSize]; // remove padding
    }
}

Test:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;

...

byte[] plaintext = Encoding.UTF8.GetBytes("{\"business_id\":\"3292aecc-886a-45e3-aa55-d3");
byte[] iv = Convert.FromHexString("76396c566a476c4c50694e376469487a");
byte[] key = Encoding.UTF8.GetBytes("ei9dzmx9f3l2CdFliYMb7iwJ7d0ce58d");

// native C# methods
byte[] ciphertext = AesEncrypt(key, iv, plaintext);
Console.WriteLine(Convert.ToBase64String(ciphertext));
byte[] decrypted = AesDecrypt(key, iv, ciphertext);
Console.WriteLine(Encoding.UTF8.GetString(decrypted));

// C#/BouncyCastle methods
byte[] ciphertext_BC = AesEncrypt_BC(key, iv, plaintext);
Console.WriteLine(Convert.ToBase64String(ciphertext_BC));
byte[] decrypted_BC = AesDecrypt_BC(key, iv, ciphertext_BC);
Console.WriteLine(Encoding.UTF8.GetString(decrypted_BC));

// native C# methods (streams)
byte[] ciphertext_streams = AesEncrypt_streams(key, iv, plaintext);
Console.WriteLine(Convert.ToBase64String(ciphertext_streams));
byte[] decrypted_streams = AesDecrypt_streams(key, iv, ciphertext_streams);
Console.WriteLine(Encoding.UTF8.GetString(decrypted_streams));

Output:

HgnDCsJPaosJkvCF8jqPHUdSOVtCO2GeVQWiBCV9ocDRDM2EGUzNYRXy
{"business_id":"3292aecc-886a-45e3-aa55-d3
HgnDCsJPaosJkvCF8jqPHUdSOVtCO2GeVQWiBCV9ocDRDM2EGUzNYRXy
{"business_id":"3292aecc-886a-45e3-aa55-d3
HgnDCsJPaosJkvCF8jqPHUdSOVtCO2GeVQWiBCV9ocDRDM2EGUzNYRXy
{"business_id":"3292aecc-886a-45e3-aa55-d3
Sign up to request clarification or add additional context in comments.

1 Comment

So the byte array of the data to be decrypted just needs to be the correct length aka a multiple of 16 regardless of if data is populated or not, at least for CFB 128. Got it. The FeedBackSize which specifies the CFB Block Size needs to be specified as well. Why CFB-128 is not the default is beyond me. Anyway Thanks a lot for your help. Now I understand the process more also your explanation was very detailed and clear. I wish more people were like you. Thank you again. Have a wonderful day.
0

Here’s a modern alternative with authenticated encryption, using LibSodium.Net

AES is a great cipher, but when used on its own (e.g. CFB, CBC...), it’s just encryption — not authentication. That means if the ciphertext is tampered with, decryption might still succeed and give garbage, silently.

If you're just trying to encrypt a JSON string, or stream a file securely, without dealing with modes, padding, segment sizes or feedback size quirks — here's how to do it with LibSodium.Net, a modern wrapper for libsodium.


Authenticated encryption with SecretBox (XSalsa20 + Poly1305)

var json = Encoding.UTF8.GetBytes("{\"hello\":\"world\"}");
var key = RandomGenerator.GetBytes(SecretBox.KeyLen);

var ciphertext = new byte[SecretBox.NonceLen + SecretBox.MacLen + json.Length];
SecretBox.Encrypt(ciphertext, json, key); // Nonce is auto-generated and prepended

var decrypted = new byte[json.Length];
SecretBox.Decrypt(decrypted, ciphertext, key);

Console.WriteLine(Encoding.UTF8.GetString(decrypted));
  • No AES modes
  • No padding
  • Authenticated (tampering is detected)

AEAD encryption with Aegis256 (for the paranoid)

var json = Encoding.UTF8.GetBytes("{\"business_id\":\"3292aecc-886a-45e3-aa55-d3\"}");
var key = RandomGenerator.GetBytes(Aegis256.KeyLen);

var ciphertext = new byte[Aegis256.NonceLen + Aegis256.MacLen + json.Length];
Aegis256.Encrypt(ciphertext, json, key);

var decrypted = new byte[json.Length];
Aegis256.Decrypt(decrypted, ciphertext, key);

Console.WriteLine(Encoding.UTF8.GetString(decrypted));

AEGIS-256 is AEAD (Authenticated Encryption with Associated Data), and very fast.


AEAD stream encryption with SecretStream

using var input = File.OpenRead("data.json");
using var encrypted = File.Create("data.json.enc");
var key = RandomGenerator.GetBytes(32);

SecretStream.Encrypt(input, encrypted, key);

// Decrypt
using var encryptedInput = File.OpenRead("data.json.enc");
using var output = File.Create("data_restored.json");
SecretStream.Decrypt(encryptedInput, output, key);

Works with any Stream. Handles chunking, authentication, framing — all under the hood.


No PaddingMode, no FeedbackSize, no CBC/CFB/ECB flags. Just solid crypto, with clean code.

Full guide: https://libsodium.net/guide Disclaimer: I'm the author of LibSodium.Net.

1 Comment

I'll keep this in mind for when I'm encrypting data to send but currently I'm receiving the data from a server to decrypt and I have no control on how they encrypt it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.