Python 3 的標準庫中沒多少用來解決加密的,不過卻有用於處理雜湊的庫。在這裡我們會對其進行一個簡單的介紹,但重點會放在兩個第三方的軟體包:PyCrypto 和 cryptography 上。我們將學習如何使用這兩個庫,來加密和解密字串。
雜湊
如果需要用到安全雜湊演算法或是訊息摘要演算法,那麼你可以使用標準庫中的 hashlib 模組。這個模組包含了符合 FIPS(美國聯邦資訊處理標準)的安全雜湊演算法,包括 SHA1,SHA224,SHA256,SHA384,SHA512 以及 RSA 的 MD5 演算法。Python 也支援 adler32 以及 crc32 雜湊函式,不過它們在 zlib 模組中。
雜湊的一個最常見的用法是,儲存密碼的雜湊值而非密碼本身。當然了,使用的雜湊函式需要穩健一點,否則容易被破解。另一個常見的用法是,計算一個檔案的雜湊值,然後將這個檔案和它的雜湊值分別傳送。接收到檔案的人可以計算檔案的雜湊值,檢驗是否與接受到的雜湊值相符。如果兩者相符,就說明檔案在傳送的過程中未經篡改。
讓我們試著建立一個 md5 雜湊:
1 2 3 4 5 6 7 8 9 10 |
>>> import hashlib >>> md5 = hashlib.md5() >>> md5.update('Python rocks!') Traceback (most recent call last): File "<pyshell#5>", line 1, in <module> md5.update('Python rocks!') TypeError: Unicode-objects must be encoded before hashing >>> md5.update(b'Python rocks!') >>> md5.digest() b'\x14\x82\xec\x1b#d\xf6N}\x16*+[\x16\xf4w' |
讓我們花點時間一行一行來講解。首先,我們匯入 hashlib ,然後建立一個 md5 雜湊物件的例項。接著,我們向這個例項中新增一個字串後,卻得到了報錯資訊。原來,計算 md5 雜湊時,需要使用位元組形式的字串而非普通字串。正確新增字串後,我們呼叫它的 digest 函式來得到雜湊值。如果你想要十六進位制的雜湊值,也可以用以下方法:
1 2 3 |
>>> md5.hexdigest() '1482ec1b2364f64e7d162a2b5b16f477' |
實際上,有一種精簡的方法來建立雜湊,下面我們看一下用這種方法建立一個 sha1 雜湊:
1 2 3 4 |
>>> sha = hashlib.sha1(b'Hello Python').hexdigest() >>> sha '422fbfbc67fe17c86642c5eaaa48f8b670cbed1b' |
可以看到,我們可以同時建立一個雜湊例項並且呼叫其 digest 函式。然後,我們列印出這個雜湊值看一下。這裡我使用 sha1 雜湊函式作為例子,但它不是特別安全,讀者可以隨意嘗試其他的雜湊函式。
金鑰匯出
Python 的標準庫對金鑰匯出支援較弱。實際上,hashlib 函式庫提供的唯一方法就是 pbkdf2_hmac 函式。它是 PKCS#5 的基於口令的第二個金鑰匯出函式,並使用 HMAC 作為偽隨機函式。因為它支援“加鹽(salt)”和迭代操作,你可以使用類似的方法來雜湊你的密碼。例如,如果你打算使用 SHA-256 加密方法,你將需要至少 16 個位元組的“鹽”,以及最少 100000 次的迭代操作。
簡單來說,“鹽”就是隨機的資料,被用來加入到雜湊的過程中,以加大破解的難度。這基本可以保護你的密碼免受字典和彩虹表(rainbow table)的攻擊。
讓我們看一個簡單的例子:
1 2 3 4 5 6 7 8 |
>>> import binascii >>> dk = hashlib.pbkdf2_hmac(hash_name='sha256', password=b'bad_password34', salt=b'bad_salt', iterations=100000) >>> binascii.hexlify(dk) b'6e97bad21f6200f9087036a71e7ca9fa01a59e1d697f7e0284cd7f9b897d7c02' |
這裡,我們用 SHA256 對一個密碼進行雜湊,使用了一個糟糕的鹽,但經過了 100000 次迭代操作。當然,SHA 實際上並不被推薦用來建立密碼的金鑰。你應該使用類似 scrypt 的演算法來替代。另一個不錯的選擇是使用一個叫 bcrypt 的第三方庫,它是被專門設計出來雜湊密碼的。
PyCryptodome
PyCrypto 可能是 Python 中密碼學方面最有名的第三方軟體包。可惜的是,它的開發工作於 2012 年就已停止。其他人還在繼續釋出最新版本的 PyCrypto,如果你不介意使用第三方的二進位制包,仍可以取得 Python 3.5 的相應版本。比如,我在 Github (https://github.com/sfbahr/PyCrypto-Wheels) 上找到了對應 Python 3.5 的 PyCrypto 二進位制包。
幸運的是,有一個該專案的分支 PyCrytodome 取代了 PyCrypto 。為了在 Linux 上安裝它,你可以使用以下 pip 命令:
1 2 |
pip install pycryptodome |
在 Windows 系統上安裝則稍有不同:
1 2 |
pip install pycryptodomex |
如果你遇到了問題,可能是因為你沒有安裝正確的依賴包(LCTT 譯註:如 python-devel),或者你的 Windows 系統需要一個編譯器。如果你需要安裝上的幫助或技術支援,可以訪問 PyCryptodome 的網站。
還值得注意的是,PyCryptodome 在 PyCrypto 最後版本的基礎上有很多改進。非常值得去訪問它們的主頁,看看有什麼新的特性。
加密字串
訪問了他們的主頁之後,我們可以看一些例子。在第一個例子中,我們將使用 DES 演算法來加密一個字串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
>>> from Crypto.Cipher import DES >>> key = 'abcdefgh' >>> def pad(text): while len(text) % 8 != 0: text += ' ' return text >>> des = DES.new(key, DES.MODE_ECB) >>> text = 'Python rocks!' >>> padded_text = pad(text) >>> encrypted_text = des.encrypt(text) Traceback (most recent call last): File "<pyshell#35>", line 1, in <module> encrypted_text = des.encrypt(text) File "C:\Programs\Python\Python35-32\lib\site-packages\Crypto\Cipher\blockalgo.py", line 244, in encrypt return self._cipher.encrypt(plaintext) ValueError: Input strings must be a multiple of 8 in length >>> encrypted_text = des.encrypt(padded_text) >>> encrypted_text b'>\xfc\x1f\x16x\x87\xb2\x93\x0e\xfcH\x02\xd59VQ' |
這段程式碼稍有些複雜,讓我們一點點來看。首先需要注意的是,DES 加密使用的金鑰長度為 8 個位元組,這也是我們將金鑰變數設定為 8 個字元的原因。而我們需要加密的字串的長度必須是 8 的倍數,所以我們建立了一個名為 pad 的函式,來給一個字串末尾填充空格,直到它的長度是 8 的倍數。然後,我們建立了一個 DES 的例項,以及我們需要加密的文字。我們還建立了一個經過填充處理的文字。我們嘗試著對未經填充處理的文字進行加密,啊歐,報了一個 ValueError 錯誤!我們需要對經過填充處理的文字進行加密,然後得到加密的字串。(LCTT 譯註:encrypt 函式的引數應為 byte 型別字串,程式碼為:encrypted_text = des.encrypt(padded_text.encode('utf-8'))
)
知道了如何加密,還要知道如何解密:
1 2 3 |
>>> des.decrypt(encrypted_text) b'Python rocks! ' |
幸運的是,解密非常容易,我們只需要呼叫 des 物件的 decrypt 方法就可以得到我們原來的 byte 型別字串了。下一個任務是學習如何用 RSA 演算法加密和解密一個檔案。首先,我們需要建立一些 RSA 金鑰。
建立 RSA 金鑰
如果你希望使用 RSA 演算法加密資料,那麼你需要擁有訪問 RAS 公鑰和私鑰的許可權,否則你需要生成一組自己的金鑰對。在這個例子中,我們將生成自己的金鑰對。建立 RSA 金鑰非常容易,所以我們將在 Python 直譯器中完成。
1 2 3 4 5 6 7 8 9 10 |
>>> from Crypto.PublicKey import RSA >>> code = 'nooneknows' >>> key = RSA.generate(2048) >>> encrypted_key = key.exportKey(passphrase=code, pkcs=8, protection="scryptAndAES128-CBC") >>> with open('/path_to_private_key/my_private_rsa_key.bin', 'wb') as f: f.write(encrypted_key) >>> with open('/path_to_public_key/my_rsa_public.pem', 'wb') as f: f.write(key.publickey().exportKey()) |
首先我們從 Crypto.PublicKey 包中匯入 RSA,然後建立一個傻傻的密碼。接著我們生成 2048 位的 RSA 金鑰。現在我們到了關鍵的部分。為了生成私鑰,我們需要呼叫 RSA 金鑰例項的 exportKey 方法,然後傳入密碼,使用的 PKCS 標準,以及加密方案這三個引數。之後,我們把私鑰寫入磁碟的檔案中。
接下來,我們通過 RSA 金鑰例項的 publickey 方法建立我們的公鑰。我們使用方法鏈呼叫 publickey 和 exportKey 方法生成公鑰,同樣將它寫入磁碟上的檔案。
加密檔案
有了私鑰和公鑰之後,我們就可以加密一些資料,並寫入檔案了。這裡有個比較標準的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from Crypto.PublicKey import RSA from Crypto.Random import get_random_bytes from Crypto.Cipher import AES, PKCS1_OAEP with open('/path/to/encrypted_data.bin', 'wb') as out_file: recipient_key = RSA.import_key( open('/path_to_public_key/my_rsa_public.pem').read()) session_key = get_random_bytes(16) cipher_rsa = PKCS1_OAEP.new(recipient_key) out_file.write(cipher_rsa.encrypt(session_key)) cipher_aes = AES.new(session_key, AES.MODE_EAX) data = b'blah blah blah Python blah blah' ciphertext, tag = cipher_aes.encrypt_and_digest(data) out_file.write(cipher_aes.nonce) out_file.write(tag) out_file.write(ciphertext) |
程式碼的前三行匯入 PyCryptodome 包。然後我們開啟一個檔案用於寫入資料。接著我們匯入公鑰賦給一個變數,建立一個 16 位元組的會話金鑰。在這個例子中,我們將使用混合加密方法,即 PKCS#1 OAEP ,也就是最優非對稱加密填充。這允許我們向檔案中寫入任意長度的資料。接著我們建立 AES 加密,要加密的資料,然後加密資料。我們將得到加密的文字和訊息認證碼。最後,我們將隨機數,訊息認證碼和加密的文字寫入檔案。
順便提一下,隨機數通常是真隨機或偽隨機數,只是用來進行密碼通訊的。對於 AES 加密,其金鑰長度最少是 16 個位元組。隨意用一個你喜歡的編輯器試著開啟這個被加密的檔案,你應該只能看到亂碼。
現在讓我們學習如何解密我們的資料。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from Crypto.PublicKey import RSA from Crypto.Cipher import AES, PKCS1_OAEP code = 'nooneknows' with open('/path/to/encrypted_data.bin', 'rb') as fobj: private_key = RSA.import_key( open('/path_to_private_key/my_rsa_key.pem').read(), passphrase=code) enc_session_key, nonce, tag, ciphertext = [ fobj.read(x) for x in (private_key.size_in_bytes(), 16, 16, -1) ] cipher_rsa = PKCS1_OAEP.new(private_key) session_key = cipher_rsa.decrypt(enc_session_key) cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce) data = cipher_aes.decrypt_and_verify(ciphertext, tag) print(data) |
如果你認真看了上一個例子,這段程式碼應該很容易解析。在這裡,我們先以二進位制模式讀取我們的加密檔案,然後匯入私鑰。注意,當你匯入私鑰時,需要提供一個密碼,否則會出現錯誤。然後,我們檔案中讀取資料,首先是加密的會話金鑰,然後是 16 位元組的隨機數和 16 位元組的訊息認證碼,最後是剩下的加密的資料。
接下來我們需要解密出會話金鑰,重新建立 AES 金鑰,然後解密出資料。
你還可以用 PyCryptodome 庫做更多的事。不過我們要接著討論在 Python 中還可以用什麼來滿足我們加密解密的需求。
cryptography 包
cryptography 的目標是成為“人類易於使用的密碼學包(cryptography for humans)”,就像 requests 是“人類易於使用的 HTTP 庫(HTTP for Humans)”一樣。這個想法使你能夠建立簡單安全、易於使用的加密方案。如果有需要的話,你也可以使用一些底層的密碼學基元,但這也需要你知道更多的細節,否則建立的東西將是不安全的。
如果你使用的 Python 版本是 3.5, 你可以使用 pip 安裝,如下:
1 2 |
pip install cryptography |
你會看到 cryptography 包還安裝了一些依賴包(LCTT 譯註:如 libopenssl-devel)。如果安裝都順利,我們就可以試著加密一些文字了。讓我們使用 Fernet 對稱加密演算法,它保證了你加密的任何資訊在不知道密碼的情況下不能被篡改或讀取。Fernet 還通過 MultiFernet 支援金鑰輪換。下面讓我們看一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> from cryptography.fernet import Fernet >>> cipher_key = Fernet.generate_key() >>> cipher_key b'APM1JDVgT8WDGOWBgQv6EIhvxl4vDYvUnVdg-Vjdt0o=' >>> cipher = Fernet(cipher_key) >>> text = b'My super secret message' >>> encrypted_text = cipher.encrypt(text) >>> encrypted_text (b'gAAAAABXOnV86aeUGADA6mTe9xEL92y_m0_TlC9vcqaF6NzHqRKkjEqh4d21PInEP3C9HuiUkS9f' b'6bdHsSlRiCNWbSkPuRd_62zfEv3eaZjJvLAm3omnya8=') >>> decrypted_text = cipher.decrypt(encrypted_text) >>> decrypted_text b'My super secret message' |
首先我們需要匯入 Fernet,然後生成一個金鑰。我們輸出金鑰看看它是什麼樣兒。如你所見,它是一個隨機的位元組串。如果你願意的話,可以試著多執行 generate_key 方法幾次,生成的金鑰會是不同的。然後我們使用這個金鑰生成 Fernet 密碼例項。
現在我們有了用來加密和解密訊息的密碼。下一步是建立一個需要加密的訊息,然後使用 encrypt 方法對它加密。我列印出加密的文字,然後你可以看到你再也讀不懂它了。為了解密出我們的祕密訊息,我們只需呼叫 decrypt 方法,並傳入加密的文字作為引數。結果就是我們得到了訊息位元組串形式的純文字。
小結
這一章僅僅淺顯地介紹了 PyCryptodome 和 cryptography 這兩個包的使用。不過這也確實給了你一個關於如何加密解密字串和檔案的簡述。請務必閱讀文件,做做實驗,看看還能做些什麼!