.NET下使用CryptoStream实现安全的TCP通信

作为一名软件工程师,经常需要一些工具来满足日常工作的需求。这些工具可能来自供应商,也可能需要自己手动编写,尤其是当需要定制工具时。最近,需要一个TCP客户端/服务器应用程序,服务器部分需要作为Windows服务运行,并且能够安全地接受客户端的连接,同时实现多种功能。

这个话题并不新鲜。网络上有多少TCP客户端/服务器的示例?数不胜数!由于已经有了TcpListener和TcpClient,所以实现起来相当容易。一旦抓住了思路,就能在不到一个小时内想出一个回声服务器的原型(有经验的程序员可能在几分钟内就能做到),但这永远不会被使用。为什么?不是因为它缺乏功能,而是因为缺乏安全性。是的,像Telnet这样的程序不是一个选项,特别是当在生产服务器上运行它们时。

所以,选择了一个显而易见的选择:使用.NET的CryptoStream进行加密。或者,做了吗?:)

一旦弄清楚了如何使用ICryptoTransform操作,.NET的CryptoStream就很容易使用,这个操作将与CryptoStream一起使用,即加密对称密钥在读取或写入模式中的任一种。但是,发现CryptoStream有一个长期存在的问题:作为一个面向对象框架中的对象,它继承了其他“正常”流的属性和方法,如.Peek、.Seek、.Length和.EndOfStream。敢于使用其中任何一个,就会得到一个痛苦的异常,告诉CryptoStream不支持这些方法/属性!

这是因为CryptoStream在其工作层次上高于Socket。它建立在TcpClient连接上创建的NetworkStream之上。可以在Google上搜索这个确切的短语“cryptostream does not support seek”,然后浏览前几个结果。大多数人建议在加密/解密之前记录数据的长度,将数据的长度作为数据本身的前几个字节发送。

这实际上有效,但为什么.Read操作除非发送方在其端关闭连接,否则无法正常完成?当翻译长度字节,然后尝试读取现在预定的字节计数CryptoStream.Read似乎永远在工作,然后最终抛出一个超时异常。一个使用长度字节技术的示例,浪费了时间阅读它的代码,而它“发明”了一个自己的TCP消息对象,只是为了发现它最终在发送方关闭连接,以便在接收方强制读取。可笑的是。是的,关闭发送方的流会自动向接收方发出信号,读取队列中剩余的所有字节,从而解决了CryptoStream.Read问题。这只创建了单向加密通信;绝对不是网络设计的方式。

有人抱怨同样的问题,注意到数据实际上可以在不关闭连接的情况下发送,只是如果.Write函数提交两次。对这个观察的回复——尽管是悲观的——是什么让去做长期以来一直避免做的事情:实时计算发送和读取的字节数。猜猜看?

幸运数字32(不是slevan:D)

就像在测试原型代码时,使用了一个777字节的“密码”(哎呀,安全漏洞:)),一些标志字节,其余的是随机生成的字节,试图将数据推到线上。一次写入这些字节,每次测试时,发现读取发送的随机字节在索引号718处停止,这将是字节号719!当每次都得到相同的结果时,计算了总字节数,它是1504。冲进了许多结论,抱歉,但结果要简单得多。由于使用的是Rijndael算法,并指定了ICrytoTransform使用的Rijndael提供程序的.BlockSize为最大允许的256位=32字节,CryptoStream一直在读取.BlockSize的数据。

VB.NET

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函数的行为方式给出确切的解释。

所以,终于能够进行2-Way CryptoStream通信,确切地知道发送的数据何时会被接收方实际读取。最后:)

文章代码

为这篇文章制作的代码分为两个应用程序:dataSender.exe和dataReceiver.exe。使用这些演示应用程序非常简单,启动两个应用程序,在两个应用程序的文本框中定义将要写入/读取的数据字节数,然后开始发送字节。为了确保交付一个非32倍数的数据块,只需在计数中添加额外的64字节,就完成了:)不唠叨!为了帮助在网络中使用相同的代码,包含了一个随机字节生成器应用程序,它将在与它运行的同一目录中生成文本行到文件中,每一行将以(Chr(i) + Chr(i) + ...)的形式,对于32个连接的字符的计数。拿走它们,将它们粘贴到代码中适当的注释行中,并进行必要的更改。

代码也大量注释。只要小心,注释总是在代码之后,而不是通常的相反。非常讨厌在代码之前显示注释;这是反逻辑的。大脑解析一行代码,然后要求解释,而不是相反!!还会找到代码友好:没有空白行会迫使重新解析整个块只是为了重新记住正在阅读的内容!!!!唯一的陷阱:这种技术需要高亮显示。所以,不要使用记事本阅读它。:D

可以在http://admincraft.net下载最新和更新的文章代码。

为了确保字节通过.NETCryptoStream与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
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485