介紹如何在 Python 中使用 PyCryptodome 模組以 AES 對稱式加密方法對資料進行加密與解密。
AES 進階加密標準由美國國家標準與技術研究院(NIST)於 2001 年 11 月 26 日發布於 FIPS PUB 197,並在 2002 年 5 月 26 日成為有效的標準,是目前主流的對稱金鑰加密演算法之一。
以下將介紹如何在 Python 中安裝並使用 PyCryptodome 模組,以 AES 加密方法對資料進行加密與解密。
安裝 PyCryptodome 模組
使用 pip 安裝 Python 的 PyCryptodome 模組:
# 安裝 PyCryptodome 模組
sudo pip3 install pycryptodome
這種安裝方式會讓 PyCryptodome 安裝在 Crypto 套件路徑之下,取代舊的 PyCrypto 模組,這兩個模組會互相干擾,所以不可以同時安裝。
測試 PyCryptodome 模組是否可以正常運作:
# 測試 PyCryptodome 模組
python3 -m Crypto.SelfTest
如果不希望影響到舊的 PyCrypto 模組,也可以選擇以獨立模組的方式安裝,將 PyCryptodome 模組安裝至 Cryptodome 套件路徑之下:
# 安裝 PyCryptodome 模組
sudo pip3 install pycryptodomex
# 測試 PyCryptodome 模組
python -m Cryptodome.SelfTest
產生隨機金鑰
AES 加密方式的區塊長度固定為 128 位元,而金鑰長度則可以是 128、192 或 256 位元,在用 AES 進行資料加密之前,亦須先建立一組金鑰,最簡單的方式就是以隨機的方式產生金鑰。
from Crypto.Random import get_random_bytes
# 產生 256 位元隨機金鑰(32 位元組 = 256 位元)
key = get_random_bytes(32)
print(key)
b'l\n\xe8\x7f#\xec{\xf9\x8a4\xb8hye\xe9V\\\xfb\x01\x08\x854\x89\xc9\xfc\x80\xa2S\x920@}'以密碼產生金鑰
如果希望使用一般的密碼來對資料進行加密與解密,可以根據密碼與一串固定的 salt 字串,產生對應的金鑰。
首先以亂數方式產生一串隨機的資料作為固定的 salt:
# 產生 salt
print(get_random_bytes(32))
b'\xd0\x18\xa7QM\xd6\x9b\xebxu\xe4\xed\xa8\x83\xf6\xa3/\x01\x9c\x9e\x86n\xda;\x10EdD\xf7\x932\xcc'
為了方便起見,可以將這串 salt 直接寫在程式當中,搭配自己的密碼即可產生金鑰:
from Crypto.Protocol.KDF import PBKDF2
# 固定的 salt
salt = b'\xd0\x18\xa7QM\xd6\x9b\xebxu\xe4\xed\xa8\x83\xf6\xa3/\x01\x9c\x9e\x86n\xda;\x10EdD\xf7\x932\xcc'
# 密碼
password = 'my#password'
# 根據密碼與 salt 產生金鑰
key = PBKDF2(password, salt, dkLen=32)
儲存金鑰
實務上我們通常會將產生的金鑰儲存在檔案中,方便後續的加密與解密使用:
# 金鑰儲存位置
keyPath = "my_key.bin"
# 儲存金鑰
with open(keyPath, "wb") as f:
f.write(key)
# 讀取金鑰
with open(keyPath, "rb") as f:
keyFromFile = f.read()
# 檢查金鑰儲存
assert key == keyFromFile, '金鑰不符'
AES CBC 加密模式
以下是使用 AES 的 CBC 模式對資料進行加密的範例,以 CBC 模式加密時需要先對資料進行 padding 處理,再進行加密。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
# 輸出的加密檔案名稱
outputFile = 'encrypted.bin'
# 要加密的資料(必須為 bytes)
data = b'My secret data.'
# 以金鑰搭配 CBC 模式建立 cipher 物件
cipher = AES.new(key, AES.MODE_CBC)
# 將輸入資料加上 padding 後進行加密
cipheredData = cipher.encrypt(pad(data, AES.block_size))
# 將初始向量與密文寫入檔案
with open(outputFile, "wb") as f:
f.write(cipher.iv)
f.write(cipheredData)
以下則是 CBC 模式的解密方式:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# 輸入的加密檔案名稱
inputFile = 'encrypted.bin'
# 從檔案讀取初始向量與密文
with open(inputFile, "rb") as f:
iv = f.read(16) # 讀取 16 位元組的初始向量
cipheredData = f.read() # 讀取其餘的密文
# 以金鑰搭配 CBC 模式與初始向量建立 cipher 物件
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
# 解密後進行 unpadding
originalData = unpad(cipher.decrypt(cipheredData), AES.block_size)
# 輸出解密後的資料
print(originalData)
b'My secret data.'
AES CFB 加密模式
AES 的 CFB 模式跟 CBC 模式很類似,不過資料在加密之前不需要經過 padding 處理。
from Crypto.Cipher import AES
# 輸出的加密檔案名稱
outputFile = 'encrypted.bin'
# 要加密的資料(必須為 bytes)
data = b'My secret data.'
# 以金鑰搭配 CFB 模式建立 cipher 物件
cipher = AES.new(key, AES.MODE_CFB)
# 將輸入資料進行加密
cipheredData = cipher.encrypt(data)
# 將初始向量與密文寫入檔案
with open(outputFile, "wb") as f:
f.write(cipher.iv)
f.write(cipheredData)
以下則是 CFB 模式的解密方式:
from Crypto.Cipher import AES
# 輸入的加密檔案名稱
inputFile = 'encrypted.bin'
# 從檔案讀取初始向量與密文
with open(inputFile, "rb") as f:
iv = f.read(16) # 讀取 16 位元組的初始向量
cipheredData = f.read() # 讀取其餘的密文
# 以金鑰搭配 CFB 模式與初始向量建立 cipher 物件
cipher = AES.new(key, AES.MODE_CFB, iv=iv)
# 解密資料
originalData = cipher.decrypt(cipheredData)
# 輸出解密後的資料
print(originalData)
b'My secret data.'
AES EAX 加密模式
AES 的 EAX 加密模式會產生 nonce 與 tag,這兩項必須連同密文一起儲存起來。
from Crypto.Cipher import AES
# 輸出的加密檔案名稱
outputFile = 'encrypted.bin'
# 要加密的資料(必須為 bytes)
data = b'My secret data.'
# 以金鑰搭配 EAX 模式建立 cipher 物件
cipher = AES.new(key, AES.MODE_EAX)
# 將輸入資料進行加密
cipheredData, tag = cipher.encrypt_and_digest(data)
# 將 nonce、tag 與密文寫入檔案
with open(outputFile, "wb") as f:
f.write(cipher.nonce)
f.write(tag)
f.write(cipheredData)
以下則是 EAX 模式的解密方式:
from Crypto.Cipher import AES
# 輸入的加密檔案名稱
inputFile = 'encrypted.bin'
# 從檔案讀取初始向量與密文
with open(inputFile, "rb") as f:
nonce = f.read(16) # 讀取 16 位元組的 nonce
tag = f.read(16) # 讀取 16 位元組的 tag
cipheredData = f.read() # 讀取其餘的密文
# 以金鑰搭配 EAX 模式與 nonce 建立 cipher 物件
cipher = AES.new(key, AES.MODE_EAX, nonce)
# 解密並驗證資料
originalData = cipher.decrypt_and_verify(cipheredData, tag)
# 輸出解密後的資料
print(originalData)
b'My secret data.'
Base64 編碼與 JSON 儲存格式
如果要將密文等資料儲存至資料庫或是進行網路傳輸,可以考慮將資料經過 base64 編碼之後,放在一個 JSON 檔案中,以下是一個簡單的範例。
import json
from base64 import b64encode, b64decode
# 要儲存的原始資料
ciphertext = b'...'
iv = b'...'
# 建立字典結構
outputJSON = {
'ciphertext': b64encode(ciphertext).decode('utf-8'),
'iv': b64encode(iv).decode('utf-8')
}
# 儲存為 JSON 檔案
with open('encrypted.json', 'w') as f:
json.dump(outputJSON, f)
# 讀取 JSON 檔案
with open('encrypted.json') as f:
inputJSON = json.load(f)
# 取用資料
ciphertext = b64decode(inputJSON['ciphertext'].encode('utf-8'))
iv = b64decode(inputJSON['iv'].encode('utf-8'))
