C# 网络加密与解密
数据在网络传输过程中的保密性是网络安全中重点要考虑的问题之一。由于通过网络传递数据是在不安全的信道上进行传输的,因此通信双方要想确保任何可能正在侦听的人无法理解通信的内容,而且希望确保接收方接收的信息没有在传输期间被任何人修改,最好的办法就是在传输数据前对数据进行加密,接收方接收到加密的数据后再进行解密处理,从而保证数据的安全性。
在.NET 库的System.Security.Cryptography 命名空间中,包含多种加密数据的类,涉及多种加密算法。加密方法主要分为两大类:对称加密和不对称加密。
1 对称加密
对称加密也称为私钥加密,采用私钥算法,加密和解密数据使用同一个密钥。由于具有密钥的任意一方都可以使用该密钥解密数据,因此必须保证该密钥不能被攻击者获取,否则就失去了加密的意义。
私钥算法以块为单位加密数据,一次加密一个数据块。因此对称加密支持数据流,是加密流数据的理想方式。
.NET 类库使用的私钥算法有RC2、DES、TripleDES 和Rijndael。这些算法通过加密将n字节的输入块转换为加密字节的输出块。如果要加密或解密字节序列,必须逐块进行。由于n很小(对于RC2、DES 和TripleDES 算法,n 的值为8 字节、16 字节或24 字节,默认值为16字节;对于Rijndael 算法,n 的值为32 字节),因此每次加密的块的大小必须大于n。实际上,一次读入的数据块是否符合私钥算法要求的块的大小,如果不符合应该如何填充使其符合要求等情况,.NET 类库提供的算法类本身会自动处理,编写程序时不需要考虑这些问题。
为了保证数据的安全,.NET 基类库中提供的私钥算法类使用称作密码块链(CBC,Cipher Block Chaining)的链模式,算法使用一个密钥和一个初始化向量(IV,Initialization Vector)对数据执行加密转换。密钥和初始化向量IV 一起决定如何加密数据,以及如何将数据解密为原始数据。通信双方都必须知道这个密钥和初始化向量才能够加密和解密数据。
为什么要使用初始化向量IV 呢?因为初始化向量是一个随机生成的字符集,使用它可以确保任何两个原始数据块都不会生成相同的加密后的数据块。举例来说,对于给定的私钥k,如果不用IV,相同的明文输入块就会加密为同样的密文输出块。显然,如果在明文流中有重复的块,那么在密文流中也会存在重复的块。对于攻击者来说,知道有关明文块结构的任何信息,就可以使用这些信息解密已知的密文块并有可能发现密钥。为了解决这个问题,.NETFramework 中的私钥算法类将上一个块中的信息混合到下一个块的加密过程中。这样,两个相同的明文块的输出就会不同。由于该技术使用上一个块加密下一个块,因此使用了一个IV 来加密数据的第一个块。使用这种加密技术,非法用户即使知道了公共消息标头,也无法用于对密钥进行反向工程处理,从而使数据的安全系数大大提高。
对称加密算法的优点是保密强度高,加、解密速度快,适合加密大量数据。攻击者如果对加密后的数据进行破译,惟一的办法就是对每个可能的密钥执行穷举搜索。而采用这种加密技术,即使使用最快的计算机执行这种搜索,耗费的时间也相当长。如果使用较大的密钥,破译将会更加困难。在实际应用中,加密数据采用的密钥一般都有时效性,比如几天更换一次密钥和IV,如果攻击者采用穷举法试图破译加密后的数据,等到好不容易试出了密钥,加密者早已采用新的密钥对网络中传输的数据进行加密了,因此利用穷举搜索的方法破译加密后的数据实际上是没有意义的。
在.NET Framework 中,公共语言运行时CLR(Common Language Runtime)使用面向流的设计实现对称加密,该设计的核心是CryptoStream,实现CryptoStream 的任何被加密的对象都可以和实现Stream 的任何对象链接起来。实现对称加密算法的类有四种:
• DESCryptoServiceProvider
• RC2CryptoServiceProvider
• RijndaelManaged
• TripleDESCryptoServiceProvider
表 列出了四种对称加密类的主要特点。
表 四种对称加密类的主要特点
类 | 可用密钥长度(bit) | 加密算法 |
DESCryptoServiceProvider | 64 | DES加密算法 |
RC2CryptoServiceProvider | 40-128(每8 位递增) | RC2 加密算法 |
RijndaelManaged | 128-256(每64 位递增) | Rijndael加密算法 |
TripleDESCryptoServiceProvider | 128-192(每64 位递增) | 三重DES加密算法 |
这里仅介绍TripleDES 加密算法的相关知识和使用方法,其他对称加密类的用法与此相似。
TripleDES 使用DES 算法的三次连续迭代,支持从128 位到192 位(以64 位递增)的密钥长度,其安全性比DES 更高。DES 的含义是Data Encryption Standard,是美国1977 年公布的一种数据加密标准,DES 算法在各超市零售业、银行自动取款机、磁卡及IC 卡、加油站、高速公路收费站等领域被广泛应用,以此来实现关键数据的保密,如信用卡持卡人的PIN 的加密传输,IC 卡的认证、金融交易数据包的MAC 校验等,均用到DES 算法。DES 算法具有非常高的安全性,到目前为止,除了用穷举搜索法对DES 算法进行攻击外,还没有发现更有效的办法。而56 位长的密钥的穷举空间为256,这意味着如果一台计算机的速度是每一秒种检测一百万个密钥,则它搜索全部密钥就需要将近2285 年的时间。可见,攻击的难度是非常大的。但是,随着科学技术的发展,当出现超高速计算机后,以及用多台计算机同时进行穷举搜索,会大大缩短破译的时间,因此,为了增大攻击者破译的难度,TripleDES 在DES 的基础上又进行了三次迭代,密钥的长度最大可达192 位,使保密程度得到更进一步的提高。
下表 列出了TripleDESCryptoServiceProvider 类常用的属性和方法.
表 TripleDESCryptoServiceProvider类常用的属性和方法
名称 | 解释 |
BlockSize属性 | 获取或设置加密操作的快大小,以位为单位 |
Key属性 | 获取或设置TripleDES算法的机密密钥 |
IV属性 | 获取或设置TripleDES算法的初始化向量 |
KeySize属性 | 获取或设置TripleDES算法所用密钥的大小,以位为单位 |
CreateEncryptor方法 | 创建TripleDES加密器对象 |
CreateDecryptor方法 | 创建TripleDES解密器对象 |
GenerateIV方法 | 生成用于TripleDES算法的随机初始化向量IV |
GenerateKey方法 | 生成用于TripleDES算法的随机密钥 |
为了使用流进行加密解密处理,.NET Framework 还提供了CryptoStream 类,该类用于定义将数据流链接到加密转换的流。实现CryptoStream 的任何加密对象均可以和实现Stream 的任何对象链接起来,因此一个对象的流式处理输出可以馈送到另一个对象的输入,而不需要分别存储中间结果,即不需要存储第一个对象的输出。
CryptoStream 对象的用法和其他流的用法相似,这里不再重复介绍。但是要注意,完成CryptoStream 对象的使用后,不要忘了调用Close 方法关闭该对象。Close 方法会刷新流并使所有剩余的数据块都被CryptoStream 对象处理。由于在调用Close 方法前对流的读写操作有可能会出现异常,所以为确保流处理能够正常关闭,一般在try/catch 语句的finally 块中调用Close方法。
下例说明了该类的使用方法。为了让读者将注意力集中在如何加密和解密上,这个例子没有通过网络传递加密后的数据,而是全部在同一台计算机上进行加密解密处理。
【例】使用TripleDES 加密算法对输入的字符串进行加密,并输出加密后的字符串和解密后的结果。
(2) 添加对应的命名空间引用、方法和事件,源程序如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; //添加的命名空间引用 using System.Security.Cryptography; using System.IO; namespace TdesEncryptExample { public partial class FormTdesEncrypt : Form { public FormTdesEncrypt() { InitializeComponent(); } private void FormTdesEncrypt_Load(object sender, EventArgs e) { textBoxEncrypt.ReadOnly = true; textBoxDecrypt.ReadOnly = true; } private void buttonOK_Click(object sender, EventArgs e) { string str = textBoxInput.Text; if (str.Length == 0) { MessageBox.Show("请输入被加密的字符串"); return; } //加密 try { TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider(); //随机生成密钥Key 和初始化向量IV tdes.GenerateKey(); tdes.GenerateIV(); textBoxKey.Text = Encoding.UTF8.GetString(tdes.Key); //得到加密后的字节流 byte[] encryptedBytes = EncryptText(str, tdes.Key, tdes.IV); //显示加密后的字符串 textBoxEncrypt.Text = Encoding.UTF8.GetString(encryptedBytes); //解密 string decryptString = DecryptText(encryptedBytes, tdes.Key, tdes.IV); //显示解密后的字符串 textBoxDecrypt.Text = decryptString; } catch (Exception err) { MessageBox.Show(err.Message, "出错"); } } private byte[] EncryptText(string str, byte[] Key, byte[] IV) { //创建一个内存流 MemoryStream memoryStream = new MemoryStream(); //使用传递的私钥和IV 创建加密流 CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateEncryptor(Key, IV), CryptoStreamMode.Write); //将传递的字符串转换为字节数组 byte[] toEncrypt = Encoding.UTF8.GetBytes(str); try { //将字节数组写入加密流,并清除缓冲区 cryptoStream.Write(toEncrypt, 0, toEncrypt.Length); cryptoStream.FlushFinalBlock(); //得到加密后的字节数组 byte[] encryptedBytes = memoryStream.ToArray(); return encryptedBytes; } catch (CryptographicException err) { throw new Exception("加密出错:" + err.Message); } finally { cryptoStream.Close(); memoryStream.Close(); } } private string DecryptText(byte[] dataBytes, byte[] Key, byte[] IV) { //根据加密后的字节数组创建一个内存流 MemoryStream memoryStream = new MemoryStream(dataBytes); //使用传递的私钥、IV 和内存流创建解密流 CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateDecryptor(Key, IV), CryptoStreamMode.Read); //创建一个字节数组保存解密后的数据 byte[] decryptBytes = new byte[dataBytes.Length]; try { //从解密流中将解密后的数据读到字节数组中 cryptoStream.Read(decryptBytes, 0, decryptBytes.Length); //得到解密后的字符串 string decryptedString = Encoding.UTF8.GetString(decryptBytes); return decryptedString; } catch (CryptographicException err) { throw new Exception("解密出错:" + err.Message); } finally { cryptoStream.Close(); memoryStream.Close(); } } } }
(3) 按<F5>键编译并执行,输入一些字符串,然后单击开始加密和解密按钮,运行效果如图所示。
2 不对称加密
对称加密的缺点是双方使用相同的密钥和IV 进行加密、解密。由于接收方必须知道密钥和IV 才能解密数据,因此发送方需要先将密钥和IV 传递给接收方。这就有一个问题,如果攻击者截获了密钥和IV,也就等于知道了如何解密数据!如何保证发送方传递给接收方的密钥和IV 不被攻击者截获并破译呢?
不对称加密也叫公钥加密,这种技术使用不同的加密密钥与解密密钥,是一种“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。不对称加密产生的主要原因有两个,一是对称加密的密钥分配问题,另一个是由于对数字签名的需求。
不对称加密使用一个需要保密的私钥和一个可以对任何人公开的公钥,即使用公钥/私钥对来加密和解密数据。公钥和私钥都在数学上相关联,用公钥加密的数据只能用私钥解密,反之,用私钥加密的数据只能用公钥解密。两个密钥对于通信会话都是惟一的。公钥加密算法也称为不对称算法,原因是需要用一个密钥加密数据而需要用另一个密钥来解密数据。
私钥加密算法使用长度可变的缓冲区,而公钥加密算法使用固定大小的缓冲区,无法像私钥算法那样将数据链接起来成为流,因此无法使用与对称操作相同的流模型。这是编写程序时必须注意的问题。
为什么不对称加密更不容易被攻击呢?关键在于对私钥的管理上。在对称加密中,发送方必须先将解密密钥传递给接收方,接收方才能解密。如果避免通过不安全的网络传递私钥,不就解决这个问题了吗?
不对称加密的关键就在于此。使用不对称加密算法加密数据后,私钥不是发送方传递给接收方的,而是接收方先生成一个公钥/私钥对,在接收被加密的数据前,先将该公钥传递给发送方;注意,从公钥推导出私钥是不可能的,所以不怕通过网络传递时被攻击者截获公钥。发送方得到此公钥后,使用此公钥加密数据,再将加密后的数据通过网络传递给接收方;接收方收到加密后的数据后,再用私钥进行解密。由于没有传递私钥,从而保证了数据安全性。
.NET Framework 提供以下实现不对称加密算法的类:
• DSACryptoServiceProvider
• RSACryptoServiceProvider
下面以RSACryptoServiceProvider 类为例介绍具体的使用方法。
RSACryptoServiceProvider 类使用加密服务提供程序提供的RSA算法实现不对称加密和解密。加密服务提供程序CSP(Cryptographic Service Provider)是微软在Windows 操作系统中内置的加密处理模块,RSACryptoServiceProvider 类已经对其提供的相关接口和参数进行了封装,所以即使我们不知道CSP 内部是如何实现的,也一样可以使用其提供的功能。下表出了RSACryptoServiceProvider 类的部分属性和方法。
表 RSACryptoServiceProvider类的部分属性和方法
名称 | 解释 |
CspKeyContainerInfo 属性 | 检索关于加密密钥对的相关信息,比如密钥是否可导出、密钥容器名称以及提供程序的信息等 |
PersistKeyInCsp属性 |
获取或设置一个值,该值指示密钥是否应该永久驻留在加密服务提供程序(CSP)中。当在 |
PublicOnly属性 |
获取一个值, 该值指示RSACryptoServiceProvider 对象是否仅包含一个公钥。如果 |
Encrypt方法 |
使用RSA算法对数据进行加密。该方法有两个参数,第一个参数是被加密的字节数组;第二个参数 |
Decrypt方法 | 使用RSA算法对数据进行解密 |
ImportParameters方法 | 导入指定的RSAParameters。RSAParameters表示RSA算法涉及的相关参数 |
ExportParameters方法 | 导出指定的RSAParameters |
FromXmlString方法 |
通过XML字符串中的密钥信息初始化RSA对象。该XML字符串是使用ToXmlString方法生成的。 |
ToXmlString方法 |
创建并返回包含当前RSA对象的密钥的XML字符串。该方法有一个布尔型参数,true表示同时包含 |
下例演示了利用RSACryptoServiceProvider 类加密和解密数据的方法。与例6-1 的思路一样,为了让读者将注意力集中在如何加密和解密上,这个例子中仍然采用在同一台计算机上进行加密和解密处理。
【例】利用不对称加密算法加密指定的字符串,并输出加密和解密后的结果。
(1) 新建一个名为RsaEncryptExample 的Windows 应用程序, 修改Form1.cs 为FormRsaEncrypt.cs,设计界面如下图所示。
(2) 添加对应的命名空间引用、方法和事件,源程序如下:
using System; using System.Security.Cryptography; using System.Text; using System.Windows.Forms; namespace RsaEncryptExample { public partial class FormRsaEncrypt : Form { public FormRsaEncrypt() { InitializeComponent(); this.Text = "RSA 加密解密"; textBoxEncrypt.ReadOnly = true; textBoxDecrypt.ReadOnly = true; } private void buttonOK_Click(object sender, EventArgs e) { //使用默认密钥创建RSACryptoServiceProvider 对象 RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); //显示包含公钥/私钥对的XML 表示形式,如果只显示公钥,将参数改为false 即可 richTextBoxKeys.Text = rsa.ToXmlString(true); //将被加密的字符串转换为字节数组 byte[] dataToEncrypt = Encoding.UTF8.GetBytes(textBoxInput.Text); try { //得到加密后的字节数组 byte[] encryptedData = rsa.Encrypt(dataToEncrypt, false); textBoxEncrypt.Text = Encoding.UTF8.GetString(encryptedData); //得到解密后的字节数组 byte[] decryptedData = rsa.Decrypt(encryptedData, false); textBoxDecrypt.Text = Encoding.UTF8.GetString(decryptedData); } catch (Exception err) { MessageBox.Show(err.Message); } } } }
(3) 按<F5>键编译并执行,输入一些字符串,然后单击开始加密和解密按钮,运行效果如图6-4 所示。
从这个例子可以看出,使用不对称加密类RSACryptoServiceProvider 加密和解密数据的过程并不复杂。但是,在网络应用编程中,RSACryptoServiceProvider 类的主要用途是加密和解密通过网络传递的对称加密用的私钥。而加密大量数据则应该使用对称加密算法。通过不安全的网络将用对称加密算法加密的数据传递到接收方后,接收方必须知道对称加密用的密钥才能够解密。而且实际应用中的密钥还需要经常更换,以避免攻击者破译。为了解决传递对称加密密钥的安全问题,发送方可以用不对称加密算法加密对称加密算法用的密钥,并将加密后的密钥传递给接收方,以便接收方用此密钥解密数据。可要完成这个功能,接收方必须在接收对称加密算法加密的数据前先将不对称加密的公钥传递给发送方,以便发送方据此加密对称加密的密钥。可见,使用RSACryptoServiceProvider 类的关键是如何导出公钥,并将公钥通过网络传递给发送方。
还有一点要注意,本节的例子是使用XML 的格式通过网络传递公钥的,这是因为公钥不需要保密。但是千万不要将私钥以XML 形式通过网络传递,也不要将私钥以XML 形式存储在本地计算机上,如果确实需要保存公钥/私钥对,应该使用密钥容器。
顾名思义,密钥容器就是保存密钥用的容器,使用密钥容器的目的是为了保证密钥的安全性。由于密钥容器可以有多个,为了区分是哪一个密钥容器,一般都要给每个密钥容器起一个名称。在System.Security.Cryptography 命名空间中,有一个CspParameters 类,可以通过该类提供的属性设置或获取密钥容器的名称。
3 通过网络传递加密数据
虽然不对称加密解决了用对称加密传递消息必须传递密钥的问题,但是由于不对称加密无法使用流进行处理,因此与对称加密相比效率较低,不适用于加密大量数据的场合。在实际应用中,一般将两种加密方法配合使用。其基本思想是:用不对称加密算法加密对称加密算法的密钥,用对称加密算法加密实际数据。
具体设计思路可以简单描述为:A 和B 相互传递加密的数据前,B 首先生成一个不对称加密算法使用的公钥/私钥对,假定公钥为publicKey,私钥为privateKey,然后B 将公钥publicKey 通过网络传递给A;A 接收到此公钥后,根据此公钥初始化不对称加密对象,并用此对象加密使用对称加密算法的密钥key,并将加密后的密钥key 通过网络传递给B;这样,A 和B 都有了一个共同使用的对称加密的密钥,然后双方用此密钥加密数据,并将加密后的数据传递给对方,对方收到加密后的数据后,再用密钥key 解密数据。
通过这种方式,在不安全的网络上传递加密后的数据时,虽然攻击者可以截获公钥,但是由于用公钥加密的数据只能用私钥解密,而私钥并没有通过网络传递,因此攻击者无法通过公钥publicKey 破译加密后的密钥key,因此也无法破译加密的消息。
在实际应用中,一般使用TCP 协议通过网络传输数据。对于比较重要的数据,必须进行加密解密处理。一般实现方案为:
1) 传输双方均各自生成一个公钥/私钥对。
2) 通过TCP 协议交换公钥。
3) 双方各自生成一个对称加密用的私钥,并使用对方的公钥加密新创建的私钥。
4) 双方将加密后的对称加密用的私钥发送给对方,以便对方利用此私钥解密。
5) 双方使用对称加密进行会话。
采用TCP 协议进行网络数据传输时,注意不要忘了解决TCP 协议的消息边界问题。对于发送大量数据的场合,一般的解决办法是,将数据发送到网络流之前,先计算出每个加密后的数据包的长度,然后将数据包的长度和数据全部发送到网络流中。下图说明了发送方和接收方的网络传输的过程。从图中可以看出,在通过网络传输数据之前,发送方先读取一个数据块,进行加密,并将加密后的数据保存在内存流中,然后计算加密后的数据长度,最后将数据长度和内存流中的数据转换成字节序列,通过网络流发送给接收方;接收方接收数据时,首先从网络流中获取要读取的加密后的数据量的大小值,然后根据获取的要读取的字节数,从网络流中读取数据,并解密这些数据到内存流中,再把内存流中的数据转换成字节序列,从而形成原始数据。对于较大的不能一次传输的数据,循环执行这个过程,直到数据全部传输完毕。
下面通过一个例子说明具体的实现方法,为了不使问题复杂化,以便读者容易理解,这个例子对实际的实现方案进行了简化处理,简化后的设计思路为:
1) 客户端生成一个使用RSA 算法的不对称加密的公钥/私钥对,然后通过TCP 协议将公钥发送到服务器端。
2) 服务器端用客户端发送的公钥初始化RSA 对象。然后利用此对象加密使用TripleDES算法的对称加密的密钥。
3) 服务器端将加密后的对称加密的密钥发送到客户端,客户端利用RSA 的私钥解密TripleDES 密钥,并用此密钥初始化TripleDES 对象。
4) 双方使用对称加密算法加密对话内容,并将加密后的对话内容发送给对方。
5) 接收方接收到加密后的对话内容后,利用对称加密算法解密对话内容,并显示解密前和解密后的结果。
【例】利用同步TCP 传递会话数据。要求使用不对称加密算法加密对称加密算法使用的私钥,使用对称加密算法加密会话信息。
1. 服务器端设计
(1) 新建一个名为EncryptedTcpServer 的Windows 应用程序, 修改Form1.cs 为FormServer.cs,设计界面如图6-6 所示。
(2) 添加一个类文件User.cs,源程序如下:
using System.Net.Sockets; using System.IO; using System.Text; using System.Security.Cryptography; namespace EncryptedTcpServer { class User { public TcpClient client; public BinaryReader br; public BinaryWriter bw; //对称加密 public TripleDESCryptoServiceProvider tdes; //不对称加密 public RSACryptoServiceProvider rsa; public User(TcpClient client) { this.client = client; NetworkStream networkStream = client.GetStream(); br = new BinaryReader(networkStream, Encoding.UTF8); bw = new BinaryWriter(networkStream, Encoding.UTF8); tdes = new TripleDESCryptoServiceProvider(); //随机生成密钥Key 和初始化向量IV,也可以不用此两句,而使用默认的Key 和IV //tdes.GenerateKey(); //tdes.GenerateIV(); rsa = new RSACryptoServiceProvider(); } } }
(3) 在FormServer.cs 中添加对应的代码,源程序如下:
using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Windows.Forms; namespace EncryptedTcpServer { public partial class FormServer : Form { //连接的用户 System.Collections.Generic.List<User> userList = new List<User>(); private delegate void SetListBoxCallback(string str); private SetListBoxCallback setListBoxCallback; private delegate void SetComboBoxCallback(User user); private SetComboBoxCallback setComboBoxCallback; //使用的本机IP 地址 IPAddress localAddress; //监听端口 private int port = 51888; private TcpListener myListener; public FormServer() { InitializeComponent(); listBoxStatus.HorizontalScrollbar = true; setListBoxCallback = new SetListBoxCallback(SetListBox); setComboBoxCallback = new SetComboBoxCallback(AddComboBoxitem); IPAddress[] addrIP = Dns.GetHostAddresses(Dns.GetHostName()); localAddress = addrIP[0]; buttonStop.Enabled = false; } //【开始监听】按钮的Click 事件 private void buttonStart_Click(object sender, EventArgs e) { myListener = new TcpListener(localAddress, port); myListener.Start(); SetListBox(string.Format("开始在{0}:{1}监听客户连接", localAddress, port)); //创建一个线程监听客户端连接请求 ThreadStart ts = new ThreadStart(ListenClientConnect); Thread myThread = new Thread(ts); myThread.Start(); buttonStart.Enabled = false; buttonStop.Enabled = true; } //接收客户端连接的线程 private void ListenClientConnect() { while (true) { TcpClient newClient = null; try { //等待用户进入 newClient = myListener.AcceptTcpClient(); } catch { //当单击“停止监听”或者退出此窗体时AcceptTcpClient()会产生异常 //因此可以利用此异常退出循环 break; } //每接受一个客户端连接,就创建一个对应的线程循环接收该客户端发来的信息 ParameterizedThreadStart pts = new ParameterizedThreadStart(ReceiveData); Thread threadReceive = new Thread(pts); User user = new User(newClient); threadReceive.Start(user); userList.Add(user); AddComboBoxitem(user); SetListBox(string.Format("[{0}]进入", newClient.Client.RemoteEndPoint)); SetListBox(string.Format("当前连接用户数:{0}", userList.Count)); } } //接收、处理客户端信息的线程,每客户1 个线程,参数用于区分是哪个客户 private void ReceiveData(object obj) { User user = (User)obj; TcpClient client = user.client; //是否正常退出接收线程 bool normalExit = false; //用于控制是否退出循环 bool exitWhile = false; while (exitWhile == false) { //保存接收的命令字符串 string receiveString = null; //解析命令用 //每条命令均带有一个参数,值为true 或者false,表示是否有紧跟的字节数组 string[] splitString = null; byte[] receiveBytes = null; try { //从网络流中读出命令字符串 //此方法会自动判断字符串长度前缀,并根据长度前缀读出字符串 receiveString = user.br.ReadString(); splitString = receiveString.Split(','); if (splitString[1] == "true") { //先从网络流中读出32 位的长度前缀 int bytesLength = user.br.ReadInt32(); //然后读出指定长度的内容保存到字节数组中 receiveBytes = user.br.ReadBytes(bytesLength); } } catch { //底层套接字不存在时会出现异常 SetListBox("接收数据失败"); } if (receiveString == null) { if (normalExit == false) { //如果停止了监听,Connected 为false if (client.Connected == true) { SetListBox(string.Format( "与[{0}]失去联系,已终止接收该用户信息", client.Client.RemoteEndPoint)); } } break; } SetListBox(string.Format("来自[{0}]:{1}", user.client.Client.RemoteEndPoint, receiveString)); if (receiveBytes != null) { SetListBox(string.Format("来自[{0}]:{1}", user.client.Client.RemoteEndPoint, Encoding.Default.GetString(receiveBytes))); } switch (splitString[0]) { case "rsaPublicKey": //使用传递过来的公钥重新初始化该客户端对 //应的RSACryptoServiceProvider 对象, //然后就可以使用这个对象加密对称加密的私钥了 user.rsa.FromXmlString(Encoding.Default.GetString(receiveBytes)); //加密对称加密的私钥 try { //使用RSA 算法加密对称加密算法的私钥Key byte[] encryptedKey = user.rsa.Encrypt(user.tdes.Key, false); SendToClient(user, "tdesKey,true", encryptedKey); //加密IV byte[] encryptedIV = user.rsa.Encrypt(user.tdes.IV, false); SendToClient(user, "tdesIV,true", encryptedIV); } catch (Exception err) { MessageBox.Show(err.Message); } break; case "Logout": //格式:Logout SetListBox(string.Format("[{0}]退出", user.client.Client.RemoteEndPoint)); normalExit = true; exitWhile = true; break; case "Talk": //解密 string talkString = DecryptText(receiveBytes, user.tdes.Key, user.tdes.IV); if (talkString != null) { SetListBox(string.Format("[{0}]说:{1}", client.Client.RemoteEndPoint, talkString)); } break; default: SetListBox("什么意思啊:" + receiveString); break; } } userList.Remove(user); client.Close(); SetListBox(string.Format("当前连接用户数:{0}", userList.Count)); } //使用对称加密加密字符串 private byte[] EncryptText(string str, byte[] Key, byte[] IV) { //创建一个内存流 MemoryStream memoryStream = new MemoryStream(); //使用传递的私钥和IV 创建加密流 CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateEncryptor(Key, IV), CryptoStreamMode.Write); //将传递的字符串转换为字节数组 byte[] toEncrypt = Encoding.UTF8.GetBytes(str); try { //将字节数组写入加密流,并清除缓冲区 cryptoStream.Write(toEncrypt, 0, toEncrypt.Length); cryptoStream.FlushFinalBlock(); //得到加密后的字节数组 byte[] encryptedBytes = memoryStream.ToArray(); return encryptedBytes; } catch (Exception err) { SetListBox("加密出错:" + err.Message); return null; } finally { cryptoStream.Close(); memoryStream.Close(); } } //使用对称加密算法解密接收的字符串 private string DecryptText(byte[] dataBytes, byte[] Key, byte[] IV) { //根据加密后的字节数组创建一个内存流 MemoryStream memoryStream = new MemoryStream(dataBytes); //使用传递的私钥、IV 和内存流创建解密流 CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateDecryptor(Key, IV), CryptoStreamMode.Read); //创建一个字节数组保存解密后的数据 byte[] decryptBytes = new byte[dataBytes.Length]; try { //从解密流中将解密后的数据读到字节数组中 cryptoStream.Read(decryptBytes, 0, decryptBytes.Length); //得到解密后的字符串 string decryptedString = Encoding.UTF8.GetString(decryptBytes); return decryptedString; } catch (Exception err) { SetListBox("解密出错:" + err.Message); return null; } finally { cryptoStream.Close(); memoryStream.Close(); } } //发送信息到客户端 private void SendToClient(User user, string command, byte[] bytes) { //每条命令均带有一个参数,值为true 或者false,表示是否有紧跟的字节数组 string[] splitCommand = command.Split(','); try { //先将命令字符串写入网络流,此方法会自动附加字符串长度前缀 user.bw.Write(command); SetListBox(string.Format("向[{0}]发送:{1}", user.client.Client.RemoteEndPoint, command)); if (splitCommand[1] == "true") { //先将字节数组的长度(32 位整数)写入网络流 user.bw.Write(bytes.Length); //然后将字节数组写入网络流 user.bw.Write(bytes); user.bw.Flush(); SetListBox(string.Format("向[{0}]发送:{1}", user.client.Client.RemoteEndPoint, Encoding.UTF8.GetString(bytes))); if (splitCommand[0] == "Talk") { SetListBox("加密前内容:" + textBoxSend.Text); } } } catch { SetListBox(string.Format("向[{0}]发送信息失败", user.client.Client.RemoteEndPoint)); } } private void AddComboBoxitem(User user) { if (comboBoxReceiver.InvokeRequired == true) { this.Invoke(setComboBoxCallback, user); } else { comboBoxReceiver.Items.Add(user.client.Client.RemoteEndPoint); } } private void SetListBox(string str) { if (listBoxStatus.InvokeRequired == true) { this.Invoke(setListBoxCallback, str); } else { listBoxStatus.Items.Add(str); listBoxStatus.SelectedIndex = listBoxStatus.Items.Count - 1; listBoxStatus.ClearSelected(); } } //单击停止监听按钮触发的事件 private void buttonStop_Click(object sender, EventArgs e) { SetListBox(string.Format("目前连接用户数:{0}", userList.Count)); SetListBox("开始停止服务,并依次使用户退出!"); for (int i = 0; i < userList.Count; i++) { comboBoxReceiver.Items.Remove(userList[i].client.Client.RemoteEndPoint); userList[i].bw.Close(); userList[i].br.Close(); userList[i].client.Close(); } //通过停止监听让myListener.AcceptTcpClient()产生异常退出监听线程 myListener.Stop(); buttonStart.Enabled = true; buttonStop.Enabled = false; } //单击【发送】按钮的Click 事件 private void buttonSend_Click(object sender, EventArgs e) { int index = comboBoxReceiver.SelectedIndex; if (index == -1) { MessageBox.Show("请先选择接收方,然后再单击[发送]"); } else { User user = (User)userList[index]; //加密textBoxSend.Text 的内容 byte[] encryptedBytes = EncryptText(textBoxSend.Text, user.tdes.Key, user.tdes.IV); if (encryptedBytes != null) { SendToClient(user, "Talk,true", encryptedBytes); textBoxSend.Clear(); } } } private void FormServer_FormClosing(object sender, FormClosingEventArgs e) { //未单击开始监听就直接退出时,myListener 为null if (myListener != null) { buttonStop_Click(null, null); } } private void textBoxSend_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Return) { buttonSend_Click(null, null); } } } }
(4) 按<F5>键编译并运行,确保没有语法错误。
2.客户端设计
(1) 新建一个名为EncryptedTcpClient 的Windows 应用程序, 修改Form1.cs 为FormClient.cs,设计界面如图6-7 所示。
(2) 添加对应的代码,源程序如下:
using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Windows.Forms; namespace EncryptedTcpClient { public partial class FormClient : Form { private bool isExit = false; private delegate void SetListBoxCallback(string str); private SetListBoxCallback setListBoxCallback; private TcpClient client; private BinaryReader br; private BinaryWriter bw; //对称加密 private TripleDESCryptoServiceProvider tdes; //不对称加密 private RSACryptoServiceProvider rsa; public FormClient() { InitializeComponent(); listBoxStatus.HorizontalScrollbar = true; setListBoxCallback = new SetListBoxCallback(SetListBox); } private void buttonConnect_Click(object sender, EventArgs e) { try { //实际使用时要将Dns.GetHostName()改为服务器域名 client = new TcpClient(Dns.GetHostName(), 51888); SetListBox(string.Format("本机EndPoint:{0}", client.Client.LocalEndPoint)); SetListBox("与服务器建立连接成功"); } catch { SetListBox("与服务器连接失败"); return; } buttonConnect.Enabled = false; //获取网络流 NetworkStream networkStream = client.GetStream(); //将网络流作为二进制读写对象 br = new BinaryReader(networkStream); bw = new BinaryWriter(networkStream); Thread threadReceive = new Thread(new ThreadStart(ReceiveData)); threadReceive.Start(); //使用默认密钥创建对称加密对象 tdes = new TripleDESCryptoServiceProvider(); //使用默认密钥创建不对称加密对象 rsa = new RSACryptoServiceProvider(); //导出不对称加密密钥的xml 表示形式,false 表示不包括私钥 string rsaPublicKey = rsa.ToXmlString(false); //将导出的公钥发送到服务器,公钥可以对任何人公开 SendData("rsaPublicKey,true", Encoding.Default.GetBytes(rsaPublicKey)); } //接收线程 private void ReceiveData() { while (isExit == false) { //保存接收的命令字符串 string receiveString = null; //解析命令用 //每条命令均带有一个参数,值为true 或者false,表示是否有紧跟的字节数组 string[] splitString = null; byte[] receiveBytes = null; try { //从网络流中读出命令字符串 //此方法会自动判断字符串长度前缀,并根据长度前缀读出字符串 receiveString = br.ReadString(); splitString = receiveString.Split(','); if (splitString[1] == "true") { //先从网络流中读出32 位的长度前缀 int bytesLength = br.ReadInt32(); //然后读出指定长度的内容保存到字节数组中 receiveBytes = br.ReadBytes(bytesLength); } } catch { //底层套接字不存在时会出现异常 SetListBox("接收数据失败"); } if (receiveString == null) { if (isExit == false) { MessageBox.Show("与服务器失去联系!"); } break; } SetListBox("收到:" + receiveString); if (receiveBytes != null) { SetListBox(string.Format("收到:{0}", Encoding.Default.GetString(receiveBytes))); } switch (splitString[0]) { case "Talk": //解密 string talkString = DecryptText(receiveBytes, tdes.Key, tdes.IV); if (talkString != null) { SetListBox(string.Format("服务器说:{0}", talkString)); } break; case "tdesKey": //解密 tdes.Key = rsa.Decrypt(receiveBytes, false); break; case "tdesIV": //解密 tdes.IV = rsa.Decrypt(receiveBytes, false); break; default: SetListBox("什么意思啊:" + receiveString); break; } } Application.Exit(); } //使用对称加密加密字符串 private byte[] EncryptText(string str, byte[] Key, byte[] IV) { //创建一个内存流 MemoryStream memoryStream = new MemoryStream(); //使用传递的私钥和IV 创建加密流 CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateEncryptor(Key, IV), CryptoStreamMode.Write); //将传递的字符串转换为字节数组 byte[] toEncrypt = Encoding.UTF8.GetBytes(str); try { //将字节数组写入加密流,并清除缓冲区 cryptoStream.Write(toEncrypt, 0, toEncrypt.Length); cryptoStream.FlushFinalBlock(); //得到加密后的字节数组 byte[] encryptedBytes = memoryStream.ToArray(); return encryptedBytes; } catch (CryptographicException err) { SetListBox("加密出错:" + err.Message); return null; } finally { cryptoStream.Close(); memoryStream.Close(); } } //使用对称加密算法解密接收的字符串 private string DecryptText(byte[] dataBytes, byte[] Key, byte[] IV) { //根据加密后的字节数组创建一个内存流 MemoryStream memoryStream = new MemoryStream(dataBytes); //使用传递的私钥、IV 和内存流创建解密流 CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateDecryptor(Key, IV), CryptoStreamMode.Read); //创建一个字节数组保存解密后的数据 byte[] decryptBytes = new byte[dataBytes.Length]; try { //从解密流中将解密后的数据读到字节数组中 cryptoStream.Read(decryptBytes, 0, decryptBytes.Length); //得到解密后的字符串 string decryptedString = Encoding.UTF8.GetString(decryptBytes); return decryptedString; } catch (CryptographicException err) { SetListBox("解密出错:" + err.Message); return null; } finally { cryptoStream.Close(); memoryStream.Close(); } } //发送信息到服务器 private void SendData(string command, byte[] bytes) { //每条命令均带有一个参数,值为true 或者false,表示是否有紧跟的字节数组 //如果不带参数,也可以实现,但是会导致接收方判断代码复杂化 string[] splitCommand = command.Split(','); try { //先将命令字符串写入网络流,此方法会自动附加字符串长度前缀 bw.Write(command); SetListBox(string.Format("发送:{0}", command)); if (splitCommand[1] == "true") { //先将字节数组的长度(32 位整数)写入网络流 bw.Write(bytes.Length); //然后将字节数组写入网络流 bw.Write(bytes); bw.Flush(); SetListBox(string.Format("发送:{0}", Encoding.UTF8.GetString(bytes))); if (splitCommand[0] == "Talk") { SetListBox("加密前内容:" + textBoxSend.Text); } } } catch { SetListBox("发送失败!"); } } private void SetListBox(string str) { if (listBoxStatus.InvokeRequired == true) { this.Invoke(setListBoxCallback, str); } else { listBoxStatus.Items.Add(str); listBoxStatus.SelectedIndex = listBoxStatus.Items.Count - 1; listBoxStatus.ClearSelected(); } } //单击发送按钮触发的事件 private void buttonSend_Click(object sender, EventArgs e) { //加密textBoxSend.Text 的内容 byte[] encryptedBytes = EncryptText(textBoxSend.Text, tdes.Key, tdes.IV); if (encryptedBytes != null) { SendData("Talk,true", encryptedBytes); textBoxSend.Clear(); } } private void FormClient_FormClosing(object sender, FormClosingEventArgs e) { //未与服务器连接前client 为null if (client != null) { SendData("Logout,false", null); isExit = true; br.Close(); bw.Close(); client.Close(); } } private void textBoxSend_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Return) { buttonSend_Click(null, null); } } } }
(3) 同时执行服务器端程序和客户端程序,运行效果如图6-8 所示。
4 Hash算法与数字签名
通过Internet 下载文件后,怎样知道下载的文件是否和原始文件完全相同呢?或者说,发送方通过Internet 发送数据后,接收方如何验证接收的数据是否和原始数据完全相同呢?这就是数字签名的用途。
数字签名是利用不对称加密和Hash 算法共同实现的。为了真正理解数字签名的实现原理,还必须简单介绍一下Hash 算法。
Hash 算法也叫散列算法,其功能是把任意长度的二进制值映射为较小的固定长度的二进制值,实现原理就是提供一种数据内容和数据存放地址之间的映射关系。利用Hash 算法得到的这个固定长度的较小的二进制值叫Hash 值。
Hash 算法具有如下特点:
1) 散列效果好。即使原始数据只发生一个小小的改动,数据的散列也会发生非常大的变化。假如两个单词非常相似,比如只有一个字母不同,使用Hash 算法得到的结果也相差甚远。甚至根本看不出二者之间有什么相似之处。
2) 散列函数不可逆。即不可能从散列结果推导出原始数据。
3) 对不同的数据进行Hash 运算不可能生成相同的Hash 值。
Hash 算法的用途主要有两大类:一类是将Hash 值作为消息身份验证代码(MAC,MessageAuthentication Code),用于和数字签名一起实现对消息数据进行身份验证;另一类是将Hash值作为消息检测代码(MDC,Message Detection Code),用于检测数据完整性。
在应用程序中,可以利用数字签名实现数据身份验证和数据完整性验证。数据身份验证是为了验证数据是不是持有私钥的人发送的;数据完整性验证则用于验证数据在传输过程中是否被修改过。
验证数据完整性的实现原理是:发送方先使用Hash 算法对数据进行Hash 运算得到数据的Hash 值,然后将数据和Hash 值一块儿发送给接收方;接收方接收到数据和Hash 值后,对接收的数据进行和发送方相同的Hash 运算,然后将计算得到的Hash 值和接收的Hash 值进行比较,如果二者一致,说明收到的数据肯定与发送方发送的原始数据相同,从而说明数据是完整的。
.NET Framework 提供以下实现数字签名的类:
• DSACryptoServiceProvider
• RSACryptoServiceProvider
可见,这两个类既能实现加密解密数据的功能,也能实现数字签名的功能。具体使用方法并不复杂,这里不再赘述。
保证数据在网络传递中的安全性和完整性,涉及的技术很多,本章只是简单地介绍了一下相关的知识,更深入的内容需要读者自己研究。从技术选择上,主要考虑以下情况:
1) 如果需要使用一种方法验证数据在传输过程中是否被修改,可以使用Hash 值。
2) 如果要证明实体知道机密但不相互发送机密,或者想使用简单的Hash 值以防止在传输过程中被截获,可以使用加密的Hash 值。
3) 如果要隐藏通过不安全的媒介发送的数据或者要永久性保留数据,可以使用加密。
4) 如果要验证声称是公钥所有者的人员的身份,可以使用证书。
5) 如果双方事先均知道准备使用的密钥,可以使用对称加密以提高速度。
6) 如果想通过不安全的媒介安全地交换数据,可以使用非对称加密。
7) 如果要进行身份验证和实现不可否认性,可以使用数字签名。
8) 如果为了防范穷举搜索而进行的攻击,可以使用加密技术生成的随机数。
设计一个安全系统时,应该根据安全第一、性能第二的原则来选择实现的技术。在.NET环境下,对数据进行加密,可以解决数据的保密性,完整性和身份验证等重要安全问题。对称加密和不对称加密各有优缺点,一般情况下,要将两种加密方法结合使用,这样可以提高数据的保密性和数据传输效率。.NET 提供的HASH 算法类主要用于数字签名,利用.NET 提供的数字签名技术,接收者可以核实发送者对报文的签名,防止发送者抵赖对数据的签名,同时也避免了攻击者伪造对报文的签名。