【问题标题】:Add IV to beginning of CryptoStream将 IV 添加到 CryptoStream 的开头
【发布时间】:2018-05-21 09:29:44
【问题描述】:

我正在现有文件管理程序中实施本地加密。

我能找到的大部分示例代码,例如Microsoft's,演示了如何直接写入文件,但我需要做的是提供一个流,以便在程序的其他地方使用:

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
}

我的问题在最后一行的评论中有一个概述:我如何将 IV 添加到 CryptoStream 的开头,以便在读取 CryptoStream 时返回第一个 X 字节,前提是控制何时真正开始读取流并写入文件超出了我的代码范围吗?

【问题讨论】:

  • 这是我的加密方法。当我无法控制流的使用时,问题是在 IV 之前。上面的代码(松散地)基于 MS 代码,主要区别在于我不能直接写入文件,因为我必须将对流的控制推迟到应用程序的其他处理进度条等内容的部分。
  • 两种模式都有效,这就是它们存在的原因。我正在使用读取模式,因为在这种情况下它是必要的。
  • @xanatos 不,它是未加密的流。作者希望在读取模式下提供一个CipherStream,它返回密文,而另一方控制如何/何时可以读取它。希望是因为它在同一个线程中。
  • @MaartenBodewes 现在很清楚了:-)

标签: c# encryption aes


【解决方案1】:

好的...现在您的问题已经很清楚了,这“相当”容易...遗憾的是 .NET 不包含用于合并两个 Stream 的类,但我们可以轻松创建它。 MergedStream 是一个只读、只进的多流合并。

你使用喜欢:

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

现在...当有人尝试读取MergedStream 时,首先会消耗包含IV 的MemoryStream,然后会消耗cryptoStream

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

【讨论】:

  • 对,我在答案的开头添加了这个作为答案,但请注意,问题中的函数返回CipherStream 而不仅仅是Stream,这意味着调用函数仍然必须重构。
  • @MaartenBodewes 对于Streams,部分读取是合法的。如果您尝试读取 100 个字节,则 Stream 可以为您提供 1 个字节。只有当 Read 返回 0 字节时,流才结束。
【解决方案2】:

使用CryptoStream 在两个本地方之间进行通信并不是一个好的设计。您应该改用通用的InputStream 或管道(用于进程间通信)。然后,您可以将 IV 的 MemoryStreamCryptoStream 组合起来并返回组合。请参阅answer of xanatos 了解如何执行此操作(如果需要,您可能仍需要填写Seek 功能)。

CryptoStream 永远只能处理密文。由于您需要在接收器处更改代码无论如何,如果您想解密,您不妨重构为InputStream


如果您需要保留当前设计,则可以使用 hack。首先使用ECB模式“解密”IV而不使用填充。当单个分组密码调用总是成功时,结果将是一个数据块 - 当使用 CipherStream 加密时 - 再次变成 IV。

步骤:

  1. 在数组中生成 16 个随机字节,这将是真正的 IV;
  2. 解密使用不带填充的 ECB 和用于CipherStream 的密钥的 16 字节 IV;
  3. 使用密钥和全零、16 字节 IV 初始化 CipherStream
  4. 使用CipherStream加密“解密”IV;
  5. 输入明文的其余部分。

您需要创建一个InputStream,它首先接收解密的IV(如MemoryStream),然后是明文(如FileStream),这才可行。同样,also see the answer of xanatos 如何做到这一点。或者参见例如 this combinerthis HugeStream 在好的 ol' StackOverflow 上。然后使用组合流作为CipherInputStream 的源。

但不用说,应该尽早记录并删除此类黑客行为。


注意事项:

  • 这个技巧在任何模式下都不起作用;它适用于 CBC 模式,但其他模式可能会以不同的方式使用 IV;
  • 请注意,OutputStream 通常对加密更有意义,但设计可能存在其他问题。

【讨论】:

    【解决方案3】:

    感谢那些花时间回答的人。最后我意识到我必须知道缓冲代码中的 IV 长度,没有办法绕过它,所以选择保持简单:

    加密方式(伪代码):

    /* 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 */
    

    解密方法(伪代码):

    /* 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 */
    

    【讨论】:

    • 我刚刚向您展示了一种解决方法。然后你说这个答案是不可能的。然后通过展示执行文件加密的一般方法来解决您自己的问题。然后您仍然没有使用输出流进行加密或使用输入流进行解密
    • 您的代码也不正确,读取可能不会返回所有可用字节,最后chunk 缓冲区肯定不会正确(除非文件包含正好 N * 块大小以字节为单位)。跨度>
    • 感谢您的输入,将我的代码清楚地标记为伪代码,它可以工作,并且可以完成我需要它做的事情。我把它贴在这里是为了帮助任何追随我的人。不必太吝啬。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-28
    • 2015-09-07
    • 2019-10-06
    • 1970-01-01
    相关资源
    最近更新 更多