Python 3: 加密簡介
哈希
如果需要用到安全哈希演算法或是消息摘要演算法,那麼你可以使用標準庫中的 hashlib 模塊。這個模塊包含了符合 FIPS(美國聯邦信息處理標準)的安全哈希演算法,包括 SHA1,SHA224,SHA256,SHA384,SHA512 以及 RSA 的 MD5 演算法。Python 也支持 adler32 以及 crc32 哈希函數,不過它們在 zlib 模塊中。
哈希的一個最常見的用法是,存儲密碼的哈希值而非密碼本身。當然了,使用的哈希函數需要穩健一點,否則容易被破解。另一個常見的用法是,計算一個文件的哈希值,然後將這個文件和它的哈希值分別發送。接收到文件的人可以計算文件的哈希值,檢驗是否與接受到的哈希值相符。如果兩者相符,就說明文件在傳送的過程中未經篡改。
讓我們試著創建一個 md5 哈希:
>>> 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'x14x82xecx1b#dxf6N}x16*+[x16xf4w'
讓我們花點時間一行一行來講解。首先,我們導入 hashlib ,然後創建一個 md5 哈希對象的實例。接著,我們向這個實例中添加一個字元串後,卻得到了報錯信息。原來,計算 md5 哈希時,需要使用位元組形式的字元串而非普通字元串。正確添加字元串後,我們調用它的 digest 函數來得到哈希值。如果你想要十六進位的哈希值,也可以用以下方法:
>>> md5.hexdigest()
'1482ec1b2364f64e7d162a2b5b16f477'
實際上,有一種精簡的方法來創建哈希,下面我們看一下用這種方法創建一個 sha1 哈希:
>>> sha = hashlib.sha1(b'Hello Python').hexdigest()
>>> sha
'422fbfbc67fe17c86642c5eaaa48f8b670cbed1b'
可以看到,我們可以同時創建一個哈希實例並且調用其 digest 函數。然後,我們列印出這個哈希值看一下。這裡我使用 sha1 哈希函數作為例子,但它不是特別安全,讀者可以隨意嘗試其他的哈希函數。
密鑰導出
Python 的標準庫對密鑰導出支持較弱。實際上,hashlib 函數庫提供的唯一方法就是 pbkdf2_hmac 函數。它是 PKCS#5 的基於口令的第二個密鑰導出函數,並使用 HMAC 作為偽隨機函數。因為它支持「 加鹽 」和迭代操作,你可以使用類似的方法來哈希你的密碼。例如,如果你打算使用 SHA-256 加密方法,你將需要至少 16 個位元組的「鹽」,以及最少 100000 次的迭代操作。
簡單來說,「鹽」就是隨機的數據,被用來加入到哈希的過程中,以加大破解的難度。這基本可以保護你的密碼免受字典和 彩虹表 的攻擊。
讓我們看一個簡單的例子:
>>> 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 命令:
pip install pycryptodome
在 Windows 系統上安裝則稍有不同:
pip install pycryptodomex
如果你遇到了問題,可能是因為你沒有安裝正確的依賴包(LCTT 譯註:如 python-devel),或者你的 Windows 系統需要一個編譯器。如果你需要安裝上的幫助或技術支持,可以訪問 PyCryptodome 的網站。
還值得注意的是,PyCryptodome 在 PyCrypto 最後版本的基礎上有很多改進。非常值得去訪問它們的主頁,看看有什麼新的特性。
加密字元串
訪問了他們的主頁之後,我們可以看一些例子。在第一個例子中,我們將使用 DES 演算法來加密一個字元串:
>>> 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:ProgramsPythonPython35-32libsite-packagesCryptoCipherblockalgo.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'>xfcx1fx16xx87xb2x93x0exfcHx02xd59VQ'
這段代碼稍有些複雜,讓我們一點點來看。首先需要注意的是,DES 加密使用的密鑰長度為 8 個位元組,這也是我們將密鑰變數設置為 8 個字元的原因。而我們需要加密的字元串的長度必須是 8 的倍數,所以我們創建了一個名為 pad 的函數,來給一個字元串末尾填充空格,直到它的長度是 8 的倍數。然後,我們創建了一個 DES 的實例,以及我們需要加密的文本。我們還創建了一個經過填充處理的文本。我們嘗試著對未經填充處理的文本進行加密,啊歐,報了一個 ValueError 錯誤!我們需要對經過填充處理的文本進行加密,然後得到加密的字元串。(LCTT 譯註:encrypt 函數的參數應為 byte 類型字元串,代碼為:encrypted_text = des.encrypt(padded_text.encode('utf-8'))
)
知道了如何加密,還要知道如何解密:
>>> des.decrypt(encrypted_text)
b'Python rocks! '
幸運的是,解密非常容易,我們只需要調用 des 對象的 decrypt 方法就可以得到我們原來的 byte 類型字元串了。下一個任務是學習如何用 RSA 演算法加密和解密一個文件。首先,我們需要創建一些 RSA 密鑰。
創建 RSA 密鑰
如果你希望使用 RSA 演算法加密數據,那麼你需要擁有訪問 RAS 公鑰和私鑰的許可權,否則你需要生成一組自己的密鑰對。在這個例子中,我們將生成自己的密鑰對。創建 RSA 密鑰非常容易,所以我們將在 Python 解釋器中完成。
>>> 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 方法生成公鑰,同樣將它寫入磁碟上的文件。
加密文件
有了私鑰和公鑰之後,我們就可以加密一些數據,並寫入文件了。這裡有個比較標準的例子:
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 個位元組。隨意用一個你喜歡的編輯器試著打開這個被加密的文件,你應該只能看到亂碼。
現在讓我們學習如何解密我們的數據。
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 的目標是成為「 人類易於使用的密碼學包 」,就像 requests 是「 人類易於使用的 HTTP 庫 」一樣。這個想法使你能夠創建簡單安全、易於使用的加密方案。如果有需要的話,你也可以使用一些底層的密碼學基元,但這也需要你知道更多的細節,否則創建的東西將是不安全的。
如果你使用的 Python 版本是 3.5, 你可以使用 pip 安裝,如下:
pip install cryptography
你會看到 cryptography 包還安裝了一些依賴包(LCTT 譯註:如 libopenssl-devel)。如果安裝都順利,我們就可以試著加密一些文本了。讓我們使用 Fernet 對稱加密演算法,它保證了你加密的任何信息在不知道密碼的情況下不能被篡改或讀取。Fernet 還通過 MultiFernet 支持密鑰輪換。下面讓我們看一個簡單的例子:
>>> 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 這兩個包的使用。不過這也確實給了你一個關於如何加密解密字元串和文件的簡述。請務必閱讀文檔,做做實驗,看看還能做些什麼!
相關閱讀
via: http://www.blog.pythonlibrary.org/2016/05/18/python-3-an-intro-to-encryption/
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive