數據在網絡傳輸過程中的保密性是網絡安全中重點要考慮的問題之一。由於通過網絡傳遞數據是在不安全的信道上進行傳輸的,因此通信雙方要想確保任何可能正在偵聽的人無法理解通信的內容,而且希望確保接收方接收的信息沒有在傳輸期間被任何人修改,最好的辦法就是在傳輸數據前對數據進行加密,接收方接收到加密的數據后再進行解密處理,從而保證數據的安全性。
在.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 提供的數字簽名技術,接收者可以核實發送者對報文的簽名,防止發送者抵賴對數據的簽名,同時也避免了攻擊者偽造對報文的簽名。