密碼的故事
Billy Hollis
2002 年 3 月 14 日
本文是由一個問題引出的。我需要一種將密碼儲存在加密檔案中的方法,因為我需要記住許多密碼,但記憶力卻已大不如前。我知道有許多商用工具能夠做到這一點,但我感到學習 .NET 中的一項新技術真的很有好處。
我用 Visual Basic® .NET 完成了一個簡單而完整的程式,用於加密和解密檔案,從中學到了許多知識。既然加密對於多種開發都是一個重要問題,本文就介紹一下如何構造這樣的程式。
有各種低層級的技術可以用於加密,如 Microsoft Crypto API。而在 .NET 中,則是將這些複雜內容打包在各個 .NET 架構類中,並且由一個 System.Security 命名空間包含這些與加密相關的類。我們不可能查看該命名空間中的所有類,但通過分析一個最簡單的、使用資料加密標準 (DES) 演算法進行加密和解密的類,可以大概瞭解它們的工作原理。
正如前面提到的,我們要執行一個完整的加密和解密檔案的過程,但首先需要解釋一下該程式中涉及的許多基本概念。除有關密碼的原理外,還有必要簡單討論一下 .NET 中的流,因為加密類是以流的形式實現的。
理解流
流是 .NET 中處理位元組的基本概念。下面簡單介紹一下其工作原理。
假設要讀取一個檔案,將所有大寫字母更改為小寫字母,然後將結果寫入另一個檔案。圖 1 顯示了要完成的各個步驟的關係圖。
圖 1:讀取檔案、處理內容並寫回結果的過程
在 .NET 中,完成此過程的最好方法是使用流。“流”是一個對象,用於接收和/或發送資訊位元組。流有兩種 - 後端流和過程流。
後端流
後端流從某個可以儲存位元組的位置擷取位元組或將位元組儲存到該位置。檔案流就是一種後端流。檔案流使用檔案作為位元組的後端儲存,並讀取或寫入該檔案。
檔案流在 .NET 的
FileStream 類中實現,該類位於 System.IO 命名空間。
FileStream 對象使用
Read 和
Write 方法訪問檔案。將
FileStream 對象附加到現有檔案時,您可以使用
Read 方法,以一系列位元組的形式擷取檔案內容。而使用
Write 方法時,
FileStream 對象可以將一系列位元組寫入檔案(現有檔案或新檔案)。
FileStream 類還使用
Seek 方法來定位檔案中的特定位置。
後端流的其他樣本有網路流(將資料放到 TCP/IP 堆棧或從中擷取資料)和記憶體流(使用記憶體作為臨時後端)。它們的基本結構與
FileStream 對象相同,都使用
Read 和
Write 方法訪問後端儲存的位元組。有些後端流(如網路流)不支援
Seek 方法,因為沒有可供執行尋找操作的永久儲存內容。
過程流
過程流用於接收並處理位元組,然後將位元組寫入其他流(通常是後端流)。例如,我們可以從名為
Stream 的 .NET 基類中繼承,然後建立一個將大寫字母更改為小寫字母的過程流。再將這個流附加到任何後端流。現在,上圖的關係可以表示為圖 2。
圖 2:使用流表示的讀取檔案、處理內容並寫回結果的過程
我們的“變為小寫”流類只在經常需要執行該操作時才有用。但這種流類可以對通過它的位元組執行所需的各種操作。
.NET 中的加密
在 .NET 中,加密和解密是使用過程流來實現的。例如,加密的典型步驟為:
- 從某個輸入資料流(例如,磁碟中的未加密檔案)傳入位元組。
- 將位元組送到加密流,加密流本身串連到某個輸出資料流(例如,要儲存加密資料的檔案)。
- 加密流加密位元組並自動將位元組放到相關聯的輸出資料流中。
加密流被封裝到一個名為
CryptoStream 的類(本文後面將詳細介紹該類)。假設我們正在讀取和寫入磁碟檔案,那麼如果使用這一術語,則此過程的關係如圖 3 所示。
圖 3:加密檔案的過程
加密類型
加密資訊的方法已經有幾百年的曆史。小說家艾倫·坡就曾經涉足密碼學,而設計和破解密碼也曾經是第二次世界大戰中的一項重要活動。然而,電腦的出現使密碼學有了飛速發展。電腦強大的分析加密訊息的能力迫使人們不斷研究越來越難以破解的加密技術。
其結果是研究出了多種加密方法。.NET 中提供的常用方法包括:加密方法一般類型實現方法的 .NET 類資料加密標準 (DES)對稱
(私密金鑰)DESCryptoServiceProviderRC2 (RSA Data Security, Inc.)對稱
(私密金鑰)RC2CryptoServiceProviderRijndael對稱
(私密金鑰)RijndaelManagedTripleDES(在一行中使用三重 DES 加密)對稱
(私密金鑰)TripleDESCryptoServiceProvider數位簽章演算法非對稱
(公開金鑰)DSACryptoServiceProviderRSA(由 Rivest、Shamir 和 Adelman 發明,以他們名字的首字母命名)非對稱
(公開金鑰)RSACryptoServiceProvider
密碼編譯演算法的一般類型有對稱和非對稱兩種。對稱演算法使用相同的密鑰來加密和解密資料。非對稱演算法使用一個公開金鑰進行加密,而使用另一個密鑰來解密。在本文最後,我們將繼續介紹這一點。
如果只是使用加密方法,則不必詳細瞭解其工作原理(謝天謝地,某些內容是相當複雜的);但如果要選擇一種演算法,則必須考慮以下三個主要因素:
- 破解使用該演算法加密的訊息的難度
- 演算法的效能
- 密鑰的安全性
有許多 Web 網站討論了以上因素。對於初學者,以下兩個網站比較適合:http://www.microsoft.com/china/security/ 和 Snake Oil FAQ http://www.interhack.net/people/cmcurtin/snake-oil-faq.html(英文)。
使用 .NET 加密類
本表列出的 .NET 類都在 System.Security.Cryptography 命名空間中,因此使用它們時必須引用 System.Security.dll。此外,使用一對
IMPORTS 語句來引用命名空間會使代碼更加簡潔:
Imports System.SecurityImports System.Security.Cryptography
上表中的類都和一個名為
CryptoStream 的一般密碼流類一起工作。這樣便可以僅使用一個能實現多種加密的類來處理流操作。您甚至可以建立自己的加密類並將其插入
CryptoStream 中(儘管其安全性不太可能與上表列出的類相比)。
在我們的樣本中,我們使用的加密方法可能是 .NET 中最簡單的,即使用 DSE 演算法進行對稱金鑰密碼編譯。實現 DES 的過程流被稱為 DESCryptoServiceProvider。
大多數對稱演算法要求有兩個單獨的位元組數組,用於加密過程。第一個是密鑰。對於 DES,密鑰是 8 個位元組,而其他演算法使用的位元組數則不同。
必須將此私密金鑰以一種安全的方式傳遞給解密檔案的人,如果私密金鑰泄露,加密資訊也將泄露。但即使密鑰不泄露,DES 加密也正常工作,這種加密方法也遠遠稱不上是最安全的演算法。
要加密一個資訊塊(通常是 8 個位元組),需要同時使用密鑰和上一個塊的加密結果,也就是說,具有相同原始字元的塊在加密後不會得出相同的結果。這樣做的優點是,重複的塊不會提供線索而使加密的破解變得更容易。
不過,第一個塊沒有前置塊作為加密的輸入。如果第一個塊包含已知資訊(如網路標題),則對第一個塊實施反向工程,就會很容易地獲得密鑰。
為防止這種破解方法,DES 使用所謂的“初始化向量”。這是另一個位元組數組,長度與密鑰相同。將其與密鑰一起使用,進一步加密 8 個位元組的第一個塊。還有其他幾種對稱演算法也使用初始化向量。
建立密鑰
如果使用隨機密鑰,則對稱式加密演算法最安全。所以,產生密鑰的最好方法是使用隨機過程來獲得所需的 8 個位元組。但是,8 個隨機位元組並不容易記住。在下面的樣本中,我們使用“密碼”來產生密鑰。簡單地說,密碼是一個 8 字元的字串,使用字元的 ASCII 值來初始化構成密鑰的位元組數組。
我們需要兩個這樣的密碼:一個用於密鑰,另一個用於初始化向量。這還遠不是產生密鑰的最安全方法,但比較適合我們的樣本;而且對於常規的使用,它提供了適當的安全性層級。
加密/解密程式
我們已經介紹了相關概念,現在可以開始建立加密和解密檔案的程式了。我們將其設計為一個 Windows 表單應用程式。
在 Visual Studio 中建立一個新的 Windows 表單應用程式。要訪問密碼類,請轉至
Project | Add Reference(項目|添加引用),添加對 System.Security 的引用。
項目中的表單需要相互並排的四個標籤和四個文字框,靠近底部有兩個按鈕,底邊是一個狀態列。完成後,表單應如圖 4 所示。
圖 4:加密/解密程式的表單布局
使用以下名稱從上到下設定文字框:
- txtUnencryptedFile
- txtEncryptedFile
- txtKeyPassword
- txtIVPassword
將各個文字框的
Text 屬性設定為空白。將狀態列命名為
sbEncryptionStatus。
將按鈕命名為
btnEncrypt 和
btnDecrypt,並將它們的
Text 屬性分別更改為
Encrypt 和
Decrypt。為
btnEncrypt 按鈕添加以下代碼:
Dim byteKey() As BytebyteKey = GetKeyByteArray(txtKeyPassword.Text)Dim byteInitializationVector() As BytebyteInitializationVector = GetKeyByteArray(txtIVPassword.Text)EncryptOrDecryptFile(txtUnencryptedFile.Text, _ txtEncryptedFile.Text, _ byteKey, byteInitializationVector, _ CryptoAction.actionEncrypt)
為
btnDecrypt 按鈕添加以下代碼:
Dim byteKey() As BytebyteKey = GetKeyByteArray(txtKeyPassword.Text)Dim byteInitializationVector() As BytebyteInitializationVector = GetKeyByteArray(txtIVPassword.Text)EncryptOrDecryptFile(txtEncryptedFile.Text, _ txtUnencryptedFile.Text, _ byteKey, byteInitializationVector, _ CryptoAction.actionDecrypt)
您會注意到這兩個按鈕相關的代碼很類似。它們使用相同的函數擷取密鑰和初始化向量的數組,並使用相同的函數(名為
EncryptOrDecryptFile)加密或解密檔案。使用
EncryptOrDecryptFile 時的唯一區別是文字框中輸入和輸出檔案的檔案名稱正好相反,並且動作(加密或解密)不同。
該動作被指定為
CryptoAction 枚舉類型,所以需要定義枚舉。將此代碼添加到 Inherits System.Windows.Forms.Form 行的下面:
Private Enum CryptoAction actionEncrypt = 1 actionDecrypt = 2End Enum
還需要在模組頂部添加語句,以便輕鬆地引用密碼類和流類。以下是所需的程式碼:
Imports System.Security.CryptographyImports System.SecurityImports System.IO
到目前為止,代碼都比較簡單。現在我們看看怎樣產生密鑰。下面是一個將密碼變成位元組數組的函數,應當將它添加到表單代碼中:
Private Function GetKeyByteArray(ByVal sPassword As String) As Byte() Dim byteTemp(7) As Byte sPassword = sPassword.PadRight(8) ' 確保是 8 個字元 Dim iCharIndex As Integer For iCharIndex = 0 To 7 byteTemp(iCharIndex) = Asc(Mid$(sPassword, iCharIndex + 1, 1)) Next Return byteTempEnd Function
這也是一段很直觀的代碼。Visual Basic 6.0 開發人員應當注意對字串的
PadRight 方法(取代 Visual Basic 6.0 中的等效字串處理代碼)的使用,以確保長度正確。
下面是關鍵內容。插入下面的函數以處理加密和解密:
Private Sub EncryptOrDecryptFile(ByVal sInputFile As String, _ ByVal sOutputFile As String, _ ByVal byteDESKey() As Byte, _ ByVal byteDESIV() As Byte, _ ByVal Direction As CryptoAction) ' 建立處理輸入和輸出檔案的檔案流。 Dim fsInput As New FileStream(sInputFile, _ FileMode.Open, FileAccess.Read) Dim fsOutput As New FileStream(sOutputFile, _ FileMode.OpenOrCreate, FileAccess.Write) fsOutput.SetLength(0) ' 加密/解密過程中需要的變數 Dim byteBuffer(4096) As Byte ' 儲存位元組塊以進行處理 Dim nBytesProcessed As Long = 0 ' 運行對加密位元組的計數 Dim nFileLength As Long = fsInput.Length Dim iBytesInCurrentBlock As Integer Dim desProvider As New DESCryptoServiceProvider() Dim csMyCryptoStream As CryptoStream Dim sDirection As String ' 設定為加密或解密 Select Case Direction Case CryptoAction.actionEncrypt csMyCryptoStream = New CryptoStream(fsOutput, _ desProvider.CreateEncryptor(byteDESKey, byteDESIV), _ CryptoStreamMode.Write) sDirection = "加密" Case CryptoAction.actionDecrypt csMyCryptoStream = New CryptoStream(fsOutput, _ desProvider.CreateDecryptor(byteDESKey, byteDESIV), _ CryptoStreamMode.Write) sDirection = "解密" End Select sbEncryptionStatus.Text = sDirection + "正在啟動..." ' 從輸入檔案讀取,然後加密或解密 ' 並寫入輸出檔案。 While nBytesProcessed < nFileLength iBytesInCurrentBlock = fsInput.Read(byteBuffer, 0, 4096) csMyCryptoStream.Write(byteBuffer, 0, iBytesInCurrentBlock) nBytesProcessed = nBytesProcessed + CLng(iBytesInCurrentBlock) sbEncryptionStatus.Text = sDirection + _ "正在處理 - 已處理位元組數 - " + _ nBytesProcessed.ToString End While sbEncryptionStatus.Text = "完成" + sDirection + _ "。處理的位元組總數 - " + nBytesProcessed.ToString csMyCryptoStream.Close() fsInput.Close() fsOutput.Close()End Sub
現在我們具體說明以上代碼。
第一段建立檔案流對象(名為
fsInput 和
fsOutput),用於從正在讀取的檔案獲得輸入,再輸出到一個新檔案中。然後是函數的其餘部分中需要的幾個變數和對象的聲明。所聲明的元素如下:
- byteBuffer:位元組數組,用於處理當前資料區塊。通過讀取輸入檔案來填充該數組,然後將其傳遞到 CryptoStream 對象進行加密。後面的代碼有一個讀取輸入檔案的迴圈,以大小為 4096 位元組的塊提取檔案內容,並放到 byteBuffer 中。
- nBytesProcessed:到目前為止處理的輸入檔案的總位元組數。
- nFileLength:輸入檔案的長度。
- iBytesInCurrentBlock:在迴圈的特定迭代操作中處理的位元組數。除最後一次外,每次迭代都是 4096 位元組。在最後一次迭代中,其位元組數是檔案的最後一個塊中的剩餘位元組數(通常小於 4096)。
- desProvider:將其插入 CryptoStream 中以提供要使用的加密/解密功能。
- csMyCryptoStream:是用於加密或解密的 CryptoStream 對象。
- sDirection:是 CryptoAction 值(actionEncrypt 或 actionDecrypt),表明通過執行此函數所要完成的操作。
接下來的一段是
Select Case,根據我們執行該函數所要完成的操作來設定為加密或解密。
DESCryptoServiceProvider 可以分別使用
CreateEncryptor 或
CreateDecryptor 方法建立加密器或解密器。這用於執行個體化我們要使用的
CryptoStream 對象,並命名為
csMyCryptoStream。
csMyCryptoStream 對象還需要知道使用哪個流進行輸出。為此,在執行個體化
csMyCryptoStream 過程中,加密和解密都指定 fsOutput 流。
我們還設定了一個用於狀態列訊息的字串,其值為“加密”或“解密”。
最後一段真正執行加密和解密。
While 迴圈從輸入檔案讀取資料,一次讀取一個塊。然後使用其
Write 方法將塊寫入到
csMyCryptoStream 中。之後,
csMyCryptoStream 自動執行加密或解密,並將結果寫入到所附加的檔案流 fsOutput 中。然後,更新處理的位元組總數和狀態列訊息,並執行迴圈以處理另一個塊。
迴圈完成後,更新狀態列文本並關閉流對象,整個過程便告結束。
現在我們可以測試該項目。要進行正確的測試,加密的檔案大小要適當。我使用從 Project Gutenberg(http://promo.net/pg/ [英文])獲得的一個文字檔,該 Web 網站提供各種可以下載且不受著作權限制的文字檔。我選擇的檔案是柯南·道爾著的《福爾摩斯探案集》。文字檔長度為 573 KB。
在
Unencrypted File(未加密檔案)文字框中,輸入您選擇的需要加密的檔案的路徑名;並在
Encrypted File(已加密檔案)文字框中,輸入加密檔案的路徑名。還需要為密鑰和初始化向量構造密碼。
在測試時,我用的是 700-MHz 的 Pentium III 電腦,加密大小為 573 KB 的《福爾摩斯探案集》用了不到兩秒鐘時間。這展示了 DES 演算法的優勢之一,即其優異的效能。
如果在測試後開啟輸出檔案,會看到檔案已被完全加密。現在,使用相同的密碼將它解密到另一個檔案中。您可能希望確保新的解密檔案與原檔案完全相同。在 Microsoft Windows® 中有一個命令列公用程式,名為 FC(檔案比較),可以用它來進行檢驗。
很顯然,如果您在解密時更改了密碼,解密將不起作用。然而,如果在解密時更改初始化向量,則會發生有趣的事情。除了前 8 個字元外,檔案被正常解密。也就是說,初始化向量只能保護第一個塊。
總結
如上所述,DES 演算法只是一種選擇。而最靈活的選擇之一是使用公開金鑰加密系統,也稱為非對稱式加密。在這種技術中,加密和解密使用不同的密鑰。加密金鑰是公開的,而解密密鑰則是保密的,只有需要執行解密的人才知道。人人都可以使用加密金鑰進行加密,但只能使用解密密鑰進行解密。
公開金鑰加密的最大缺點是其效能較差。對稱演算法對處理能力的要求比公開金鑰演算法低得多。但是,公開金鑰加密令所有人(即使是您不認識的人)都能加密檔案並將它發送給您,這會給您提供更大的靈活性。
無論選擇哪種加密技術,使用 .NET 密碼類都能使操作更容易。正如本文樣本所示,您只需提供必要的密鑰和其他參數,然後將密碼類插入相應的流即可。這為建立使用加密技術的 Visual Basic .NET 應用程式提供了更大方便。