菜鳥學堂:
密碼的故事
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 應用程序提供了更大方便。