Python

Python 以 PyCryptodome 實作 AES 對稱式加密方法教學與範例

介紹如何在 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'))

參考資料:PyCryptodomeNitratineNitratine

Share
Published by
Office Guide

Recent Posts

Python 使用 PyAutoGUI 自動操作滑鼠與鍵盤

本篇介紹如何在 Python ...

1 年 ago

Ubuntu Linux 以 WireGuard 架設 VPN 伺服器教學與範例

本篇介紹如何在 Ubuntu ...

1 年 ago

Linux 網路設定 ip 指令用法教學與範例

本篇介紹如何在 Linux 系...

1 年 ago

Linux 以 Cryptsetup、LUKS 加密 USB 隨身碟教學與範例

介紹如何在 Linux 系統中...

1 年 ago