1

I want to encrypt some string through RijndaelManaged and get encrypted result. I want to do it with MemoryStream. But I get the empty string. I get the same problem if I use Rijndael class instead of RijndaelManaged. What I did wrong?

static string EncodeString(string text, byte[] key, byte[] iv) {
    RijndaelManaged alg = new RijndaelManaged();
    var encryptor = alg.CreateEncryptor(key, iv);
    string encodedString = null;
    using (var s = new MemoryStream())
    using (var cs = new CryptoStream(s, encryptor, CryptoStreamMode.Write))
    using (var sw = new StreamWriter(cs)) {
        // encrypt the string
        sw.Write(text);
        sw.Flush();
        cs.Flush();
        s.Flush();
        Console.WriteLine($"Stream position: {s.Position}"); // Oops... It is 0 still. Why?
        // get encrypted string
        var sr = new StreamReader(s);
        s.Position = 0;
        encodedString = sr.ReadToEnd(); // I get empty string here
    }
    return encodedString;
}

Then I use this method:

    RijndaelManaged alg = new RijndaelManaged();
    alg.GenerateKey();
    alg.GenerateIV();
    var encodedString = EncodeString("Hello, Dev!", alg.Key, alg.IV); // I get empty string. Why?
Heinzi
  • 167,459
  • 57
  • 363
  • 519
Andrey Bushman
  • 11,712
  • 17
  • 87
  • 182

1 Answers1

2

You have two issues here:


Problem 1: You try to read the result before it is ready. You need to close the StreamWriter first:

using (var s = new MemoryStream())
using (var cs = new CryptoStream(s, encryptor, CryptoStreamMode.Write))
using (var sw = new StreamWriter(cs)) {
    // encrypt the string
    sw.Write(text);
    Console.WriteLine(s.ToArray().Length); // prints 0
    sw.Close();
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

But why do I need this? Didn't you see all those Flush statements in my code? Yes, but Rijndael is a block cypher. It can only encrypt a block once it has read the full block (or you have told it that this was the final partial block). Flush allows further data to be written to the stream, so the encryptor cannot be sure that the block is complete.

You can solve this by explicitly telling the crypto stream that you are done sending input. The reference implementation does this by closing the StreamWriter (and, thus the CryptoStream) with a nested using statement. As soon as the CryptoStream is closed, it flushes the final block.

using (var s = new MemoryStream())
using (var cs = new CryptoStream(s, encryptor, CryptoStreamMode.Write))
{ 
    using (var sw = new StreamWriter(cs))
    {
        // encrypt the string
        sw.Write(text);
    }
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

Alternatively, as mentioned by Jimi in the comments, you can call FlushFinalBlock explicitly. In addition, you can skip the StreamWriter by explicitly converting your base string to a byte array:

using (var s = new MemoryStream())
using (var cs = new CryptoStream(s, encryptor, CryptoStreamMode.Write))
{
    cs.Write(Encoding.UTF8.GetBytes(text));
    cs.FlushFinalBlock();
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

Or, as mentioned by V.Lorz in the comments, you can just dispose the CryptoStream to call FlushFinalBlock implicitly:

using (var s = new MemoryStream())
{
    using (var cs = new CryptoStream(s, encryptor, CryptoStreamMode.Write))
    {
        cs.Write(Encoding.UTF8.GetBytes(text));
    }
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

Problem 2: You tried to read the result as a string. Encryption does not work on strings, it works on byte arrays. Thus, trying to read the result as an UTF-8 string will result in garbage.

Instead, you could, for example, use a Base64 representation of the resulting byte array:

return Convert.ToBase64String(s.ToArray());

Here are working fiddles of your code with all those fixes applied:

  1. With StreamWriter: https://dotnetfiddle.net/8kGI4N
  2. Without StreamWriter: https://dotnetfiddle.net/Nno0DF
Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • 1
    You don't even need a StreamWriter, just write to the MemoryStream. The code is missing `cs.FlushFinalBlock();` – Jimi May 16 '21 at 09:58
  • @Jimi: StreamWriter.Dispose closes the underlying CryptoStream. CryptoStream.Close calls Dispose, [which calls FlushFinalBlock if necessary](https://referencesource.microsoft.com/#mscorlib/system/security/cryptography/cryptostream.cs,701). – Heinzi May 16 '21 at 10:03
  • @Jimi: MemoryStream.Write does not accept a string, only a byte[], i.e., you'd have to do Encoding.GetBytes first. – Heinzi May 16 '21 at 10:04
  • @Jimi: I have added FlushFinalBlock as an alternative to my answer. – Heinzi May 16 '21 at 10:07
  • *...you'd have to do Encoding.GetBytes first*: that exactly. Explicit Encoding and explicit flushing, before `Convert.ToBase64String()`. – Jimi May 16 '21 at 10:07
  • No need for explicit flushing if the instances are scoped properly. using (var s = new MemoryStream()) { using (var cs = new CryptoStream(s, encryptor, CryptoStreamMode.Write)) cs.Write(System.Text.Encoding.UTF8.GetBytes(text)); return Convert.ToBase64String(s.ToArray()); } – V.Lorz May 16 '21 at 10:12
  • @Jimi: The StreamWriter should perform better, though, since you don't need to have the complete input byte array in memory - we are using a stream cipher, after all. I admit that this is a micro-optimization, though. – Heinzi May 16 '21 at 10:15
  • 1
    @V.Lorz: Good point, added. This analysis has become quite more detailed than expected. :-) – Heinzi May 16 '21 at 10:19
  • Well, I would rather get an exception related to wrong padding before I generate the final result. I also could no make my code pass if I don't specify the length of the data to be written by the CryptoStream. But, all right... simple strings rarely generate this kind of exception. Rerely. – Jimi May 16 '21 at 10:35