按:本文的圖片連結已失效,這裡有完整文章:http://huan-lin.blogspot.com/2008/04/dotnet-encryption.html

簡介

這裡不打算講太多加解密的原理,我想,對這篇文章有興趣的人,對於雜湊演算法(hashing algorithm,如:SHA-1)、對稱式加密(如:DESAES)、非對稱式加密(如:RSA 演算法)、公開金鑰(public key)、私密金鑰(private key)等應該多少都有一點概念吧。當然,在說明程式如何實作之前,還是會帶到一些關鍵的概念與原理,否則光看程式碼而不知其所以然,等到實際應用時恐怕還是不知從何著手。
基本上,數位簽章是基於非對稱式加密的技術,而目前較常用的非對稱式加密方法,則是 RSA 演算法。就實務面的應用而言,你可以不用了解非對稱式加密技術是怎麼來的,但至少要知道以下規則:
  1. 私密金鑰必須妥善保存,絕不可外洩;公開金鑰可對外公開,任何人都能取得。
  2. 使用公開金鑰加密的資料只有對應的私鑰可解,使用私密金鑰加密的資料只有對應的公開金鑰能解。

基 於上述特性,非對稱式加密方法非常適合用於許多需要加密和驗證資料完整性的場合。比如說,Alice 想要寫一封情書給隔壁班的 Bob,並拜託中間人 Eve 轉交給他,可是 Alice  擔心 Eve 會偷看情書的內容,此時她就可以將情書內容用 Bob 的公開金鑰(以下簡稱公鑰)加密;基於前述第 2 項特性,因此只有 Bob 能夠用他的私密金鑰(以下簡稱私鑰)解密。由於 Eve 沒有 Alice 的私密金鑰,因此她無法窺得原文,而且,就算 Eve 花再多時間,也很難在有生之年解開這段密文。
接著就來看看如何使用 .NET Framework 提供的類別實作非對稱式加密和解密。
註:在舉例說明加解密的應用情境時,經常會看到 Alice 和 Bob 這兩個人名,這已經成了慣例。

使用公鑰加密,私鑰解密

.NET Framework 提供的加解密類別全都放在 System.Security.Cryptography 命名空間裡,因此以下的範例程式都必須引用此命名空間。以下示範以 RSA 演算法來實作非對稱式加解密:

    1     // 建立 RSA 加解密物件。
    2     RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
    3
    4     // 欲加密的原文。
    5     string orgText = "Hello, 密碼學!";
    6
    7     // 加密。
    8     byte[] orgData = Encoding.Default.GetBytes(orgText);
    9     byte[] encryptedData = rsa.Encrypt(orgData, false);
   10
   11     // 解密。
   12     byte[] decryptedData = rsa.Decrypt(encryptedData, false);
   13     string decryptedText = Encoding.Default.GetString(decryptedData);
  
這段範例是把加密和解密的動作一次做完,當然實際上並不會這樣寫,通常是先將資料加密之後,傳送至目標,目標在收到密文之後才做解密的動作。這裡只是為了讓程式儘可能簡單,並且讓你一次看到全貌。
注意範例程式中的第 2 行,它會建立一個 RSACryptoServiceProvider 的物件,而且在建立物件時,就已經產生一對公鑰跟私鑰,在後續的加解密動作時,就是直接使用這對金鑰,因此你不會覺得需要對公鑰私鑰做什麼特別的處理。不 過,當你將加密和解密的動作拆開時,就會碰到傳遞金鑰的問題。你可以想像一下,Alice 若要將密文傳給 Bob,而且希望只有 Bob 能看解開,她就必須使用 Bob 的公開金鑰來加密。因此在實作時,程式必須先產生 Bob 的金鑰,並將公鑰儲存在可任意存取的地方,以便加密時使用。
接著就來修改前面的範例,把加密和解密動作拆開。首先,在 Visual Studio 2005 中建立一個 Windows Forms 專案,然後在 form 上面放兩個 TextBox 和兩個 Button,其個別命名與用途如下:
  • btnEncrypt:執行加密動作的按鈕。
  • btnDecrypt:執行解密動作的按鈕。
  • txtEncrypted:用來顯示加密的結果。
  • txtDecrypted:用來顯示解密的結果。
我們需要先建立接收者的金鑰,這個動作就放在 form1 的 Load 事件中完成,而 btnEncrypt 和 btnDecrypt 按鈕的事件處理程序則分別處理資料的加密和解密。以下是修改後的完整原始碼:
    1 using System;
    2 using System.Collections.Generic;
    3 using System.ComponentModel;
    4 using System.Data;
    5 using System.Drawing;
    6 using System.Text;
    7 using System.Windows.Forms;
    8 using System.Security.Cryptography;
    9
   10 namespace AsymmetricEncryptionDemo
   11 {
   12     public partial class Form1 : Form
   13     {
   14         public string bobPrivateKey;
   15         public string bobPublicKey;
   16
   17
   18         private void Form1_Load(object sender, EventArgs e)
   19         {
   20             // 產生 Bob 的金鑰對。
   21             RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
   22             bobPrivateKey = rsa.ToXmlString(true);
   23             bobPublicKey = rsa.ToXmlString(false);
   24         }
   25
   26         private void btnEncrypt_Click(object sender, EventArgs e)
   27         {
   28             // 建立 RSA 加解密物件。
   29             RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
   30
   31             // 匯入公開金鑰(因為要使用接收者的公鑰加密)。
   32             rsa.FromXmlString(bobPublicKey);
   33
   34             // 欲加密的原文。
   35             string orgText = "Hello, 密碼學!";
   36
   37             // 加密。
   38             byte[] orgData = Encoding.Default.GetBytes(orgText);
   39             byte[] encryptedData = rsa.Encrypt(orgData, false);
   40
   41             // 將加密過的文字顯示於 UI。
   42             txtEncrypted.Text = Convert.ToBase64String(encryptedData);
   43         }
   44
   45         private void btnDecrypt_Click(object sender, EventArgs e)
   46         {
   47             // 建立 RSA 加解密物件。
   48             RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
   49
   50             // 將私密金鑰匯入 RSA 物件。
   51             rsa.FromXmlString(bobPrivateKey);
   52
   53             byte[] encryptedData = Convert.FromBase64String(txtEncrypted.Text);
   54
   55             // 解密。
   56             byte[] decryptedData = rsa.Decrypt(encryptedData, false);
   57             txtDecrypted.Text = Encoding.Default.GetString(decryptedData);
   58         }
   59     }
   60 }
程 式的 Form1_Load 事件處理程序(第 18~24 行)會先產生接收者的金鑰對,並將私鑰和公鑰分別保存在變數 bobPrivateKey 和 bobPublicKey 中。這裡我用  RSACryptoServiceProvider 的 ToXmlString 方法將金鑰匯出成 XML 格式的字串,此方法需要一個布林參數,當此參數為 true 時,表示要包含公鑰及私鑰,若為 false 則只傳回公鑰。因此這裡的 bobPrivateKey 其實包含了公鑰與私鑰。
加密和解密的處理過程都加了詳細的註解,應該不用再額外說明了。比較需要特別注意的是 第 32 行和第 51 行,從這兩行可以看出加密跟解密時使用的是公鑰還是私鑰。此外,由於加解密的輸入與輸出資料都是 byte 陣列,因此在顯示加密的結果時,會先將資料轉成 Base64 編碼的字串(第 42 行),而在進行解密時,會先將 Base64 編碼的密文轉回 byte 陣列(第 53 行)。

使用私鑰加密,公鑰解密?

測試過公鑰加密、私鑰解密之後,也許你會想嘗試顛倒過來,用訊息發送者的私鑰加密,然後用公鑰解密。
不行,你不能這麼做。當你嘗試修改前面的範例程式碼,將加密和解密時使用的金鑰互相調換,像這樣(只列出有修改的部分):
   ....(略)
   32
             rsa.FromXmlString(bobPrivateKey);
   ....(略)
   51
             rsa.FromXmlString(bobPubliceKey);
執行時將會出現錯誤訊息:「機碼無效」(英文是 "Invalid Key")。
可是,前面不是才說過,非對稱式加密的基本規則是:「使用公鑰加密的資料只有對應的私鑰可解,而使用私鑰加密的資料只有對應的公鑰能解」?那又為什麼在撰寫程式時,使用私鑰加密會發生錯誤?
仔 細想一下,使用公開金鑰加密,並利用私密金鑰解密,這種方式有其實際的應用場合,就像我們前面舉的例子;這種方式主要是用來防止資料被他人窺視。 但如果反過來,假設 Alice 是用她自己的私鑰將訊息加密,那麼這段密文只有她的公鑰可解,而公鑰是可以任意散佈的,所以 Bob 也一定能用 Alice 的公鑰將密文解開。問題就在於,Alice 的公鑰任誰都可以取得,因此,不只 Bob,任何第三者只要能截取到該密文,也一定能夠將它解密。這麼一來,人人都可以輕易解密,那還要這種加密方式做什麼?
如果你花點時間思考一下上面的問題,或許會反駁:「不對!這種方式還是有用處。當我希望我發布的訊息不會遭到第三者篡改時,就可以先用我的私鑰將訊息加密,如此一來,別人如果要看訊息原文,就必須用我的公開金鑰解密,如果解密失敗,那就表示這段密文被篡改過了。」
是 的,這種方式的確可以防止資料被篡改。不過,RSA 加解密演算法需要複雜的運算,並不適合用來加密大量資料;況且,如果只是要防止資料被篡改,並不需要將整份明文加密,我們需要的是能夠檢查文件是否被篡改 的方法,而這個方法,就是接下來要談的數位簽章(digital signature)。

數位簽章

精確地說,數位簽章 並不能防止訊息被篡改,而是讓我們能夠判斷訊息是否遭到篡改。那麼,它是如何做到的?首先,訊息傳送者使用雜湊演算法 (hashing algorithm,例如:SHA-1)將明文加以運算,得到一組雜湊值,這組雜湊值有個正式的名稱,叫做「訊息摘要」(message digest)。顧名思義,它就是將原始資料運算之後得到的一小塊摘要資料,這塊摘要資料雖然小,但是卻足以用來驗證原始資料是否遭到篡改。你也可以把訊 息摘要想成是原始資料的縮影,但請注意它既非壓縮,也不是原始資料加密的結果;也就是說,你無法將訊息摘要還原成原始資料(因為雜湊函數是單向、不可逆 的)。你或許會質疑,兩份不同的文件是否可能產生相同的雜湊值?儘管機率很低,但還是有可能的(因此便可能偽造簽章),如果對此議題有興趣,可以上網找一 下關鍵字「加密 雜湊碰撞」,或者直接一點:「SHA-1 被破解」。
舉例來說,假設 Alice 要傳遞一段訊息給 Bob,而且她希望 Bob 收到時,能夠確認該訊息是否遭到篡改,整個處理過程描述如下:
  1. Alice 使用雜湊演算法針對該訊息加以運算並產生雜湊值,然後用私鑰將雜湊值加密,以產生簽章,再將這個加密過的簽章連同訊息內容一併傳遞給 Bob。
  2. 當 Bob 收到訊息和加密過的簽章,他可以將訊息透過相同的雜湊演算法重新計算雜湊值,然後用 Alice 的公鑰將簽章解密,以取得原有的雜湊值,最後再比對兩個雜湊值,若不相等, 即表示訊息被篡改了。
注 意 Alice 必須用私鑰將雜湊值加密,如此一來,就算有第三者從中攔截訊息,並篡改其內容,然後利用雜湊演算法產生一組新的雜湊值,並替換掉原本的雜湊值,也無法達成 其偽造訊息的目的。因為 Bob 會先用 Alice 的公鑰將簽章解密,如果簽章被替換過,就無法解密成功,當然 Bob 也就能發現訊息已經遭到篡改了。
我們可以根據前面描述的情境寫一個範例程式,來模擬及測試數位簽章。首先看一下範例程式的執行畫面:


畫面分成上下兩個區塊,上方區塊代表訊息傳送方,也就是 Alice,下方則是 Bob;紅字所標示的,是箭頭所指的控制項名稱。
當 Alice 要傳送訊息給 Bob 時,首先要產生數位簽章,而視窗中的「產生簽章」按鈕就是在做這件事情。簽章產生完畢之後,Alice 將明文、簽章、公開金鑰一併傳送給 Bob,此時再按「驗證簽章」按鈕,以執行驗證的動作。這裡我將 Alice 和 Bob 的行為分別用兩個類別封裝起來,而根據前面的描述可以知道,Alice 需要對文件簽章,並對外公布其公開金鑰,因此 Alice 類別會像這樣:
    1     public class Alice
    2     {
    3         RSACryptoServiceProvider m_Rsa;
    4
    5         public Alice()
    6         {
    7             m_Rsa = new RSACryptoServiceProvider(); // 建立 RSA 加密器,並自動建立一組金鑰對。
    8         }
    9
   10         public byte[] HashAnsSign(byte[] dataToSign)
   11         {
   12             // 使用 SHA1 演算法產生雜湊值,然後產生簽章。
   13             return m_Rsa.SignData(dataToSign, new SHA1CryptoServiceProvider());
   14         }
   15
   16         public string PublicKey
   17         {
   18             get
   19             {
   20                 return m_Rsa.ToXmlString(false); // 傳入 false 代表只傳回公鑰的 XML 字串。
   21             }
   22         }
   23     }
其中 HashAndSign 方法就是用來產生文件簽章,而 PublicKey 屬性則是 Alice 對外界公佈的公開金鑰,其內容格式為 XML 字串。計算雜湊值的演算法是 SHA-1,產生的簽章會是一塊大小為 128 bytes 的二進位資料區塊。
接著,Bob 只要作驗證簽章的動作,其類別定義如下:
    1     public class Bob
    2     {
    3         public bool VerifySignedData(byte[] data, byte[] signature, string publicKey)
    4         {
    5             RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
    6             rsa.FromXmlString(publicKey);  // 需要用到傳送者(Alice)的公鑰將簽章解密。
    7             return rsa.VerifyData(data, new SHA1CryptoServiceProvider(), signature);
    8         }
    9     }
VerifySignedData 方法接受三個參數,data 是訊息的原始資料,signature 是簽章,publicKey 則是訊息傳送者的公開金鑰。
Alice 和 Bob 物件是在範例程式的視窗載入時就已經建立好。當「產生簽章」按鈕按下時,要執行的處理如下:
    1         private void btnSign_Click(object sender, EventArgs e)
    2         {
    3             byte[] orgData = Encoding.Default.GetBytes(txtOrgData.Text); // 將訊息內容轉成 byte 陣列。
    4             byte[] signature = alice.HashAnsSign(orgData); // Alice 產生數位簽章。
    5
    6             txtSignature.Text = Convert.ToBase64String(signature);  // 將數位簽章的內容轉成 Base64 字串,以便顯示。
    7             lblSignedLength.Text = "簽章大小: " + signature.Length.ToString() + " bytes";
    8         }
最後是「驗證簽章」按鈕的事件處理程序:
    1         private void btnVerifySignedData_Click(object sender, EventArgs e)
    2         {
    3             byte[] orgData = Encoding.Default.GetBytes(txtOrgData.Text);     // 訊息原文
    4             byte[] signedData = Convert.FromBase64String(txtSignature.Text); // 簽章資料
    5
    6             bool ok = bob.VerifySignedData(orgData, signedData, alice.PublicKey);
    7             if (ok)
    8             {
    9                 MessageBox.Show("驗證成功!");
   10             }
   11             else
   12             {
   13                 MessageBox.Show("簽章無效!");
   14             }
   15         }
以下為執行結果:


如果您也有興趣練習看看,可以下載本文的附件,裡面包含數位簽章範例的完整原始碼。MSDN 網站上也有範例可以參考:http://msdn2.microsoft.com/zh-tw/library/9tsc5d0z(VS.80).aspx,而且你會發現 MSDN 上面的範例跟這裡的有些不同,特別是匯入跟匯出金鑰的部份,我用的是 RSACryptoServiceProvider 的 ToXmlString 和 FromXmlString 方法,它是用 ExportParameters 和 ImportParameters 方法。
另 外要說明的是,這裡的數位簽章範例程式只是簡單地示範數位簽章的實作,因此該範例並沒有將訊息加密。實際應用時,如果你希望傳遞的訊息不僅可以防 止篡改,而且要避免第三者窺視,也可以再加上前面介紹的公鑰加密、私鑰解密的方法,將訊息用接收者的公鑰加密。如此一來,傳送者和接收者的公鑰及私鑰便都 會派上用場,其運作流程如下(Alice 為訊息傳送者,Bob 為接收者):
  1. Alice 使用雜湊演算法產生訊息摘要。
  2. Alice 用私鑰將訊息摘要加密,以產生數位簽章。
  3. Alice 用 Bob 的公鑰將訊息加密。
  4. Alice 將加密過的訊息連同簽章一併傳送給 Bob。
  5. Bob 用 Alice 的公鑰將簽章解密還原成訊息摘要。
  6. Bob 用自己的私鑰將密文解密。
  7. Bob 將解密後的資料以雜湊演算法重新計算訊息摘要。
  8. Bob 比對步驟 5 跟步驟 7 產生的訊息摘要是否相同,以驗證資料是否遭到篡改。
配著圖看會比較清楚:

小結

這 篇文章只是個起點,當你開始動手練習,並參考網路上多種範例之後,就會發現密碼學這塊領域在實作上的細節還挺多的。比如說,你可能會發現這裡的範 例程式,每次執行時都會重新產生一對金鑰,可是如果你希望金鑰對只產生一次,存放在你的硬碟裡,以後每次執行時就直接取用先前建立的金鑰對,這個問題該如 何處理?(你得先了解什麼是「金鑰容器」)或者,你可能會想了解如何實作對稱式加解密,那麼你可以試試 AES(RijndaelManaged 類別)。總之,這塊領域還有很多東西等著我們探索與學習。