轉載自:http://www.tracefact.net/tech/037.html
張子陽大大寫的一些文章排版看得很舒服,也很好理解,講得非常清楚
1. 引言
在一些比較重要的應用場景中,通過網絡傳遞數據需要進行加密以保證安全。本文將簡單地介紹了加密解密的一些概念,以及相關的數字簽名、證書,最后介紹了如何在.NET中對數據進行對稱加密和解密。
2. 加密和解密
說到加密,可能大家最熟悉的就是MD5了,記得幾年前我剛開始接觸Web編程的時候,研究的一個ASP論壇程序,它的用戶密碼就是采用的MD5進行加密。MD5實際上只是一種散列運算,或者可以稱為單向的加密,即是說無法根據密文(加密后的數據),推導出明文(原數據)。而我們下面要說明的,是在加密后可以進行解密、還原數據的。對於欲進行加密的對象,有的人稱為消息,有的人稱為數據,有的人稱為信息,為了避免混淆,在本文后面部分,我統一將其稱為消息。那么加密是什么呢?加密是通過對消息進行編碼,建立一種安全的交流方式,使得只有你和你所期望的接收者能夠理解。
那么怎么樣才能叫安全呢?消息在接收方和發送方進行安全傳遞,一般要滿足下面三個要點:
- 消息的發送方能夠確定消息只有預期的接收方可以解密(不保證第三方無法獲得,但保證第三方無法解密)。
- 消息的接收方可以確定消息是由誰發送的(消息的接收方可以確定消息的發送方)。
- 消息的接收方可以確定消息在途中沒有被篡改過(必須確認消息的完整性)。
加密通常分為兩種方式:對稱加密和非對稱加密,接下來我們先看看對稱加密。
2.1 對稱加密
對稱加密的思路非常簡單,就是含有一個稱為密鑰的東西,在消息發送前使用密鑰對消息進行加密,在對方收到消息之后,使用相同的密鑰進行解密。根據密鑰來產生加密后的消息(密文)的這一加工過程,由加密算法來完成,加密算法通常是公開的。它的流程如下:
- 發送方使用密鑰對消息進行加密。
- 接收方使用同樣的密鑰對消息進行解密。
可以使用下面一副圖來表示:
對稱加密存在這樣兩個問題:
- 雖然可以通過密鑰來保證消息安全地進行傳遞,但是如何確保密鑰安全地進行傳遞?因為發送者和接收者總有一次初始的通信,用來傳遞密鑰,此時的安全如何保證?
- 接收者雖然可以根據密鑰來解密消息,但因為存在上面的問題,消息有可能是由第三方(非法獲得密鑰)發來的,而接收方無法辨別。
為了解決上面兩個問題,就需要介紹一下非對稱加密。
2.2 非對稱加密
非對稱加密的接收者和發送者都持有兩個密鑰,一個是對外公開的,稱為公鑰,一個是自行保管的,稱為私鑰。非對稱加密的規則是由某人A的公鑰加密的消息,只能由A的私鑰進行解密;由A的私鑰加密的消息只能由A的公鑰解密。此時我們可以得出接收方、發送方有兩個公鑰兩個私鑰一共四個密鑰,我們先看看兩種簡單的方式,這兩種方式都是只使用兩個密鑰。
第一種模式只使用接收方的公鑰和私鑰,稱為加密模式。
a. 加密模式
在加密模式中,由消息的接收方發布公鑰,持有私鑰。比如發送方要發送消息“hello,jimmy”到接收方,它的步驟是:
- 發送方使用接收者的公鑰進行加密消息,然后發送。
- 接收方使用自己的私鑰對消息進行解密。
可以使用下面一幅圖來描述:
在這種模式下,如果第三方截獲了發送者發出的消息,因為他沒有接收者的私鑰,所以這個消息對他來說毫無意義。可見,它能夠滿足本文最開始提出的消息安全傳遞的要點一:消息的發送方能夠確定消息只有預期的接收方可以解密(不保證第三方無法獲得,但保證第三方無法解密)。
除此以外,因為接收方的公鑰是公開的,任何人都可以使用這個公鑰來加密消息並發往接收者,而接收者無法對消息進行判別,無法知道是由誰發送來的。所以,它不滿足我們開始提出的消息安全傳遞的要點二:消息的接收方可以確定消息是由誰發送的(消息的接收方可以確定消息的發送方)。
這個問題可以在下面的認證模式中得到解決。
b. 認證模式
在認證模式中,由消息的發送方發布公鑰,持有私鑰。比如發送者要發送消息“Welcome to Tracefact.net”到接收者,它的步驟是:
- 發送者使用自己的私鑰對消息進行加密,然后發送。
- 接收者使用發送者的公鑰對消息進行解密。
可以用下面一副圖來表述:
在這種模式下,假如發送方叫做Ken,接收方叫做Matthew,因為Matthew只能使用Ken的公鑰對消息進行解密,而無法使用Molly、Sandy或者任何其他人公鑰對消息進行解密,所以他一定能夠確定消息是由Ken發送來的。因此,這個模式滿足了前面提出的消息安全傳遞的要點二。
與此同時,因為Ken的公鑰是公開的,任何截獲了該消息的第三方都能夠使用Ken的公鑰來對消息進行解密,換言之,消息現在是不安全的。因此,與加密模式正好相反,它無法滿足前面提出的消息安全傳遞的要點一。
而不管是采用加密模式還是認證模式,都沒有解決加密解密中的要點三:接收方必須能夠確認消息沒有被改動過。為了解決這個問題,又引入了數字簽名。
3. 數字簽名
3.1 基本實現
數字簽名實際上就是上面非對稱加密時的認證模式,只不過做了一點點的改進,加入了散列算法。大家比較熟悉的散列算法可能就是MD5了,很多開源論壇都采用了這個算法。散列算法有三個特點:一是不可逆的,由結果無法推算出原數據;二是原數據哪怕是一丁點兒的變化,都會使散列值產生巨大的變化;三是不論多么大或者多么少的數據,總會產生固定長度的散列值(常見的為32位64位)。產生的散列值通常稱為消息的摘要(digest)。
那么如何通過引入散列函數來保證數據的完整性呢?也就是接收方能夠確認消息確實是由發送方發來的,而沒有在中途被修改過。具體的過程如下:
- 發送方將想要進行傳遞的消息進行一個散列運算,得到消息摘要。
- 發送方使用自己的私鑰對摘要進行加密,將消息和加密后的摘要發送給接收方。
- 接收方使用發送方的公鑰對消息和消息摘要進行解密(確認了發送方)。
- 接收方對收到的消息進行散列運算,得到一個消息摘要。
- 接收方將上一步獲得的消息摘要與發送方發來的消息摘要進行對比。如果相同,說明消息沒有被改動過;如果不同,說明消息已經被篡改。
這個過程可以用下面的一副圖來表述:
我們可以看出,數字簽名通過引入散列算法,將非對稱加密的認證模式又加強了一步,確保了消息的完整性。除此以外,注意到上面的非對稱加密算法,只是對消息摘要進行了加密,而沒有對消息本身進行加密。非對稱加密是一個非常耗時的操作,由於只對消息摘要加密,使得運算量大幅減少,所以這樣能夠顯著地提高程序的執行速度。同時,它依然沒有確保消息不被第三方截獲到,不僅如此,因為此時消息是以明文進行傳遞,第三方甚至不需要發送方的公鑰,就可以直接查看消息。
為了解決這樣的問題,只需要將非對稱加密的認證模式、加密模式以及消息摘要進行一個結合就可以了,這也就是下面的高級模式。
3.2 高級實現
由於這個過程比上面稍微復雜了一些,我們將其分為發送方和接收方兩部分來看。先看看發送方需要執行的步驟:
- 將消息進行散列運算,得到消息摘要。
- 使用自己的私鑰對消息摘要加密(認證模式:確保了接收方能夠確認自己)。
- 使用接收方的公鑰對消息進行加密(加密模式:確保了消息只能由期望的接收方解密)。
- 發送消息和消息摘要。
接下來我們看一下接收方所執行的步驟:
- 使用發送方的公鑰對消息摘要進行解密(確認了消息是由誰發送的)。
- 使用自己的私鑰對消息進行解密(安全地獲得了實際應獲得的信息)。
- 將消息進行散列運算,獲得消息摘要。
- 將上一步獲得的消息摘要 和 第一步解密的消息摘要進行對比(確認了消息是否被篡改)。
可以看到,通過上面這種方式,使用了接收方、發送方全部的四個密鑰,再配合使用消息摘要,使得前面提出的安全傳遞的所有三個條件全都滿足了。那么是不是這種方法就是最好的呢?不是的,因為我們已經說過了,非對稱加密是一種很耗時的操作,所以這個方案是很低效的。實際上,我們可以通過它來解決對稱加密中的密鑰傳遞問題,如果你已經忘記了可以翻到前面再看一看,也就是說,我們可以使用這里的高級實現方式來進行對稱加密中密鑰的傳遞,對於之后實際的數據傳遞,采用對稱加密方式來完成,因為此時已經是安全的了。
3.3 證書機制
與數字簽名相關的一個概念就是證書機制了,證書是用來做什么呢?在上面的各種模式中,我們一直使用了這樣一個假設,就是接收方或者發送方所持有的、對方的公鑰總是正確的(確實是對方公布的)。而實際上除非對方手把手將公鑰交給我們,否則如果不采取措施,雙方在網絡中傳遞公鑰時,一樣有可能被篡改。那么怎樣解決這個問題呢?這時就需要證書機制了:可以引入一個公正的第三方,當某一方想要發布公鑰時,它將自身的身份信息及公鑰提交給這個第三方,第三方對其身份進行證實,如果沒有問題,則將其信息和公鑰打包成為證書(Certificate)。而這個公正的第三方,就是常說的證書頒發機構(Certificate Authority)。當我們需要獲取公鑰時,只需要獲得其證書,然后從中提取出公鑰就可以了。
4. .NET中加密解密的支持
4.1 對稱加密和解密
相信通過前面幾頁的敘述,大家已經明白了加密解密、數字簽名的基本原理,下面我們看一下在.NET中是如何來支持加密解密的。正如上面我們所進行的分類,.NET中也提供了兩組類用於加密解密,一組為對稱加密,一組為非對稱加密,如下圖所示:
上面的類按照名稱還可以分為兩組,一組后綴為“CryptoServiceProvider”的,是對於底層Windows API的包裝類,一組后綴為“Managed”,是在.NET中全新編寫的類。現在假設我們以TripleDES作為算法,那么加密的流程如下:
- 先創建一個TripleDESCryptoServiceProvider的實例,實例名比如叫provider。
- 在provider上指定密鑰和IV,也就是它的Key屬性和IV屬性。這里簡單解釋一下IV(initialization vector),如果一個字符串(或者數據)加密之前很多部分是重復的比如ABCABCABC,那么加密之后盡管字符串是亂碼,但相關部分也是重復的。為了解決這個問題,就引入了IV,當使用它以后,加密之后即使是重復的也被打亂了。對於特定算法,密鑰和IV的值可以隨意指定,但長度是固定,通常密鑰為128位或196位,IV為64位。密鑰和IV都是byte[]類型,因此,如果使用Encoding類來將字符串轉換為byte[],那么編碼方式就很重要,因為UTF8是變長編碼,所以對於中文和英文,需要特別注意byte[]的長度問題。
- 如果是加密,在provider上調用CreateEncryptor()方法,創建一個ICryptoTransform類型的加密器對象;如果是解密,在provider上調用CreateDecryptor()方法,同樣是創建一個ICryptoTransform類型的解密器對象。ICryptoTransform定義了加密轉換的運算,.NET將在底層調用這個接口。
- 因為流和byte[]是數據類型無關的一種數據結構,可以保存和傳輸任何形式的數據,區別只是byte[]是一個靜態的概念而流是一個動態的概念。因此,.NET采用了流的方式進行加密和解密,我們可以想到有兩個流,一個是明文流,含有加密前的數據;一個是密文流,含有加密后的數據。那么就必然有一個中介者,將明文流轉換為密文流;或者將密文流轉換為明文流。.NET中執行這個操作的中介者也是一個流類型,叫做CryptoStream。它的構造函數如下,共有三個參數:
public CryptoStream(Stream stream, ICryptoTransform transform, CryptoStreamMode mode)
5.當加密時,stream為密文流(注意此時密文流還沒有包含數據,僅僅是一個空流);ICryptoTransform是第3步創建的加密器,包含着加密的算法;CryptoStreamMode枚舉為Write,意思是將流經CryptoStream的明文流寫入到密文流中。最后,從密文流中獲得加密后的數據。
6.當解密時,stream為密文流(此時密文流含有數據);ICryptoTransform是第3步創建的解密器,包含着解密的算法;CryptoStreamMode枚舉為Read,意思是將密文流中的數據讀出到byte[]數組中,進而再由byte[]轉換為明文流、明文字符串。
可見,CryptoStream總是接受密文流,並且根據CryptoStreamMode枚舉的值來決定是將明文流寫入到密文流(加密),還是將密文流讀入到明文流中(解密)。下面是我編寫的一個加密解密的Helper類:
// 對稱加密幫助類 public class CryptoHelper { // 對稱加密算法提供器 private ICryptoTransform encryptor; // 加密器對象 private ICryptoTransform decryptor; // 解密器對象 private const int BufferSize = 1024; public CryptoHelper(string algorithmName, string key) { SymmetricAlgorithm provider = SymmetricAlgorithm.Create(algorithmName); provider.Key = Encoding.UTF8.GetBytes(key); provider.IV = new byte[] { 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF }; encryptor = provider.CreateEncryptor(); decryptor = provider.CreateDecryptor(); } public CryptoHelper(string key) : this("TripleDES", key) { } // 加密算法 public string Encrypt(string clearText) { // 創建明文流 byte[] clearBuffer = Encoding.UTF8.GetBytes(clearText); MemoryStream clearStream = new MemoryStream(clearBuffer); // 創建空的密文流 MemoryStream encryptedStream = new MemoryStream(); CryptoStream cryptoStream = new CryptoStream(encryptedStream, encryptor, CryptoStreamMode.Write); // 將明文流寫入到buffer中 // 將buffer中的數據寫入到cryptoStream中 int bytesRead = 0; byte[] buffer = new byte[BufferSize]; do { bytesRead = clearStream.Read(buffer, 0, BufferSize); cryptoStream.Write(buffer, 0, bytesRead); } while (bytesRead > 0); cryptoStream.FlushFinalBlock(); // 獲取加密后的文本 buffer = encryptedStream.ToArray(); string encryptedText = Convert.ToBase64String(buffer); return encryptedText; } // 解密算法 public string Decrypt(string encryptedText) { byte[] encryptedBuffer = Convert.FromBase64String(encryptedText); Stream encryptedStream = new MemoryStream(encryptedBuffer); MemoryStream clearStream = new MemoryStream(); CryptoStream cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read); int bytesRead = 0; byte[] buffer = new byte[BufferSize]; do { bytesRead = cryptoStream.Read(buffer, 0, BufferSize); clearStream.Write(buffer, 0, bytesRead); } while (bytesRead > 0); buffer = clearStream.GetBuffer(); string clearText = Encoding.UTF8.GetString(buffer, 0, (int)clearStream.Length); return clearText; } public static string Encrypt(string clearText, string key) { CryptoHelper helper = new CryptoHelper(key); return helper.Encrypt(clearText); } public static string Decrypt(string encryptedText, string key) { CryptoHelper helper = new CryptoHelper(key); return helper.Decrypt(encryptedText); } }
我們可以對上面這個類進行一個簡單的測試:
static void Main(string[] args) { string key = "ABCDEFGHIJKLMNOP"; string clearText = "歡迎訪問www.tracefact.net"; CryptoHelper helper = new CryptoHelper(key); string encryptedText = helper.Encrypt(clearText); Console.WriteLine(encryptedText); clearText = CryptoHelper.Decrypt(encryptedText, key); Console.WriteLine(clearText); }
應該可以看到下面的輸出結果:
5. 總結
首先向大家表示歉意,我並沒有寫.NET中非對稱加密的部分,因為我很少用到,所以這部分我並不是很熟悉,但是原理現在應該已經很清楚了,我想等到需要的時候再去學習如何來使用它們。到那個時候,我也會對這篇文章再次進行更新。通過這篇文章,相信大家對於加密、解密、數字簽名等這些安全方面的概念已經有了一個初步的認識,同時也學習到了如何在.NET下進行對稱加密。
感謝閱讀,希望這篇文章能給你帶來幫助!