建议117:使用SSL确保通信中的数据安全
SSL(Secure Socket Layer)最初是由NetScape公司设计的,用于Web安全的网络协议。目前它已经广泛应用到各类网络传输通信中了。SSL利用数字证书技术(非对称加密),保证了通信过程中的唯一性、不可篡改性、不可抵赖性。SSL通道原理图:
非对称加密中:
- 秘钥分为两部分:公钥PK和私钥SK。
- 公钥用于加密数据用,私钥用于解密。
- 公钥可公开而且应该公开,私钥只属于创建者。
经过公钥加密的数据只有证书创建者才能解密。这是构成SSL通道所有理论的依据。
在传统的网络传输过程中,我们将通信双方定义为:服务器端和客户端。假定服务器端是数字证书的创建者,它保存好自己的私钥,同时公布了自己的公钥给所有的客户端。满足了这个条件,我们来构建SSL通道。
首先,客户端随机生成一个字符串作为密钥K,然后用公钥PK对这个密钥加密,并将加密后密钥发送给服务器端。如果客户端曾经在服务器端注册过自己的信息,则还可以在这个密钥上加上自己的身份信息,从而向服务器端汇报自己的唯一性,但在本例中略去这一步。
服务器端用私钥解密消息,获取了客户端的K,并确认了客户端的身份(不可抵赖性),SSL通道建立。
服务器端和客户端现在可以进行安全通信。过程是:发送方使用密钥K对要传输的消息进行对称加密,接受方则使用K进行解密。这就是传输过程中的不可篡改性。
我们来模拟SSL的通信,服务器部分的代码:
#region server //用于保存非对称加密(数字证书)的公钥 string publicKey = string.Empty; //用于保存非对称加密(数字证书)的私钥 string pfxKey = string.Empty; ///====================== ///服务器端代码 ///====================== ///用于跟客户端通信的socket Socket serverCommunicateSocket; ///定义接受缓存块的大小 static int serverBufferSize = 1024; ///缓存块 byte[] bytesReceivedFromClient = new byte[serverBufferSize]; ///密钥K string key = string.Empty; StringBuilder messageFromClient = new StringBuilder(); ///开启服务器 private void buttonStartServer_Click(object sender, EventArgs e) { //先生成数字证书(模拟,即非对称密钥对) RSAKeyInit(); //负责侦听 StartListen(); } private void RSAKeyInit() { RSAProcessor.CreateRSAKey(ref publicKey, ref pfxKey); } private void StartListen() { IPEndPoint iep = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 8009); //负责侦听的socket Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); listenSocket.Bind(iep); listenSocket.Listen(50); listenSocket.BeginAccept(new AsyncCallback(this.Accepted), listenSocket); ListBoxServerShow("开始侦听。。。"); buttonStartServer.Enabled = false; } ///负责客户端的连接,并开始将自己置于接收状态 void Accepted(IAsyncResult result) { Socket listenSocket = result.AsyncState as Socket; //初始化和客户端进行通信的socket serverCommunicateSocket = listenSocket.EndAccept(result); ListBoxServerShow("有客户端连接到。。。"); serverCommunicateSocket.BeginReceive(bytesReceivedFromClient, 0, serverBufferSize, SocketFlags.None, new AsyncCallback(this.ReceivedFromClient), null); } ///负责处理接受自客户端的数据 void ReceivedFromClient(IAsyncResult result) { int read = serverCommunicateSocket.EndReceive(result); if (read > 0) { messageFromClient.Append(UTF32Encoding.Default.GetString(bytesReceivedFromClient, 0, read)); //处理并显示数据 ProcessAndShowInServer(); serverCommunicateSocket.BeginReceive(bytesReceivedFromClient, 0, serverBufferSize, 0, new AsyncCallback(ReceivedFromClient), null); } } private void ProcessAndShowInServer() { string msg = messageFromClient.ToString(); //如果接收到<EOF>则表示完成完成一次,否则继续将自己置于接收状态 if (msg.IndexOf("<EOF>") > -1) { //如果客户端发送key,则负责初始化key if (msg.IndexOf("<KEY>") > -1) { //用私钥解密发送过来的Key信息 key = RSAProcessor.RSADecrypt(pfxKey, msg.Substring(0, msg.Length - 10)); ListBoxServerShow(string.Format("接收到客户端密钥:{0}", key)); } else { //解密SSL通道中发送过来的密文并显式 ListBoxServerShow(string.Format("接收到客户端消息:{0}", RijndaelProcessor.DencryptString(msg.Substring(0, msg.Length - 5), key))); } messageFromClient.Clear(); } } ///负责向客户端发送数据 private void buttonStartSendToClient_Click(object sender, EventArgs e) { //加密消息体 string msg = string.Format("{0}{1}", RijndaelProcessor.EncryptString(DateTime.Now.ToString(), key), "<EOF>"); RijndaelProcessor.DencryptString(msg.Substring(0, msg.Length - 5), key); byte[] msgBytes = UTF32Encoding.Default.GetBytes(msg); serverCommunicateSocket.BeginSend(msgBytes, 0, msgBytes.Length, SocketFlags.None, null, null); ListBoxServerShow(string.Format("发送:{0}", msg)); } private void ListBoxServerShow(string msg) { listBoxServer.BeginInvoke(new Action(() => { listBoxServer.Items.Add(msg); })); } #endregion server
RSAProcessor工具类,用于封装非对称加密算法:
RijndaelProcessor工具类,用于封装对称加密算法:
这是一WinForm窗体程序,模拟的是服务器端部分,其中有两个按钮。按钮事件方法buttonStartServe_Click负责让服务器处理侦听状态。当然,为了模拟需要,在方法中还初始化了非对称加密密钥对。记住,公钥要公开给客户端。注意,也可以使用数字证书,但是为了演示方便,本例仅使用公钥-私钥对。
通信部分直接使用了FCL中的Socket类型,并采用异步的方式处理发送和接收任务。关于通信部分,本建议不再赘述。唯一要注意的是,在发送和接收过程中,要调用RijndaelProcessor.EncryptString方法加密,然后用RijndaelProcessor.DencryptString方法解密。RijndaelProcessor类型是用来封装对称加密、解密算法的工具类。
客户端部分代码:
#region client ///====================== ///客户端代码 ///====================== ///用于跟服务器通信的socket Socket clientCommunicateSocket; ///用于暂存接收到的字符串 StringBuilder messageFromServer = new StringBuilder(); ///定义接受缓存块的大小 static int clientBufferSize = 1024; ///缓存块 byte[] bytesReceivedFromServer = new byte[clientBufferSize]; //随机生成的key,在这里硬编码为key123 string keyCreateRandom = "key123"; private void buttonConnectAndReceiveMsg_Click(object sender, EventArgs e) { IPEndPoint iep = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 8009); Socket connectSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); connectSocket.BeginConnect(iep, new AsyncCallback(this.Connected), connectSocket); buttonConnectAndReceiveMsg.Enabled = false; } void Connected(IAsyncResult result) { clientCommunicateSocket = result.AsyncState as Socket; clientCommunicateSocket.EndConnect(result); clientCommunicateSocket.BeginReceive(bytesReceivedFromServer, 0, clientBufferSize, SocketFlags.None, new AsyncCallback(this.ReceivedFromServer), null); ListBoxClientShow("客户端连接上服务器。。。"); //连接成功便发送密钥K给服务器 SendKey(); } void ReceivedFromServer(IAsyncResult result) { int read = clientCommunicateSocket.EndReceive(result); if (read > 0) { messageFromServer.Append(UTF32Encoding.Default.GetString(bytesReceivedFromServer, 0, read)); //处理并显示客户端数据 ProcessAndShowInClient(); clientCommunicateSocket.BeginReceive(bytesReceivedFromServer, 0, clientBufferSize, 0, new AsyncCallback(ReceivedFromServer), null); } } private void ProcessAndShowInClient() { //如果接收到<EOF>则表示完成一次接收,否则继续将自己置于接收状态 if (messageFromServer.ToString().IndexOf("<EOF>") > -1) { //解密消息体并呈现出来 ListBoxClientShow(string.Format("接收到服务器消息:{0}", RijndaelProcessor.DencryptString(messageFromServer.ToString().Substring(0, messageFromServer.ToString().Length - 5), keyCreateRandom))); messageFromServer.Clear(); } } private void buttonStartSendToServer_Click(object sender, EventArgs e) { //加密消息体 string msg = string.Format("{0}{1}", RijndaelProcessor.EncryptString(DateTime.Now.ToString(), keyCreateRandom), "<EOF>"); byte[] msgBytes = UTF32Encoding.Default.GetBytes(msg); clientCommunicateSocket.BeginSend(msgBytes, 0, msgBytes.Length, SocketFlags.None, null, null); ListBoxClientShow(string.Format("发送:{0}", msg)); } private void SendKey() { string msg = RSAProcessor.RSAEncrypt(publicKey, keyCreateRandom) + "<KEY><EOF>"; byte[] msgBytes = UTF32Encoding.Default.GetBytes(msg); clientCommunicateSocket.BeginSend(msgBytes, 0, msgBytes.Length, SocketFlags.None, null, null); ListBoxClientShow(string.Format("发送:{0}", keyCreateRandom)); } private void ListBoxClientShow(string msg) { listBoxClient.BeginInvoke(new Action(() => { listBoxClient.Items.Add(msg); })); } #endregion client
客户端部分也包含两个按钮,在服务器部分按下“侦听”按钮后,客户端可以按下“链接”按钮。这个过程,程序主要完成两件事情。首先,程序会根据服务器IP地址连接上服务器;其次,一旦连接上服务器,客户端会立即将自己用于加密的密钥发送给服务器。
完成这个步骤后,SSL通道已经建立起来的,这个时候就可以随意发送加密数据而不担心被盗走了。我们可以看到,客户端的代码与服务器端一样,在发送之前,消息要加密,而在接收到消息体之后,首先会解密。
转自:《编写高质量代码改善C#程序的157个建议》陆敏技