technophebe
technophebe

Reputation: 494

Add IV to beginning of CryptoStream

I'm implementing local encryption within an existing file management program.

Much of the example code I can find, such as Microsoft's, demonstrates how to write directly to a file, but what I need to do is provide a stream that is consumed elsewhere in the program:

CryptoStream GetEncryptStream(string filename)
{
    var rjndl = new RijndaelManaged();
    rjndl.KeySize = 256;
    rjndl.BlockSize = 256;
    rjndl.Mode = CipherMode.CBC;
    rjndl.Padding = PaddingMode.PKCS7;

    // Open read stream of unencrypted source fileStream:
    var fileStream = new FileStream(filename, FileMode.Open); 

    /* Get key and iv */

    var transform = rjndl.CreateEncryptor(key, iv);

    // CryptoStream in *read* mode:
    var cryptoStream = new CryptoStream(fileStream, transform, CryptoStreamMode.Read); 

    /* What can I do here to insert the unencrypted IV at the start of the
       stream so that the first X bytes returned by cryptoStream.Read are
       the IV, before the bytes of the encrypted file are returned? */

    return cryptoStream; // Return CryptoStream to be consumed elsewhere
}

My issue is outlined in the comment on the last line but one: how can I add the IV to the start of the CryptoStream such that it will be the first X bytes returned when the CryptoStream is read, given that control of when to actually start reading the stream and writing to a file is outside the scope of my code?

Upvotes: 3

Views: 1428

Answers (3)

xanatos
xanatos

Reputation: 111890

Ok... now that your problem is clear, it is "quite" easy... Sadly .NET doesn't include a class to merge two Stream, but we can easily create it. The MergedStream is a read-only, forward-only multi-Stream merger.

You use like:

var mergedStream = new MergedStream(new Stream[] 
{
    new MemoryStream(iv),
    cryptoStream,
});

Now... When someone tries to read from the MergedStream, first the MemoryStream containing the IV will be consumed, then the cryptoStream will be consumed.

public class MergedStream : Stream
{
    private Stream[] streams;
    private int position = 0;
    private int currentStream = 0;

    public MergedStream(Stream[] streams) => this.streams = streams;
    
    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => streams.Sum(s => s.Length);

    public override long Position 
    { 
        get => position; 
        set => throw new NotSupportedException();
    }

    public override void Flush()
    {
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (streams == null)
        {
            throw new ObjectDisposedException(nameof(MergedStream));
        }

        if (currentStream >= streams.Length)
        {
            return 0;
        }

        int read;

        while (true)
        {
            read = streams[currentStream].Read(buffer, offset, count);
            position += read;

            if (read != 0)
            {
                break;
            }

            currentStream++;

            if (currentStream == streams.Length)
            {
                break;
            }
        }

        return read;
    }

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    protected override void Dispose(bool disposing)
    {
        try
        {
            if (disposing && streams != null)
            {
                for (int i = 0; i < streams.Length; i++)
                {
                    streams[i].Close();
                }
            }
        }
        finally
        {
            streams = null;
        }
    }
}

Upvotes: 5

technophebe
technophebe

Reputation: 494

Thanks for those who've taken the time to answer. In the end I realized I have to have knowledge of the IV length in the buffering code, there's no way around it, so elected to keep it simple:

Encryption method (Pseudo-code):

/* Get key and IV */

outFileStream.Write(IV); // Write IV to output stream

var transform = rijndaelManaged.CreateEncryptor(key, iv);

// CryptoStream in read mode:
var cryptoStream = new CryptoStream(inFileStream, transform, CryptoStreamMode.Read);

do
{
    cryptoStream.Read(chunk, 0, blockSize); // Get and encrypt chunk
    outFileStream.Write(chunk);             // Write chunk
}
while (chunk.Length > 0)

/* Cleanup */

Decryption method (Pseudo-code):

/* Get key */

var iv = inFileStream.Read(ivLength); // Get IV from input stream

var transform = rijndaelManaged.CreateDecryptor(key, iv);

// CryptoStream in write mode:
var cryptoStream = new CryptoStream(outFileStream, transform, CryptoStreamMode.Write);

do
{
    inFileStream.Read(chunk, 0, blockSize); // Get chunk
    cryptoStream.Write(chunk);              // Decrypt and write chunk
}
while (chunk.Length > 0)

/* Cleanup */

Upvotes: -1

Maarten Bodewes
Maarten Bodewes

Reputation: 94018

It is not a good design to use a CryptoStream to communicate between two local parties. You should use a generic InputStream or pipe (for inter-process communication) instead. Then you can combine a MemoryStream for the IV and a CryptoStream and return the combination. See the answer of xanatos on how to do this (you may still need to fill in the Seek functionality if that's required).

A CryptoStream will only ever be able to handle ciphertext. As you need to change the code at the receiver anyway if you'd want to decrypt you might as well refactor to InputStream.


If you're required to keep the current design then there is a hack available. First "decrypt" the IV using ECB mode without padding. As a single block cipher call always succeeds the result will be a block of data that - when encrypted using the CipherStream - turns into the IV again.

Steps:

  1. generate 16 random bytes in an array, this will be the real IV;
  2. decrypt the 16 byte IV using ECB without padding and the key used for the CipherStream;
  3. initialize the CipherStream using the key and an all zero, 16 byte IV;
  4. encrypt the "decrypted" IV using the CipherStream;
  5. input the rest of the plaintext.

You will need to create an InputStream that first receives the decrypted IV (as MemoryStream) and then the plaintext (as FileStream) for this to be feasible. Again, also see the answer of xanatos on how to do this. Or see for instance this combiner and this HugeStream on good ol' StackOverflow. Then use the combined stream as source for the CipherInputStream.

But needless to say hacks like these should be well documented and removed at the earliest convenience.


Notes:

  • This trick won't work on any mode; it works for CBC mode, but other modes may use the IV differently;
  • Note that an OutputStream would generally make more sense for encryption, there may be other things wrong with the design.

Upvotes: 2

Related Questions