65.9K
CodeProject 正在变化。 阅读更多。
Home

令人惊叹的被遗忘的双向.NET Rijndael CryptoStream

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2009年8月21日

GPL3

7分钟阅读

viewsIcon

52822

downloadIcon

469

如何进行精确的双向 Rijndael CryptoStream 通信

2W

引言

每个系统工程师都需要工具来满足日常工作需求。这些工具不一定来自供应商,它们可以手工制作,尤其是在需要定制工具时。

我最近需要的一个工具是 TCP 客户端/服务器应用程序,其中服务器部分作为 Windows 服务运行,安全地接受来自客户端的连接,同时服务于多个目的。

嗯,这个主题本身并不新鲜。网上有多少 TCP 客户端/服务器的例子?不计其数!考虑到我手中已经有了 TcpListenerTcpClient,这真的很简单。一旦我抓住了这个想法,我就能够在一个小时内搞定一个回声服务器(有经验的程序员可能几分钟就能完成 :)),但这永远不会被使用。为什么?不是因为它缺乏功能,而是因为它缺乏安全性。是的,像 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.exedataReceiver.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 的生命周期中只能被调用*一次*——*只一次*——之后你*必须*关闭流。否则?你会得到一个丑陋的异常,迫使你关闭它。所以,仍然是单向通信。:)

如果你喜欢,别忘了投票。:)

© . All rights reserved.