Python密碼學編程:文件的加密與解密

在之前的章節中,編寫的程序只能操作較少的信息,這些信息往往是以字符串的形式直接寫在代碼中的。但本章中的程序可以對整個文件進行加密和解密,文件的大小可以包括成千上萬個字符。

本章要點

  • open()方法。
  • 讀、寫文件。
  • write()、close()及read()文件對象操作方法。
  • os.path.exists()方法。
  • upper()、lower()及title()字符串操作方法。
  • startswith()及endswith()字符串操作方法。
  • time模塊及time.time()方法。

10.1 純文本文件

對文件進行置換操作的程序只對純文本(無格式文本)文件進行加/解密,這類文件指的是那些後綴名為 .txt 且文件中不包含除文本數據以外的內容的文件。要編寫這類文件,可以選擇在Windows系統下使用Notepad、在macOS系統下使用TextEdit,或者在Linux系統下使用gedit。(Word這樣的文本處理程序同樣也可以生成純文本文件,但記住這些文件不能保存字體樣式、字體大小、顏色或其他任何格式。)除上述文本編輯軟件外,讀者甚至可以使用IDLE文本編輯器,只要將文件後綴保存為 .txt 而不是通常使用的 .py 即可。

如果需要純文本文件的樣例,則可以從網絡上下載一些txt小說,要將純文本手動輸入程序中,可能要花費很多時間,但如果使用現成的txt文件,則程序在數秒內就可以完成加密操作。

10.2 使用置換密碼加密文件的源代碼

在前兩章置換密碼測試程序的基礎上,針對文件的置換密碼程序引入了transposition Encrypt.py和transpositionDecrypt.py這兩個文件,這樣就可以調用encryptMessage()和decryptMessage() 這兩個函數。因此,編寫這個新程序將不用重新輸入兩個函數的代碼。

選中 File▶New File,打開一個新的編輯窗口,將下列代碼輸入編輯窗口並將其存儲為transpositionFileCipher.py。接下來,訪問本書配套資源下載一個名為frankenstein.txt的文件,並將其放置在與py文件相同的路徑之下,按下F5鍵運行這個程序。

transpositionFileCipher.py

<code> 1. # 置換密碼加/解密文件
2. # https://www.nostarch.com/crackingcodes/ (BSD Licensed)
3.
4. import time, os, sys, transpositionEncrypt, transpositionDecrypt
5.
6. def main():
7.   inputFilename = 'frankenstein.txt'
8.   # 注意,如果具有outputFilename 名稱的文件已存在,則此程序
9.   # 覆蓋該文件
10.   outputFilename = 'frankenstein.encrypted.txt'
11.   myKey = 10
12.   myMode = 'encrypt' # 設置為'encrypt'或'decrypt'
13.
14.   # 如果輸入文件不存在,則程序提前終止
15.   if not os.path.exists(inputFilename):
16.     print('The file %s does not exist. Quitting...' % (inputFilename))
17.     sys.exit()
18.
19.   # 如果輸出文件已存在,則給用戶退出的機會

20.   if os.path.exists(outputFilename):
21.     print('This will overwrite the file %s. (C)ontinue or (Q)uit?' %
       (outputFilename))
22.     response = input('> ')
23.     if not response.lower().startswith('c'):
24.       sys.exit()
25.
26.   # 從輸入文件中讀取消息
27.   fileObj = open(inputFilename)
28.   content = fileObj.read()
29.   fileObj.close()
30.
31.   print('%sing...' % (myMode.title()))
32.
33.   # 測量加/解密所需時間
34.   startTime = time.time()
35.   if myMode == 'encrypt':
36.     translated = transpositionEncrypt.encryptMessage(myKey, content)
37.   elif myMode == 'decrypt':
38.     translated = transpositionDecrypt.decryptMessage(myKey, content)
39.   totalTime = round(time.time() - startTime, 2)
40.   print('%sion time: %s seconds' % (myMode.title(), totalTime))
41.
42.   # 將置換後的消息寫入輸出文件
43.   outputFileObj = open(outputFilename, 'w')
44.   outputFileObj.write(translated)
45.   outputFileObj.close()
46.
47.   print('Done %sing %s (%s characters).' % (myMode, inputFilename,
     len(content)))
48.   print('%sed file is %s.' % (myMode.title(), outputFilename))
49.
50.
51. # 如果運行 transpositionCipherFile.py (而不是作為模塊引入),則
52. # 調用main() 函數
53. if __name__ == '__main__':
54.   main()/<code>

10.3 運行置換密碼加密文件程序的樣例

運行transpositionFileCipher.py得到的輸出如下。

<code>Encrypting... 

Encryption time: 1.21 seconds
Done encrypting frankenstein.txt (441034 characters).
Encrypted file is frankenstein.encrypted.txt./<code>

這樣就創建出了一個名為frankenstein.encrypted.txt 的新文件,該文件與 transposition FileCipher.py 在同一個路徑下。使用IDLE文件編輯器打開這個新文件,就可以看到frankenstein.txt 中的文本內容被加密後的結果了。它應有的格式如下所示。

<code>PtFiyedleo a arnvmt eneeGLchongnes Mmuyedlsu0#uiSHTGA r sy,n t ys
s nuaoGeL
sc7s,
--snip--/<code>

每次加密一個文件,都可以將加密的結果發送給另一個人去解密它,對方同樣需要文件置換操作程序的源代碼。

要解密密文,可以對源代碼進行下述改變(粗體部分),隨後再次運行這個程序。

<code> 7.   inputFilename = 'frankenstein.encrypted.txt'
8.   # 如果具有outputFilename 名稱的文件已存在,則此程序
9.   # 覆蓋該文件
10.   outputFilename = 'frankenstein.decrypted.txt'
11.   myKey = 10
12.   myMode = 'decrypt' # 設置為 'encrypt'或'decrypt'/<code>

這時候運行該程序,就會在當前文件夾下創建出一個名為 frankenstein.decrypted.txt 的新文件,此時這個新文件的內容和原始明文是一致的。

10.4 文件操作

在深入研究 transpositionFileCipher.py 文件的源代碼之前,首先要明白Python是如何對文件進行操作的。讀取文件內容的3個步驟分別是打開文件、讀取文件內容並將其存儲到一個變量中、關閉文件。類似地,要將新內容寫入文件中時,首先必須打開(或創建)一個文件,接著將新的內容寫入其中,最後關閉這個文件。

10.4.1 打開文件

Python可以通過open()方法打開一個文件以供讀取、寫入內容時使用,其第一個參數為文件名。當要打開的文件和Python程序處於同一個文件夾下時,可以直接使用文件名,例如“thetimemachine.txt”,如果當前文件夾存在這麼一個文件,則打開它的Python指令如下所示。

<code>fileObj = open('thetimemachine.txt')/<code>

這樣,一個文件對象就被存儲在變量 fileObj 中了,之後進行讀寫操作時使用這個變量即可。

還可以用文件的絕對路徑(absolute path)作為第一個參數,這樣引號內就需要包括文件所在的文件夾及其所有父文件夾的名稱,舉個例子,類似“C:\\\\Users\\\\Al\\\\frankenstein.txt”(Windows系統下),或“/Users/Al/frankenstein.txt”(macOS及Linux系統下)格式的都是絕對路徑。記住,Windows系統下,反斜線(/)前一定要多加一個反斜線用於轉義。

舉個例子,若想打開“frankenstein.txt”文件,則需要將其路徑以字符串的形式作為open()方法的第一個參數(絕對路徑的格式由使用的操作系統決定)。

<code>fileObj = open('C:\\\\Users\\\\Al\\\\frankenstein.txt')/<code>

文件對象有多種用於讀取、寫入和關閉文件的方法,下面將對這些方法進行詳細介紹,為方便說明這裡調換一下順序。

10.4.2 數據寫入及文件關閉

對於文件的加密程序而言,在讀取文本內容之後就需要將加密的數據寫入一個新的文件中,這時用到的方法就是write()。

要想使用一個文件對象的write()方法,首先需要將文件以寫模式打開,即將字符串 'w' 傳入open()方法作為其二個參數。open()方法的第二個參數是一個可選參數(optional parameter),這意味著open()方法在沒有第二個參數的情況下仍然能夠被調用。例如,將下列代碼輸入交互式運行環境中。

<code>>>> fileObj = open('spam.txt', 'w')/<code>

這一行以寫模式創建了一個名為“spam.txt”的文件,則可以對其進行編輯。如果在open()方法創建新文件的路徑下存在一個同名文件,則該同名文件將被重寫,因此,以寫模式使用opne()方法時需要萬分小心。

spam.txt 以寫模式打開後,就可以調用write()方法往其中寫入內容了。write()方法有一個參數:存儲在一個字符串中的、將要被寫入文件的內容。將下列代碼輸入交互式運行環境,把字符串Hello, world!寫入 spam.txt 中。

<code>>>> fileObj.write('Hello, world!')
13/<code>

上述代碼將字符串Hello, world!作為參數傳入write()方法,把該字符串寫入文件 spam.txt 中並打印出數字13,這個數字代表了寫入文件中的字符數。

對文件的操作執行完成之後,需要通過調用文件對象的close()方法告知Python此事。

<code>>>> fileObj.close()/<code>

除上述必定會覆蓋原文件內容的寫模式之外,還存在一個附加模式,在該模式下字符串會被添加到文件已有內容的末尾。儘管本章程序中沒有用到這個模式,讀者也可以自己嘗試以附加模式打開文件,只需要將字符串 'a' 作為 open() 方法的第二個參數即可。

如果在調用文件對象的write()方法時,遇到了“io.UnsupportedOperation: not readable”的報錯信息,則可能是因為沒有以寫模式打開文件。調用open()方法的過程中若沒有包括可選參數,則其默認值將被自動設置為寫模式('r'),該模式下只允許使用者調用文件對象的read()方法。

10.4.3 讀取文件

read()方法能夠以字符串的形式返回文件中包含的所有內容,為驗證其功能,本節將讀取之前用wirte()方法創建的 spam.txt 文件。在交互式運行環境中運行如下代碼。

<code>>>> fileObj = open('spam.txt', 'r')
>>> content = fileObj.read()
>>> print(content)
Hello world!
>>> fileObj.close()/<code>

打開文件之後創建的文件對象存儲在變量 fileObj 中,如果該對象存在,則可以使用read()方法讀取文件的內容並將其存儲在變量 content 中,隨後打印該變量的值。執行完上述對文件對象的操作後,使用close()方法關閉該文件。

如果遇到“IOError: [Errno 2] No such file or directory”的報錯信息,請確保想要打開的文件就在讀者認為的路徑下,並再次檢查文件名和文件夾的名稱是否正確輸入。(文件夾即路徑。)

在transpositionFileCipher.py程序中,對文件進行的加密和解密需要用到上文提到的所有open()、write() 及 close()方法。

10.5 創建main()函數

transpositionFileCipher.py 程序的第一部分應該看起來十分眼熟,第4行是一個import 語句,引入了transpositionEncypt.py和transpositionDecrypt.py兩個程序和Python庫中的time、os及sys模塊,接下來的部分即main()函數,其中創建了程序需要用到的變量。

<code>1. # 置換密碼加/解密文件
2. # https://www.nostarch.com/crackingcodes/ (BSD Licensed)
3.
4. import time, os, sys, transpositionEncrypt, transpositionDecrypt
5.
6. def main():
7.   inputFilename = 'frankenstein.txt'
8.   # 注意,如果具有outputFilename 名稱的文件已存在,則此程序
9.   # 覆蓋該文件
10.   outputFilename = 'frankenstein.encrypted.txt'
11.   myKey = 10
12.   myMode = 'encrypt' # 設置為 'encrypt'或'decrypt'/<code>

變量 inputFilename 存儲了待讀取文件名的字符串,而加密後(或解密後)的內容寫入以變量 outputFilename 的值命名的文件內。程序涉及的置換密碼使用一個整數作為密鑰,並存儲在myKey中,同時,程序需要一個變量 myMode 存儲字符串encrypt或decrypt以決定對 inputFilename 存儲的文件進行何種操作。在讀取 inputFilename 文件之前,首先要使用 os.path.exists() 檢查該文件是否存在。

10.6 檢查文件是否存在

讀取文件往往不會存在什麼危害,但往文件中寫入內容時就需要多加小心了,這是因為以寫模式調用open()方法時,若原文件已存在,會覆蓋掉原文件中的內容。針對這個潛在問題,程序可以使用os.path.exists() 方法,檢查要打開的文件是否已經存在。

10.6.1 os.path.exists() 方法

os.path.exists()方法只有一個參數,即文件名或指向文件的文件路徑,如果文件存在,則返回True;否則返回False。該方法包含在path模塊內,而path模塊包含在 os 模塊中,因此引入 os 模塊時,path模塊一併被引入了。

將下列代碼輸入交互式運行環境。

<code>  >>> import os
❶ >>> os.path.exists('spam.txt')
  False
  >>> os.path.exists('C:\\\\Windows\\\\System32\\\\calc.exe') # Windows
  True
  >>> os.path.exists('/usr/local/bin/idle3') # macOS
  False
  >>> os.path.exists('/usr/bin/idle3') # Linux
  False/<code>

在本例中,os.path.exists()方法證實了Windows系統中存在calc.exe文件。當然,只有在Windows系統下運行Python的時候,才能得到上面的結果。記住,在Windows下輸入文件路徑時,要在反斜槓前再添加一個反斜槓進行轉義。如果使用的是macOS,則上述代碼中只有macOS的樣例會返回True,同理在Linux系統下只有最後一個例子會返回True。如果沒有給出完整的路徑❶,則Python會檢查當前的工作路徑;對IDLE交互式運行環境而言,當前工作路徑即安裝了Python的文件夾。

10.6.2 使用os.path.exists()方法檢查輸入的文件是否存在

本章程序的第14~17行使用了os.path.exists()檢查 inputFilename 中的文件是否存在,如果沒有這一步,就無法獲得用於加解密的文件。

<code>14.   # 如果輸入文件不存在,則程序提前終止
15.   if not os.path.exists(inputFilename):
16.     print('The file %s does not exist. Quitting...' % (inputFilename))
17.     sys.exit()/<code>

若文件不存在,程序將為用戶彈出提示並退出。

10.7 使用字符串方法令用戶的輸入更靈活

接下來,程序需要檢查是否存在與 outputFilename 同名的文件,如果存在,則詢問用戶是輸入c繼續運行程序還是輸入q退出程序。由於用戶可能會輸入多種回覆,例如c、C,甚至是單詞Continue,因此程序需要確保可以接收所有這些輸入,要實現這一功能,必須使用更多字符串方法。

10.7.1 upper()、lower()和title()字符串方法

upper()和lower()方法能夠分別以全大寫和全小寫返回它們所接收的字符串。將下列代碼輸入交互式運行環境中以分辨這兩個方法是如何對同一個字符串進行操作的。

<code>>>> 'Hello'.upper() 

'HELLO'
>>> 'Hello'.lower()
'hello'/<code>

lower()、upper()方法以小寫和大寫的形式返回字符串,title()方法也和它們類似,然而該方法返回的是各單詞首字母大寫的字符串,這意味著字符串中的每個單詞的首字母是大寫,而其餘所有字母都是小寫。將下列代碼輸入交互式運行環境中。

<code>>>> 'hello'.title()
'Hello'
>>> 'HELLO'.title()
'Hello'
>>> 'extra! extra! man bites shark!'.title()
'Extra! Extra! Man Bites Shark!'/<code>

本章程序會在稍後部分使用title()方法,來為輸出的信息格式化。

10.7.2 startswith()和endswith()方法

若字符串以參數指定的字符串開頭,則startwith()方法返回True。將下列代碼輸入交互式運行環境。

<code> >>> 'hello'.startswith('h')
 True
 >>> 'hello'.startswith('H')
 False
 >>> spam = 'Albert'
❶ >>> spam.startswith('Al')
 True/<code>

startswith()方法對大小寫敏感,同時也可以接收多字符的字符串❶。

endswith()方法用於檢查字符串是否以某一個特定字符串結尾。將下列代碼輸入交互式運行環境。

<code> >>> 'Hello world!'.endswith('world!')
 True
❷ >>> 'Hello world!'.endswith('world')
 False/<code>

字符串的匹配必須一字不差,注意,由於❷中缺少感嘆號,因此endswith()的返回結果為False。

10.7.3 在程序中使用上述字符串方法

之前提到過,程序需要能夠接收所有以字母C開頭的響應,無論大小寫,這意味著不管用戶輸入的是C、continue、c還是其他以C開頭的字符串,程序都需要對文件進行重寫。使用lower()和upper()方法可以使程序在處理用戶輸入的字符串時更加靈活。

<code>19.   # 如果輸出文件已存在,則給用戶退出的機會
20.   if os.path.exists(outputFilename):
21.     print('This will overwrite the file %s. (C)ontinue or (Q)uit?' %
       (outputFilename))
22.     response = input('> ')
23.     if not response.lower().startswith('c'):
24.       sys.exit()/<code>

第23行,取字符串的首字母並使用startswith()方法來檢查它是否為C。由於startswith()方法大小寫敏感且檢查的是小寫的 'c',因此在調用它之前使用lower()方法改變response字符串的首字母,使其保持為小寫的 'c'。如果用戶沒有輸入以C開頭的響應,則if的條件語句將得到True(因為其中包含一個not),於是sys.exit()語句被調用,程序終止。從技術上來說,用戶不需要輸入q來退出,任何不以C開頭的字符串都會導致 sys.exit() 方法的調用,從而使程序退出。

10.8 讀取作為輸入的文件

第27行,程序開始使用本章開頭討論過的文件對象方法。

<code>26.   # 從輸入文件中讀取消息
27.   fileObj = open(inputFilename)
28.   content = fileObj.read()
29.   fileObj.close()
30.
31.   print('%sing...' % (myMode.title()))/<code>

第27~29行打開了與inputFilename同名的文件,讀取它的內容並存儲到變量 content 中,隨後關閉了文件。讀取完文件之後,第31行為用戶輸出了一行提示信息,告知他們加密或解密已經開始。由於變量 myMode 中存儲著字符串encrypt或decrypt,調用title()字符串方法將它的首字母轉換為大寫,又在它之後添加了ing字符串,因此最終它顯示的內容是 Encrypting...或者Decrypting...。

10.9 計算加/解密所需的時間

對一個文件進行全面的加/解密往往要比僅加/解密一個短短的字符串要耗時多,而用戶可能會想要了解加/解密文件的過程具體需要多長時間。程序可以使用 time 模塊計算加/解密過程所需的時間長度。

10.9.1 time模塊和time.time()方法

time.time()方法以浮點數的形式返回從1970年1月1日至當前時間的總秒數,這個數字被稱為UNIX時間戳。將下列代碼輸入交互式運行環境,觀察該方法的運行結果。

<code>>>> import time
>>> time.time()
1540944000.7197928
>>> time.time()
1540944003.4817972/<code>

由於time.time()返回的是一個浮點數,因此它可以精確到毫秒。當然,time.time()顯示的時間由程序員調用它的時間決定,並且要將它轉化為正常的時間也有一定難度,比如很難看出 1540944000.7197928 就是2018年的10月30日(星期二)的下午5點左右。然而time.time()非常適合於比較兩次調用time.time()之間相差的秒數,因此程序可以使用它計算運行時間。

舉個例子,如果按照下述代碼,把前一段代碼中兩次調用time.time()的時間相減,就可以得到兩次調用中間經過的時間了。

<code>>>> 1540944003.4817972 - 1540944000.7197928
2.7620043754577637/<code>

如果想要編寫對日期和時間進行操作的代碼,可以查閱 datetime 模塊的相關資料。

10.9.2 在程序中使用time.time()方法

第34行,time.time()方法返回了當前時間並將其存儲到名為 startTime 的變量中;第35~38行根據變量 myMode 的值是encrypt還是decrpt來調用encryptMessage()或decryptMessage()。

<code>33.   # 測量加/解密所需時間
34.   startTime = time.time()
35.   if myMode == 'encrypt':
36.     translated = transpositionEncrypt.encryptMessage(myKey, content)

37.   elif myMode == 'decrypt':
38.     translated = transpositionDecrypt.decryptMessage(myKey, content)
39.   totalTime = round(time.time() - startTime, 2)
40.   print('%sion time: %s seconds' % (myMode.title(), totalTime))/<code>

加/解密完成後,第39行再次調用了time.time()方法,並用這次調用的時間減去startTime,得到的結果是兩次調用time.time()方法的間隔時間。time.time() - startTime表達式將所得結果傳給round()方法,也就是將其取整,因為程序並不需要精確到毫秒。這個整數值賦值給了變量 totalTime。第40行使用了字符串連接,併為用戶打印了程序所處的模式及用於加密或解密的時長。

10.10 將輸出寫入文件

加密後(或解密後)的文件內容現在存儲在變量translated中,但這個變量在程序終止時就會被釋放,因此需要一個文件來存儲這個字符串,這樣哪怕程序停止執行,結果仍能保存。第43~45行的代碼進行了這部分操作,打開了一個新文件[將w傳給open()方法]並調用文件對象的write()方法。

<code>42.   # 將置換後的消息寫入輸出文件
43.   outputFileObj = open(outputFilename, 'w')
44.   outputFileObj.write(translated)
45.   outputFileObj.close()/<code>

接下來在第47行和第48行打印了更多信息,告知用戶輸出文件的名稱及加/解密過程已經結束。

<code>47.   print('Done %sing %s (%s characters).' % (myMode, inputFilename,
     len(content)))

48.   print('%sed file is %s.' % (myMode.title(), outputFilename))/<code>

第48行是main()函數的最後一行。

10.11 調用main()函數

第53行和第54行(在第6行def語句執行之後被執行的兩行)調用了main()函數,前提是當前程序處於運行狀態而非被引用的狀態下。

<code>51. # 如果運行 transpositionCipherFile.py (而不是作為模塊引入),則
52. # 調用main() 函數
53. if __name__ == '__main__':
54.   main()/<code>

7.12節對這部分進行了詳細解釋。

10.12 小結

除了open()、read()、write()和close()這些幫助我們在硬盤上加密大文本文件的函數,transpositionFileCipher.py 程序中沒有包含太多的新內容。讀者學到了如何使用 os.path.exists()函數檢查文件是否已經存在。同時如讀者所見,編程時可以通過在新程序中引入之前所寫程序的函數來拓展程序的能力,這大大增長了計算機加密信息的能力。

除此之外,讀者也學習到了一些有用的字符串方法,它們使得程序在接收yoghurt輸入時更加靈活;time模塊也可以幫助計算程序運行的時間。

和實現凱撒密碼的程序不同的是,如果想要使用暴力算法破解通過置換密碼加密的文件,會出現太多可能的密鑰。但如果編寫一個能夠識別英語(和其他無意義的亂碼)的程序,計算機就可以檢查成千上萬種解密結果,並確定一把可以成功將密文解密為英語的密鑰。這在第11章中會詳細介紹。

本文摘自《Python密碼學編程 第2版》,[美] 阿爾·斯維加特(Al Sweigart) 著,鄭新芳,趙怡翾譯。

Python密碼學編程:文件的加密與解密

  • 易懂的密碼學,暢銷書全面升級
  • Python編程實現加密算法的初學者指南

通過閱讀本書,您將學習如何通過 Python 編程製作和破解密碼,以此創建和發送密文!學習 Python 編程的基礎 知識之後,您將從使用反向和凱撒密碼的簡單程序開始,瞭解為公開密鑰加密的方法,該加密方法用於保護在線事務的 正常運作,包括數字簽名和電子郵件。另外,您還將學習如何使用經典密碼(如置換密碼和維吉尼亞密碼)對文本進行加密。 每個程序都包含完整的代碼和逐行說明。本書結尾,您將學習如何使用 Python 進行編碼。

您還將學習以下內容:

  • 將循環、變量和流控制語句組合到實際的工作程序中;
  • 使用字典文件檢測解密的消息是有效的英文還是亂碼;
  • 創建並測試程序以確保代碼正確加密和解密;
  • 對仿射密碼進行編碼(破解)的示例(使用模塊化算術對消息進行加密);
  • 使用暴力算法和頻率分析等技術破解密碼。


分享到:


相關文章: