.Net中的加密解密

原文來自:http://www.tracefact.net/CLR-and-Framework/Cryptograph.aspx


引言

在一些比較重要的應用場景中,通過網絡傳遞數據需要進行加密以保證安全。本文將簡單地介紹了加密解密的一些概念,以及相關的數字簽名、證書,最後介紹瞭如何在.NET中對數據進行對稱加密和解密。

加密和解密

說到加密,可能大家最熟悉的就是MD5了,記得幾年前我剛開始接觸Web編程的時候,研究的一個ASP論壇程序,它的用戶密碼就是採用的MD5進行加密。MD5實際上只是一種散列運算,或者可以稱爲單向的加密,即是說無法根據密文(加密後的數據),推導出明文(原數據)。而我們下面要說明的,是在加密後可以進行解密、還原數據的。對於欲進行加密的對象,有的人稱爲消息,有的人稱爲數據,有的人稱爲信息,爲了避免混淆,在本文後面部分,我統一將其稱爲消息。那麼加密是什麼呢?加密是通過對消息進行編碼,建立一種安全的交流方式,使得只有你和你所期望的接收者能夠理解。

那麼怎麼樣才能叫安全呢?消息在接收方和發送方進行安全傳遞,一般要滿足下面三個要點:

  1. 消息的發送方能夠確定消息只有預期的接收方可以解密(不保證第三方無法獲得,但保證第三方無法解密)。
  2. 消息的接收方可以確定消息是由誰發送的(消息的接收方可以確定消息的發送方)。
  3. 消息的接收方可以確定消息在途中沒有被篡改過(必須確認消息的完整性)。

加密通常分爲兩種方式:對稱加密和非對稱加密,接下來我們先看看對稱加密。

對稱加密

對稱加密的思路非常簡單,就是含有一個稱爲密鑰的東西,在消息發送前使用密鑰對消息進行加密,在對方收到消息之後,使用相同的密鑰進行解密。根據密鑰來產生加密後的消息(密文)的這一加工過程,由加密算法來完成加密算法通常是公開的。它的流程如下:

  1. 發送方使用密鑰對消息進行加密。
  2. 接收方使用同樣的密鑰對消息進行解密。

可以使用下面一副圖來表示:

對稱加密存在這樣兩個問題:

  1. 雖然可以通過密鑰來保證消息安全地進行傳遞,但是如何確保密鑰安全地進行傳遞?因爲發送者和接收者總有一次初始的通信,用來傳遞密鑰,此時的安全如何保證?
  2. 接收者雖然可以根據密鑰來解密消息,但因爲存在上面的問題,消息有可能是由第三方(非法獲得密鑰)發來的,而接收方無法辨別。

爲了解決上面兩個問題,就需要介紹一下非對稱加密。

非對稱加密

非對稱加密的接收者和發送者都持有兩個密鑰,一個是對外公開的,稱爲公鑰,一個是自行保管的,稱爲私鑰。非對稱加密的規則是由某人A的公鑰加密的消息,只能由A的私鑰進行解密;由A的私鑰加密的消息只能由A的公鑰解密。此時我們可以得出接收方、發送方有兩個公鑰兩個私鑰一共四個密鑰,我們先看看兩種簡單的方式,這兩種方式都是隻使用兩個密鑰。

第一種模式只使用接收方的公鑰和私鑰,稱爲加密模式。

加密模式

在加密模式中,由消息的接收方發佈公鑰,持有私鑰。比如發送方要發送消息“hello,jimmy”到接收方,它的步驟是:

  1. 發送方使用接收者的公鑰進行加密消息,然後發送。
  2. 接收方使用自己的私鑰對消息進行解密。

可以使用下面一幅圖來描述:

在這種模式下,如果第三方截獲了發送者發出的消息,因爲他沒有接收者的私鑰,所以這個消息對他來說毫無意義。可見,它能夠滿足本文最開始提出的消息安全傳遞的要點一:消息的發送方能夠確定消息只有預期的接收方可以解密(不保證第三方無法獲得,但保證第三方無法解密)

除此以外,因爲接收方的公鑰是公開的,任何人都可以使用這個公鑰來加密消息併發往接收者,而接收者無法對消息進行判別,無法知道是由誰發送來的。所以,它不滿足我們開始提出的消息安全傳遞的要點二:消息的接收方可以確定消息是由誰發送的(消息的接收方可以確定消息的發送方)。

這個問題可以在下面的認證模式中得到解決。

認證模式

在認證模式中,由消息的發送方發佈公鑰,持有私鑰。比如發送者要發送消息“Welcome to Tracefact.net”到接收者,它的步驟是:

  1. 發送者使用自己的私鑰對消息進行加密,然後發送。
  2. 接收者使用發送者的公鑰對消息進行解密。

可以用下面一副圖來表述:

在這種模式下,假如發送方叫做Ken,接收方叫做Matthew,因爲Matthew只能使用Ken的公鑰對消息進行解密,而無法使用Molly、Sandy或者任何其他人公鑰對消息進行解密,所以他一定能夠確定消息是由Ken發送來的。因此,這個模式滿足了前面提出的消息安全傳遞的要點二。

與此同時,因爲Ken的公鑰是公開的,任何截獲了該消息的第三方都能夠使用Ken的公鑰來對消息進行解密,換言之,消息現在是不安全的。因此,與加密模式正好相反,它無法滿足前面提出的消息安全傳遞的要點一。

而不管是採用加密模式還是認證模式,都沒有解決加密解密中的要點三:接收方必須能夠確認消息沒有被改動過。爲了解決這個問題,又引入了數字簽名。

數字簽名

基本實現

數字簽名實際上就是上面非對稱加密時的認證模式,只不過做了一點點的改進,加入了散列算法。大家比較熟悉的散列算法可能就是MD5了,很多開源論壇都採用了這個算法。散列算法有三個特點:一是不可逆的,由結果無法推算出原數據;二是原數據哪怕是一丁點兒的變化,都會使散列值產生巨大的變化;三是不論多麼大或者多麼少的數據,總會產生固定長度的散列值(常見的爲32位64位)。產生的散列值通常稱爲消息的摘要(digest)。

那麼如何通過引入散列函數來保證數據的完整性呢?也就是接收方能夠確認消息確實是由發送方發來的,而沒有在中途被修改過。具體的過程如下:

  1. 發送方將想要進行傳遞的消息進行一個散列運算,得到消息摘要。
  2. 發送方使用自己的私鑰對摘要進行加密,將消息和加密後的摘要發送給接收方。
  3. 接收方使用發送方的公鑰對消息和消息摘要進行解密(確認了發送方)。
  4. 接收方對收到的消息進行散列運算,得到一個消息摘要。
  5. 接收方將上一步獲得的消息摘要與發送方發來的消息摘要進行對比。如果相同,說明消息沒有被改動過;如果不同,說明消息已經被篡改。

這個過程可以用下面的一副圖來表述:

我們可以看出,數字簽名通過引入散列算法,將非對稱加密的認證模式又加強了一步,確保了消息的完整性。除此以外,注意到上面的非對稱加密算法,只是對消息摘要進行了加密,而沒有對消息本身進行加密。非對稱加密是一個非常耗時的操作,由於只對消息摘要加密,使得運算量大幅減少,所以這樣能夠顯著地提高程序的執行速度。同時,它依然沒有確保消息不被第三方截獲到,不僅如此,因爲此時消息是以明文進行傳遞,第三方甚至不需要發送方的公鑰,就可以直接查看消息。

爲了解決這樣的問題,只需要將非對稱加密的認證模式、加密模式以及消息摘要進行一個結合就可以了,這也就是下面的高級模式。

高級實現

由於這個過程比上面稍微複雜了一些,我們將其分爲發送方和接收方兩部分來看。先看看發送方需要執行的步驟:

  1. 將消息進行散列運算,得到消息摘要。
  2. 使用自己的私鑰對消息摘要加密(認證模式:確保了接收方能夠確認自己)。
  3. 使用接收方的公鑰對消息進行加密(加密模式:確保了消息只能由期望的接收方解密)。
  4. 發送消息和消息摘要。

接下來我們看一下接收方所執行的步驟:

  1. 使用發送方的公鑰對消息摘要進行解密(確認了消息是由誰發送的)。
  2. 使用自己的私鑰對消息進行解密(安全地獲得了實際應獲得的信息)。
  3. 將消息進行散列運算,獲得消息摘要。
  4. 將上一步獲得的消息摘要 和 第一步解密的消息摘要進行對比(確認了消息是否被篡改)。

可以看到,通過上面這種方式,使用了接收方、發送方全部的四個密鑰,再配合使用消息摘要,使得前面提出的安全傳遞的所有三個條件全都滿足了。那麼是不是這種方法就是最好的呢?不是的,因爲我們已經說過了,非對稱加密是一種很耗時的操作,所以這個方案是很低效的。實際上,我們可以通過它來解決對稱加密中的密鑰傳遞問題,如果你已經忘記了可以翻到前面再看一看,也就是說,我們可以使用這裏的高級實現方式來進行對稱加密中密鑰的傳遞,對於之後實際的數據傳遞,採用對稱加密方式來完成,因爲此時已經是安全的了。

證書機制

與數字簽名相關的一個概念就是證書機制了,證書是用來做什麼呢?在上面的各種模式中,我們一直使用了這樣一個假設,就是接收方或者發送方所持有的、對方的公鑰總是正確的(確實是對方公佈的)。而實際上除非對方手把手將公鑰交給我們,否則如果不採取措施,雙方在網絡中傳遞公鑰時,一樣有可能被篡改。那麼怎樣解決這個問題呢?這時就需要證書機制了:可以引入一個公正的第三方,當某一方想要發佈公鑰時,它將自身的身份信息及公鑰提交給這個第三方,第三方對其身份進行證實,如果沒有問題,則將其信息和公鑰打包成爲證書(Certificate)。而這個公正的第三方,就是常說的證書頒發機構(Certificate Authority)。當我們需要獲取公鑰時,只需要獲得其證書,然後從中提取出公鑰就可以了。

.NET中加密解密的支持

對稱加密和解密

相信通過前面幾頁的敘述,大家已經明白了加密解密、數字簽名的基本原理,下面我們看一下在.NET中是如何來支持加密解密的。正如上面我們所進行的分類,.NET中也提供了兩組類用於加密解密,一組爲對稱加密,一組爲非對稱加密,如下圖所示:

上面的類按照名稱還可以分爲兩組,一組後綴爲“CryptoServiceProvider”的,是對於底層Windows API的包裝類,一組後綴爲“Managed”,是在.NET中全新編寫的類。現在假設我們以TripleDES作爲算法,那麼加密的流程如下:

  1. 先創建一個TripleDESCryptoServiceProvider的實例,實例名比如叫provider。
  2. 在provider上指定密鑰和IV,也就是它的Key屬性和IV屬性。這裏簡單解釋一下IV(initialization vector),如果一個字符串(或者數據)加密之前很多部分是重複的比如ABCABCABC,那麼加密之後儘管字符串是亂碼,但相關部分也是重複的。爲了解決這個問題,就引入了IV,當使用它以後,加密之後即使是重複的也被打亂了。對於特定算法,密鑰和IV的值可以隨意指定,但長度是固定,通常密鑰爲128位或196位,IV爲64位。密鑰和IV都是byte[]類型,因此,如果使用Encoding類來將字符串轉換爲byte[],那麼編碼方式就很重要,因爲UTF8是變長編碼,所以對於中文和英文,需要特別注意byte[]的長度問題。
  3. 如果是加密,在provider上調用CreateEncryptor()方法,創建一個ICryptoTransform類型的加密器對象;如果是解密,在provider上調用CreateDecryptor()方法,同樣是創建一個ICryptoTransform類型的解密器對象。ICryptoTransform定義了加密轉換的運算,.NET將在底層調用這個接口。
  4. 因爲流和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);
}

應該可以看到下面的輸出結果:

總結

首先向大家表示歉意,我並沒有寫.NET中非對稱加密的部分,因爲我很少用到,所以這部分我並不是很熟悉,但是原理現在應該已經很清楚了,我想等到需要的時候再去學習如何來使用它們。到那個時候,我也會對這篇文章再次進行更新。通過這篇文章,相信大家對於加密、解密、數字簽名等這些安全方面的概念已經有了一個初步的認識,同時也學習到了如何在.NET下進行對稱加密。

感謝閱讀,希望這篇文章能給你帶來幫助!


發佈了13 篇原創文章 · 獲贊 3 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章