Add IV to beginning of CryptoStream

独自空忆成欢 提交于 2019-12-04 18:24:40

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 => throw new NotImplementedException();

    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;
        }
    }
}

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.

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 */
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!