令人惊叹的被遗忘的双向.NET Rijndael CryptoStream
如何进行精确的双向 Rijndael CryptoStream 通信

引言
每个系统工程师都需要工具来满足日常工作需求。这些工具不一定来自供应商,它们可以手工制作,尤其是在需要定制工具时。
我最近需要的一个工具是 TCP 客户端/服务器应用程序,其中服务器部分作为 Windows 服务运行,安全地接受来自客户端的连接,同时服务于多个目的。
嗯,这个主题本身并不新鲜。网上有多少 TCP 客户端/服务器的例子?不计其数!考虑到我手中已经有了 TcpListener
和 TcpClient
,这真的很简单。一旦我抓住了这个想法,我就能够在一个小时内搞定一个回声服务器(有经验的程序员可能几分钟就能完成 :)),但这永远不会被使用。为什么?不是因为它缺乏功能,而是因为它缺乏安全性。是的,像 Telnet 这样的程序不是选项,尤其是在生产服务器上运行时。
所以,我选择了显而易见的选项:使用 .NET 的 CryptoStream
进行加密。或者,我做了吗?:)
背景
.NET 的 CryptoStream
在你弄清楚如何进行 ICryptoTransform
操作后就可以轻松使用,该操作将与 CryptoStream
一起使用,即对称密钥加密,无论是在读取还是写入模式下。但是,CryptoStream
有一个顽固的问题,我作为一个加密新手痛苦地发现了这一点:CryptoStream
最终无法区分 TCP 消息或数据块何时结束(至少网上所有人都这么说,我稍后会讲到 ;))。是的,作为一个面向对象的框架中的对象,它继承了其他“普通”流的属性和方法,如 .Peek
、.Seek
、.Length
和 .EndOfStream
。如果你敢使用这些方法中的任何一个,你就会收到一个痛苦的异常,告诉你 CryptoStream
不支持这些方法/属性!
这是因为 CryptoStream
的工作级别比 Socket 高。它建立在 TcpClient
连接上创建的 NetworkStream
之上。你可以谷歌搜索这个确切的短语“cryptostream
does not support seek”,并浏览前几条结果。大多数建议是在加密/解密之前和之后进行字节计数,通过在数据本身之前记录发送数据量的前几个字节来实现。
这实际上是有效的,但为什么 .Read
操作除非发送方关闭连接,否则就无法正常完成?当你转换了长度字节然后尝试读取预先确定的字节计数时,CryptoStream.Read
似乎一直在工作,直到最终抛出超时异常。有一个例子使用了这种长度字节技术,我浪费时间阅读它的代码,因为它“发明”了一个自己的 TCP 消息对象,结果发现它最终关闭了发送方的连接,以便强制接收方读取。太搞笑了。是的,关闭发送方的流会自动向接收方发出信号,读取队列中剩余的所有字节,从而解决了 CryptoStream.Read
的问题。这只创建了单向加密通信;这绝对不是网络设计的工作方式。抱怨同样问题的人之一注意到,如果 .Write
函数提交两次,数据就可以在不关闭连接的情况下发送。对这个观察的回应——尽管很悲观——促使我做了我一直想做但又回避的事情:实时计算发送和读取的字节数。你猜怎么着?
幸运数字 32(不是 7:D)
在我测试原型代码时,我使用了一个 777 字节的“密码”(哎呀,安全漏洞 :)),一些标志字节,其余是随机生成的字节来尝试将数据推送到网络上。我一次写一个字节,每次测试时,我发现读取随机字节在索引号 718 处停止,也就是第 719 个字节!每次得到相同的结果时,我计算了总字节数,是 1504 字节。我仓促地做出了许多结论,很抱歉,但事实证明要简单得多。由于我使用的是 Rijndael 算法,并且指定了 ICrytoTransform
使用的 Rijndael 提供程序的 .BlockSize
为最大允许的 256 位 = 32 字节,CryptoStream
一直在以 .BlockSize
读取数据。
Dim x As New RijndaelManaged 'initialize the AES CryptoProvider
x.BlockSize = 256 'maximum length
x.KeySize = 256 'maximum length
x.Mode = CipherMode.CBC 'most secure cipher mode
x.Padding = PaddingMode.ISO10126 'most secure padding with random bytes
Dim key() As Byte = New Rfc2898DeriveBytes("keyPass", ASCII.GetBytes_
("saltsaltsalt".ToCharArray)).GetBytes(32) 'generate 32 bytes for key
Dim iv() As Byte = New Rfc2898DeriveBytes("ivCryptic", ASCII.GetBytes_
("moreSaltmoreSalt".ToCharArray)).GetBytes(32) 'generate 32 bytes for iv
这里的诀窍是,你必须*至少*附加一个额外的“虚拟块”,其大小等于 .BlockSize
,这样 ICryptoTransform
才能感知到有另一个块。如果我选择 128 位的 BlockSize,那么推送 .Read
操作所需的数据将是 16 字节,而不是。
Dim x As New RijndaelManaged 'initialize the AES CryptoProvider
x.BlockSize = 128 '16 bytes
x.KeySize = 128 '16 bytes, as it must match the BlockSize
x.Mode = CipherMode.CBC 'most secure cipher mode
x.Padding = PaddingMode.ISO10126 'most secure padding with random bytes
Dim key() As Byte = New Rfc2898DeriveBytes("keyPass", ASCII.GetBytes
("saltsaltsalt".ToCharArray)).GetBytes(16) 'generate 16 bytes for key
Dim iv() As Byte = New Rfc2898DeriveBytes("ivCryptic", ASCII.GetBytes
("moreSaltmoreSalt".ToCharArray)).GetBytes(16) 'generate 16 bytes for iv
不幸的是,关于 CryptoStream
的参考资料不多,我所拥有的都是网络资源,其中大多数都没有对 CryptoStream.Read
函数的行为方式给出确切的解释。
所以,我终于能够实现双向 CryptoStream
通信了,确切地知道发送的数据何时能在接收方实际读取。终于 :)
文章代码
我为这篇文章编写的代码分为两个应用程序:dataSender.exe 和 dataReceiver.exe。使用这些演示应用程序非常简单,启动两个应用程序,在两个应用程序的文本框中定义将写入/读取的字节数,然后开始发送字节。为了保证非 32 的倍数的数据块能够送达,只需额外添加 64 字节到计数即可,这样就完成了 :) 无需烦恼!为了帮助你在网络上使用相同的代码,我包含了一个随机字节生成器应用程序,它将在其运行的同一目录下生成文本行到文件中,每行将是 (Chr(i) + Chr(i) + ...) 的形式,用于连接 32 个字符。将这些文本复制粘贴到代码中适当的注释行内,并进行必要的更改。
代码也有大量的注释。请注意,注释总是跟在代码后面,而不是反过来。我非常讨厌出现在代码之前的注释;这不合逻辑。你的大脑解析一行代码,然后需要解释,而不是反过来!!你还会发现代码对眼睛友好:没有空白行,迫使你重新解析整个块才能记住你在读什么!!!!唯一的陷阱是:这项技术需要高亮显示。所以,不要用记事本来阅读它。:D
你可以在 http://admincraft.net 下载最新和更新的文章代码。
关注点
为了确保你的字节通过 .NET CryptoStream
和 Rijndael 算法成功传输,请在末尾附加额外的 .BlockSize
大小的虚拟字节块。如果你的数据长度除以 .BlockSize
的结果是小数,则计算你的最后一个 BlockSize
片段的数据长度,并附加额外的字节,直到计数等于 (BlockSize x 2)
crypto.Write(ASCII.GetBytes("done!".ToCharArray), 0, 5) 'send the word "done!",
'only 5 bytes
Dim y As New Random 'random bytes generator
Array.Resize(rwBuffer, 59) 'resize buffer to the remaining length of
'(.BlockSize x2)(i.e. 64 - 5 = 59 bytes remaining)
y.NextBytes(rwBuffer) 'fill buffer with random bytes
crypto.Write(rwBuffer, 0, rwBuffer.Length) 'write the extra
'remaining bytes to the wire
我还想补充一点,有些人坚持认为 .Flush
和 .FlushFinalBlock
这两个函数对接收方的 .Read
函数有“修复”作用。这是完全胡说八道,如果你使用我的演示应用程序,你肯定会知道 CryptoStream.Write
不需要也不需要任何刷新。传递给 .Write
函数的所有缓冲区数据会立即被写入。此外,如果——仅仅如果——.FlushFinalBlock
会对接收方的 .Read
函数产生任何影响——我极度怀疑这一点——它已经没有用了。为什么?仅仅因为它在 CryptoStream
的生命周期中只能被调用*一次*——*只一次*——之后你*必须*关闭流。否则?你会得到一个丑陋的异常,迫使你关闭它。所以,仍然是单向通信。:)
如果你喜欢,别忘了投票。:)