天天看點

Python密碼學程式設計:檔案的加密與解密

在之前的章節中,編寫的程式隻能操作較少的資訊,這些資訊往往是以字元串的形式直接寫在代碼中的。但本章中的程式可以對整個檔案進行加密和解密,檔案的大小可以包括成千上萬個字元。

本章要點

  • open()方法。
  • 讀、寫檔案。
  • write()、close()及read()檔案對象操作方法。
  • os.path.exists()方法。
  • upper()、lower()及title()字元串操作方法。
  • startswith()及endswith()字元串操作方法。
  • time子產品及time.time()方法。

1 純文字檔案

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

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

2 使用置換密碼加密檔案的源代碼

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

選中 File▶New File,打開一個新的編輯視窗,将下列代碼輸入編輯視窗并将其存儲為transpositionFileCipher.py。接下來,通路本書配套資源下載下傳一個名為frankenstein.txt的檔案,并将其放置在與py檔案相同的路徑之下,按下F5鍵運作這個程式。

transpositionFileCipher.py

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()      

3 運作置換密碼加密檔案程式的樣例

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

Encrypting...
Encryption time: 1.21 seconds
Done encrypting frankenstein.txt (441034 characters).
Encrypted file is frankenstein.encrypted.txt.      

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

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

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

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

7.   inputFilename = 'frankenstein.encrypted.txt'
 8.   # 如果具有outputFilename 名稱的檔案已存在,則此程式
 9.   # 覆寫該檔案
10.   outputFilename = 'frankenstein.decrypted.txt'
11.   myKey = 10
12.   myMode = 'decrypt' # 設定為 'encrypt'或'decrypt'      

這時候運作該程式,就會在目前檔案夾下建立出一個名為 frankenstein.decrypted.txt 的新檔案,此時這個新檔案的内容和原始明文是一緻的。

4 檔案操作

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

4.1 打開檔案

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

fileObj = open('thetimemachine.txt')      

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

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

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

fileObj = open('C:\\Users\\Al\\frankenstein.txt')      

檔案對象有多種用于讀取、寫入和關閉檔案的方法,下面将對這些方法進行詳細介紹,為友善說明這裡調換一下順序。

4.2 資料寫入及檔案關閉

對于檔案的加密程式而言,在讀取文本内容之後就需要将加密的資料寫入一個新的檔案中,這時用到的方法就是write()。

要想使用一個檔案對象的write()方法,首先需要将檔案以寫模式打開,即将字元串 ‘w’ 傳入open()方法作為其二個參數。open()方法的第二個參數是一個<span style=“color:#20B2AA”;“font-family: Times New Roman,楷體_GB2312”>可選參數(optional parameter),這意味着open()方法在沒有第二個參數的情況下仍然能夠被調用。例如,将下列代碼輸入互動式運作環境中。

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

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

spam.txt 以寫模式打開後,就可以調用write()方法往其中寫入内容了。write()方法有一個參數:存儲在一個字元串中的、将要被寫入檔案的内容。将下列代碼輸入互動式運作環境,把字元串Hello, world!寫入 spam.txt 中。

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

上述代碼将字元串Hello, world!作為參數傳入write()方法,把該字元串寫入檔案 spam.txt 中并列印出數字13,這個數字代表了寫入檔案中的字元數。

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

>>> fileObj.close()      

除上述必定會覆寫原檔案内容的寫模式之外,還存在一個附加模式,在該模式下字元串會被添加到檔案已有内容的末尾。盡管本章程式中沒有用到這個模式,讀者也可以自己嘗試以附加模式打開檔案,隻需要将字元串 ‘a’ 作為 open() 方法的第二個參數即可。

如果在調用檔案對象的write()方法時,遇到了“io.UnsupportedOperation: not readable”的報錯資訊,則可能是因為沒有以寫模式打開檔案。調用open()方法的過程中若沒有包括可選參數,則其預設值将被自動設定為寫模式(‘r’),該模式下隻允許使用者調用檔案對象的read()方法。

4.3 讀取檔案

read()方法能夠以字元串的形式傳回檔案中包含的所有内容,為驗證其功能,本節将讀取之前用wirte()方法建立的 spam.txt 檔案。在互動式運作環境中運作如下代碼。

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

打開檔案之後建立的檔案對象存儲在變量 fileObj 中,如果該對象存在,則可以使用read()方法讀取檔案的内容并将其存儲在變量 content 中,随後列印該變量的值。執行完上述對檔案對象的操作後,使用close()方法關閉該檔案。

如果遇到“IOError: [Errno 2] No such file or directory”的報錯資訊,請確定想要打開的檔案就在讀者認為的路徑下,并再次檢查檔案名和檔案夾的名稱是否正确輸入。(<span style=“color:#20B2AA”;“font-family: Times New Roman,楷體_GB2312”>檔案夾即<span style=“color:#20B2AA”;“font-family: Times New Roman,楷體_GB2312”>路徑。)

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

5 建立main()函數

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

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'      

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

6 檢查檔案是否存在

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

6.1 os.path.exists() 方法

os.path.exists()方法隻有一個參數,即檔案名或指向檔案的檔案路徑,如果檔案存在,則傳回True;否則傳回False。該方法包含在path子產品内,而path子產品包含在 os 子產品中,是以引入 os 子產品時,path子產品一并被引入了。

将下列代碼輸入互動式運作環境。

>>> 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      

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

6.2 使用os.path.exists()方法檢查輸入的檔案是否存在

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

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

若檔案不存在,程式将為使用者彈出提示并退出。

7 使用字元串方法令使用者的輸入更靈活

接下來,程式需要檢查是否存在與 outputFilename 同名的檔案,如果存在,則詢問使用者是輸入c繼續運作程式還是輸入q退出程式。由于使用者可能會輸入多種回複,例如c、C,甚至是單詞Continue,是以程式需要確定可以接收所有這些輸入,要實作這一功能,必須使用更多字元串方法。

7.1 upper()、lower()和title()字元串方法

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

>>> 'Hello'.upper()
'HELLO'
>>> 'Hello'.lower()
'hello'      

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

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

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

7.2 startswith()和endswith()方法

若字元串以參數指定的字元串開頭,則startwith()方法傳回True。将下列代碼輸入互動式運作環境。

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

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

endswith()方法用于檢查字元串是否以某一個特定字元串結尾。将下列代碼輸入互動式運作環境。

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

字元串的比對必須一字不差,注意,由于❷中缺少感歎号,是以endswith()的傳回結果為False。

7.3 在程式中使用上述字元串方法

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

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()      

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

8 讀取作為輸入的檔案

第27行,程式開始使用本章開頭讨論過的檔案對象方法。

26.   # 從輸入檔案中讀取消息
27.   fileObj = open(inputFilename)
28.   content = fileObj.read()
29.   fileObj.close()
30.
31.   print('%sing...' % (myMode.title()))      

第27~29行打開了與inputFilename同名的檔案,讀取它的内容并存儲到變量 content 中,随後關閉了檔案。讀取完檔案之後,第31行為使用者輸出了一行提示資訊,告知他們加密或解密已經開始。由于變量 myMode 中存儲着字元串encrypt或decrypt,調用title()字元串方法将它的首字母轉換為大寫,又在它之後添加了ing字元串,是以最終它顯示的内容是 Encrypting…或者Decrypting…。

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

對一個檔案進行全面的加/解密往往要比僅加/解密一個短短的字元串要耗時多,而使用者可能會想要了解加/解密檔案的過程具體需要多長時間。程式可以使用 time 子產品計算加/解密過程所需的時間長度。

9.1 time子產品和time.time()方法

time.time()方法以浮點數的形式傳回從1970年1月1日至目前時間的總秒數,這個數字被稱為<span style=“color:#20B2AA”;“font-family: Times New Roman,楷體_GB2312”>UNIX時間戳。将下列代碼輸入互動式運作環境,觀察該方法的運作結果。

>>> import time
>>> time.time()
1540944000.7197928
>>> time.time()
1540944003.4817972      

由于time.time()傳回的是一個浮點數,是以它可以精确到<span style=“color:#20B2AA”;“font-family: Times New Roman,楷體_GB2312”>毫秒。當然,time.time()顯示的時間由程式員調用它的時間決定,并且要将它轉化為正常的時間也有一定難度,比如很難看出 1540944000.7197928 就是2018年的10月30日(星期二)的下午5點左右。然而time.time()非常适合于比較兩次調用time.time()之間相差的秒數,是以程式可以使用它計算運作時間。

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

>>> 1540944003.4817972 - 1540944000.7197928
2.7620043754577637      

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

9.2 在程式中使用time.time()方法

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

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))      

加/解密完成後,第39行再次調用了time.time()方法,并用這次調用的時間減去startTime,得到的結果是兩次調用time.time()方法的間隔時間。time.time() - startTime表達式将所得結果傳給round()方法,也就是将其取整,因為程式并不需要精确到毫秒。這個整數值指派給了變量 totalTime。第40行使用了字元串連接配接,并為使用者列印了程式所處的模式及用于加密或解密的時長。

10 将輸出寫入檔案

加密後(或解密後)的檔案内容現在存儲在變量translated中,但這個變量在程式終止時就會被釋放,是以需要一個檔案來存儲這個字元串,這樣哪怕程式停止執行,結果仍能儲存。第43~45行的代碼進行了這部分操作,打開了一個新檔案[将w傳給open()方法]并調用檔案對象的write()方法。

42.   # 将置換後的消息寫入輸出檔案
43.   outputFileObj = open(outputFilename, 'w')
44.   outputFileObj.write(translated)
45.   outputFileObj.close()      

接下來在第47行和第48行列印了更多資訊,告知使用者輸出檔案的名稱及加/解密過程已經結束。

47.   print('Done %sing %s (%s characters).' % (myMode, inputFilename,
     len(content)))
48.   print('%sed file is %s.' % (myMode.title(), outputFilename))      

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

11 調用main()函數

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

51. # 如果運作 transpositionCipherFile.py (而不是作為子產品引入),則
52. # 調用main() 函數
53. if __name__ == '__main__':
54.   main()      

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

12 小結

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