.Net中的加密解密
引言
在一些比較重要的應用場景中,通過網絡傳遞資料需要進行加密以保證安全。本文将簡單地介紹了加密解密的一些概念,以及相關的數字簽名、證書,最後介紹了如何在.NET中對資料進行對稱加密和解密。
加密和解密
說到加密,可能大家最熟悉的就是MD5了,記得幾年前我剛開始接觸Web程式設計的時候,研究的一個ASP論壇程式,它的使用者密碼就是采用的MD5進行加密。MD5實際上隻是一種散列運算,或者可以稱為單向的加密,即是說無法根據密文(加密後的資料),推導出明文(原資料)。而我們下面要說明的,是在加密後可以進行解密、還原資料的。對于欲進行加密的對象,有的人稱為消息,有的人稱為資料,有的人稱為資訊,為了避免混淆,在本文後面部分,我統一将其稱為
消息。那麼加密是什麼呢?加密是通過對消息進行編碼,建立一種安全的交流方式,使得隻有你和你所期望的接收者能夠了解。
那麼怎麼樣才能叫安全呢?消息在接收方和發送方進行安全傳遞,一般要滿足下面三個要點:
- 消息的發送方能夠确定消息隻有預期的接收方可以解密(不保證第三方無法獲得,但保證第三方無法解密)。
- 消息的接收方可以确定消息是由誰發送的(消息的接收方可以确定消息的發送方)。
- 消息的接收方可以确定消息在途中沒有被篡改過(必須确認消息的完整性)。
加密通常分為兩種方式:對稱加密和非對稱加密,接下來我們先看看對稱加密。
對稱加密
對稱加密的思路非常簡單,就是含有一個稱為
密鑰的東西,在消息發送前使用密鑰對消息進行加密,在對方收到消息之後,使用
相同的密鑰進行解密。根據密鑰來産生加密後的消息(密文)的這一加工過程,由
加密算法來完成
,加密算法通常是公開的。它的流程如下:
- 發送方使用密鑰對消息進行加密。
- 接收方使用同樣的密鑰對消息進行解密。
可以使用下面一副圖來表示:

對稱加密存在這樣兩個問題:
- 雖然可以通過密鑰來保證消息安全地進行傳遞,但是如何確定密鑰安全地進行傳遞?因為發送者和接收者總有一次初始的通信,用來傳遞密鑰,此時的安全如何保證?
- 接收者雖然可以根據密鑰來解密消息,但因為存在上面的問題,消息有可能是由第三方(非法獲得密鑰)發來的,而接收方無法辨識。
為了解決上面兩個問題,就需要介紹一下非對稱加密。
非對稱加密
非對稱加密的
接收者和發送者都持有兩個密鑰,一個是對外公開的,稱為
公鑰,一個是自行保管的,稱為
私鑰。非對稱加密的規則是
由某人A的公鑰加密的消息,隻能由A的私鑰進行解密;由A的私鑰加密的消息隻能由A的公鑰解密。此時我們可以得出接收方、發送方有兩個公鑰兩個私鑰一共四個密鑰,我們先看看兩種簡單的方式,這兩種方式都是隻使用兩個密鑰。
第一種模式隻使用接收方的公鑰和私鑰,稱為加密模式。
加密模式
在加密模式中,由消息的
接收方釋出公鑰,持有私鑰。比如發送方要發送消息“hello,jimmy”到接收方,它的步驟是:
- 發送方使用接收者的公鑰進行加密消息,然後發送。
- 接收方使用自己的私鑰對消息進行解密。
可以使用下面一幅圖來描述:
在這種模式下,如果第三方截獲了發送者發出的消息,因為他沒有接收者的私鑰,是以這個消息對他來說毫無意義。可見,它能夠滿足本文最開始提出的消息安全傳遞的要點一:
消息的發送方能夠确定消息隻有預期的接收方可以解密(不保證第三方無法獲得,但保證第三方無法解密)。
除此以外,因為接收方的公鑰是公開的,任何人都可以使用這個公鑰來加密消息并發往接收者,而接收者無法對消息進行判别,無法知道是由誰發送來的。是以,它不滿足我們開始提出的消息安全傳遞的要點二:
這個問題可以在下面的認證模式中得到解決。
認證模式
在認證模式中,由消息的
發送方釋出公鑰,持有私鑰。比如發送者要發送消息“Welcome to Tracefact.net”到接收者,它的步驟是:
- 發送者使用自己的私鑰對消息進行加密,然後發送。
- 接收者使用發送者的公鑰對消息進行解密。
可以用下面一副圖來表述:
在這種模式下,假如發送方叫做Ken,接收方叫做Matthew,因為Matthew隻能使用Ken的公鑰對消息進行解密,而無法使用Molly、Sandy或者任何其他人公鑰對消息進行解密,是以他一定能夠确定消息是由Ken發送來的。是以,這個模式滿足了前面提出的消息安全傳遞的要點二。
與此同時,因為Ken的公鑰是公開的,任何截獲了該消息的第三方都能夠使用Ken的公鑰來對消息進行解密,換言之,消息現在是
不安全的。是以,與加密模式正好相反,它無法滿足前面提出的消息安全傳遞的要點一。
而不管是采用加密模式還是認證模式,都沒有解決加密解密中的要點三:接收方必須能夠确認消息沒有被改動過。為了解決這個問題,又引入了數字簽名。
數字簽名
基本實作
數字簽名實際上就是上面非對稱加密時的認證模式,隻不過做了一點點的改進,加入了雜湊演算法。大家比較熟悉的雜湊演算法可能就是MD5了,很多開源論壇都采用了這個算法。雜湊演算法有三個特點:一是不可逆的,由結果無法推算出原資料;二是原資料哪怕是一丁點兒的變化,都會使散列值産生巨大的變化;三是不論多麼大或者多麼少的資料,總會産生固定長度的散列值(常見的為32位64位)。産生的散列值通常稱為消息的
摘要(digest)。
那麼如何通過引入散列函數來保證資料的完整性呢?也就是接收方能夠确認消息确實是由發送方發來的,而沒有在中途被修改過。具體的過程如下:
- 發送方将想要進行傳遞的消息進行一個散列運算,得到消息摘要。
- 發送方使用自己的私鑰對摘要進行加密,将消息和加密後的摘要發送給接收方。
- 接收方使用發送方的公鑰對消息和消息摘要進行解密(确認了發送方)。
- 接收方對收到的消息進行散列運算,得到一個消息摘要。
- 接收方将上一步獲得的消息摘要與發送方發來的消息摘要進行對比。如果相同,說明消息沒有被改動過;如果不同,說明消息已經被篡改。
這個過程可以用下面的一副圖來表述:
我們可以看出,數字簽名通過引入雜湊演算法,将非對稱加密的認證模式又加強了一步,確定了消息的完整性。除此以外,注意到上面的非對稱加密算法,
隻是對消息摘要進行了加密,而沒有對消息本身進行加密。非對稱加密是一個非常耗時的操作,由于隻對消息摘要加密,使得運算量大幅減少,是以這樣能夠顯著地提高程式的執行速度。同時,它依然沒有確定消息不被第三方截獲到,不僅如此,因為此時消息是以明文進行傳遞,第三方甚至不需要發送方的公鑰,就可以直接檢視消息。
為了解決這樣的問題,隻需要将非對稱加密的認證模式、加密模式以及消息摘要進行一個結合就可以了,這也就是下面的進階模式。
進階實作
由于這個過程比上面稍微複雜了一些,我們将其分為發送方和接收方兩部分來看。先看看
需要執行的步驟:
- 将消息進行散列運算,得到消息摘要。
- 使用自己的私鑰對消息摘要加密(認證模式:確定了接收方能夠确認自己)。
- 使用接收方的公鑰對消息進行加密(加密模式:確定了消息隻能由期望的接收方解密)。
- 發送消息和消息摘要。
接下來我們看一下接收方所執行的步驟:
- 使用發送方的公鑰對消息摘要進行解密(确認了消息是由誰發送的)。
- 使用自己的私鑰對消息進行解密(安全地獲得了實際應獲得的資訊)。
- 将消息進行散列運算,獲得消息摘要。
- 将上一步獲得的消息摘要 和 第一步解密的消息摘要進行對比(确認了消息是否被篡改)。
可以看到,通過上面這種方式,使用了接收方、發送方全部的四個密鑰,再配合使用消息摘要,使得前面提出的安全傳遞的所有三個條件全都滿足了。那麼是不是這種方法就是最好的呢?不是的,因為我們已經說過了,非對稱加密是一種很耗時的操作,是以這個方案是很低效的。實際上,我們可以通過它來解決對稱加密中的密鑰傳遞問題,如果你已經忘記了可以翻到前面再看一看,也就是說,我們可以使用這裡的進階實作方式來進行對稱加密中密鑰的傳遞,對于之後實際的資料傳遞,采用對稱加密方式來完成,因為此時已經是安全的了。
證書機制
與數字簽名相關的一個概念就是證書機制了,證書是用來做什麼呢?在上面的各種模式中,我們一直使用了這樣一個假設,就是接收方或者發送方所持有的、對方的公鑰總是正确的(确實是對方公布的)。而實際上除非對方手把手将公鑰交給我們,否則如果不采取措施,雙方在網絡中傳遞公鑰時,一樣有可能被篡改。那麼怎樣解決這個問題呢?這時就需要證書機制了:可以引入一個
公正的第三方,當某一方想要釋出公鑰時,它将自身的身份資訊及公鑰送出給這個第三方,第三方對其身份進行證明,如果沒有問題,則将其資訊和公鑰打包成為
證書(Certificate)。而這個公正的第三方,就是常說的
證書頒發機構(Certificate Authority)。當我們需要擷取公鑰時,隻需要獲得其證書,然後從中提取出公鑰就可以了。
.NET中加密解密的支援
對稱加密和解密
相信通過前面幾頁的叙述,大家已經明白了加密解密、數字簽名的基本原理,下面我們看一下在.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)
當加密時,stream為密文流(注意此時密文流還沒有包含資料,僅僅是一個空流);ICryptoTransform是第3步建立的加密器,包含着加密的算法;CryptoStreamMode枚舉為Write,意思是将流經CryptoStream的明文流寫入到密文流中。最後,從密文流中獲得加密後的資料。
當解密時,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();
new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read);
bytesRead = cryptoStream.Read(buffer, 0, BufferSize);
clearStream.Write(buffer, 0, bytesRead);
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) {
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);
應該可以看到下面的輸出結果:
總結
首先向大家表示歉意,我并沒有寫.NET中非對稱加密的部分,因為我很少用到,是以這部分我并不是很熟悉,但是原理現在應該已經很清楚了,我想等到需要的時候再去學習如何來使用它們。到那個時候,我也會對這篇文章再次進行更新。通過這篇文章,相信大家對于加密、解密、數字簽名等這些安全方面的概念已經有了一個初步的認識,同時也學習到了如何在.NET下進行對稱加密。
感謝閱讀,希望這篇文章能給你帶來幫助!