AES 加密/解密——示例

下面通过可运行的 Python 源代码演示 AES 加密AES 解密概念。

下面第一个示例将演示一种不含消息认证的简单基于密码的 AES 加密(PBKDF2 + AES-CTR,即非认证加密)。下一个示例会加入消息认证(使用 AES-GCM 模式),随后再加入基于密码的密钥派生(AES-256-GCM + Scrypt)。

简单的 AES-CTR 示例

先从简单的 AES-256-CTR 非认证加密开始。

安装 Python 库 pyaespbkdf2

首先,安装实现 AES 对称密钥加密算法的 Python 库 pyaes

pip install pyaes

接下来,安装实现 PBKDF2 基于密码的密钥派生算法的 Python 库 pbkdf2

pip install pbkdf2

现在来试用一个简单的 AES 加密/解密示例。

从密码派生密钥

首先进行密钥派生:从密码生成 256 位加密密钥。

import pyaes, pbkdf2, binascii, os, secrets

# Derive a 256-bit AES encryption key from the password
password = "s3cr3t*c0d3"
passwordSalt = os.urandom(16)
key = pbkdf2.PBKDF2(password, passwordSalt).read(32)
print('AES encryption key:', binascii.hexlify(key))

运行上面的代码示例:https://repl.it/@nakov/AES-CTR-in-Python

上述代码使用 PBKDF2 密钥派生算法,从密码 s3cr3t*c0d3 派生出一个 256 位密钥。它使用一个随机的密码派生(128 位)。该盐应与密文一同存入输出;没有它就无法再次派生解密密钥,也就无法完成解密。

上述代码的输出可能如下:

AES encryption key: b'7625e224dc0f0ec91ad28c1ee67b1eb96d1a5459533c5c950f44aae1e32f2da3'

派生出的密钥64 个十六进制数字(32 字节)组成,表示一个 256 位整数。多次运行上述代码会得到不同的密钥,因为每次都会使用随机盐。如果使用相同的盐,则会派生出相同的密钥。

AES 加密(CTR 分组模式)

接下来,为 AES CTR 分组模式生成一个随机 128 位初始化向量(IV),并执行 AES-256-CTR 加密

# Encrypt the plaintext with the given key:
#   ciphertext = AES-256-CTR-Encrypt(plaintext, key, iv)
iv = secrets.randbits(128)
plaintext = "Text for encryption"
aes = pyaes.AESModeOfOperationCTR(key, pyaes.Counter(iv))
ciphertext = aes.encrypt(plaintext)
print('Encrypted:', binascii.hexlify(ciphertext))

运行上面的代码示例:https://repl.it/@nakov/AES-encryption-in-Python

上述代码的输出可能如下:

Encrypted: b'53022cf12c5959ddf3e733128930dd3d52e3ea'

密文38 个十六进制数字(19 字节,152 位)组成,与输入数据(消息 Text for encryption)大小相同。

请注意,AES-CTR 加密后应将初始化向量(IV)与密文一同保存,因为没有它就无法解密。为提高安全性,每次 AES 加密都应随机生成 IV,而不应硬编码。

还要注意,即使使用相同的加密密钥多次加密同一明文,由于 IV 具有随机性,每次输出也会不同。这是预期行为,可以增强安全性,例如提高对字典攻击的抵抗力。

AES 解密(CTR 分组模式)

现在看看如何使用 AES-256-CTR 算法解密密文。输入包括密文 + 加密密钥 + CTR 计数器的 IV,输出为原始明文。代码非常简单:

# Decrypt the ciphertext with the given key:
#   plaintext = AES-256-CTR-Decrypt(ciphertext, key, iv)
aes = pyaes.AESModeOfOperationCTR(key, pyaes.Counter(iv))
decrypted = aes.decrypt(ciphertext)
print('Decrypted:', decrypted)

运行上面的代码示例:https://repl.it/@nakov/AES-decryption-in-Python

上述代码的输出应如下:

Decrypted: b'Text for encryption'

请注意,应该重新初始化 aes 对象,因为 CTR 分组密码模式算法会维护一个随时间变化的内部状态

还要注意,上述代码无法检测错误密钥、错误密文或错误 IV。如果使用不正确的密钥解密密文,会得到错误且不可读的文本。下面的代码清楚地展示了这一点:

key = os.urandom(32)   # random decryption key
aes = pyaes.AESModeOfOperationCTR(key, pyaes.Counter(iv))
print('Wrongly decrypted:', aes.decrypt(ciphertext))

运行上面的代码示例:https://repl.it/@nakov/AES-decryption-wrong-key-in-Python

上述错误解密尝试的输出可能如下:

Wrongly decrypted: b'\xe6!\n\x9a\xa9\x15\x12\xd9\xcb\x9cS\x86\xcc\xe1\x1d\x1a\x8blw'

现在轮到你试用上面的代码示例了。尝试加密和解密不同消息,修改输入消息和密钥大小,硬编码 IV、密钥及其他参数,切换到 CBC 模式,并观察结果如何变化。通过动手实践享受学习过程。

AES-256-GCM 示例

现在给出一个完整示例,说明如何使用 AES-256-GCM 对称加密构造。我们将使用另一个名为 pycryptodome 的 AES Python 库,它支持 AES-256-GCM 构造:

pip install pycryptodome

接下来试用下面的 Python AES-GCM 示例。它会生成一个随机加密密钥(秘密密钥),用该密钥加密文本消息,再将其解密回原始明文消息:

from Crypto.Cipher import AES
import binascii, os

def encrypt_AES_GCM(msg, secretKey):
    aesCipher = AES.new(secretKey, AES.MODE_GCM)
    ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
    return (ciphertext, aesCipher.nonce, authTag)

def decrypt_AES_GCM(encryptedMsg, secretKey):
    (ciphertext, nonce, authTag) = encryptedMsg
    aesCipher = AES.new(secretKey, AES.MODE_GCM, nonce)
    plaintext = aesCipher.decrypt_and_verify(ciphertext, authTag)
    return plaintext

secretKey = os.urandom(32)  # 256-bit random encryption key
print("Encryption key:", binascii.hexlify(secretKey))

msg = b'Message for AES-256-GCM + Scrypt encryption'
encryptedMsg = encrypt_AES_GCM(msg, secretKey)
print("encryptedMsg", {
    'ciphertext': binascii.hexlify(encryptedMsg[0]),
    'aesIV': binascii.hexlify(encryptedMsg[1]),
    'authTag': binascii.hexlify(encryptedMsg[2])
})

decryptedMsg = decrypt_AES_GCM(encryptedMsg, secretKey)
print("decryptedMsg", decryptedMsg)

运行上面的代码示例:https://repl.it/@nakov/AES-256-GCM-in-Python

AES-GCM 加密以消息 + 加密密钥作为输入,并输出一组值:{ 密文 + nonce + authTag }。

  • ciphertext 是加密后的消息。
  • nonce 是为 GCM 构造生成的随机初始化向量(IV)。
  • authTag 是加密过程中计算的消息认证码(MAC)。

上述代码生成的加密密钥大小为 256 位(32 字节),因此将 AES-GCM 密码配置为 AES-256-GCM。如果把密钥大小改为 128 位或 192 位,则会分别使用 AES-128-GCM 或 AES-192-GCM。

上述代码的输出如下:

Encryption key: b'233f8ce4ac6aa125927ccd98af5750d08c9c61d98a3f5d43cbf096b4caaebe80'
encryptedMsg {'ciphertext': b'1334cd5d487f7f47924187c94424a2079656838e063e5521e7779e441aa513de268550a89917fbfb0492fc', 'aesIV': b'2f3849399c60cb04b923bd33265b81c7', 'authTag': b'af453a410d142bc6f926c0f3bc776390'}
decryptedMsg b'Message for AES-256-GCM + Scrypt encryption'

可以看到,上面的加密密钥为 256 位(64 个十六进制数字),密文与输入消息长度相同(43 字节),IV 为 128 位(32 个十六进制数字),认证标签也是 128 位(32 个十六进制数字)。如果在解密前更改某些内容(例如密文IV),由于消息完整性遭到破坏,将会抛出异常

encryptedMsg = (b'wrong chiphertext', encryptedMsg[1], encryptedMsg[2])
decryptedMsg = decrypt_AES_GCM(encryptedMsg, secretKey)  # ValueError: MAC check failed

运行上面的代码示例:https://repl.it/@nakov/AES-256-GCM-wrong-chiphertext-in-Python

AES-256-GCM + Scrypt 示例

现在来看一个更复杂的示例:使用文本密码对文本进行 AES 加密。我们将采用认证加密构造 AES-256-GCM,并结合 Scrypt 密钥派生:

from Crypto.Cipher import AES
import scrypt, os, binascii

def encrypt_AES_GCM(msg, password):
    kdfSalt = os.urandom(16)
    secretKey = scrypt.hash(password, kdfSalt, N=16384, r=8, p=1, buflen=32)
    aesCipher = AES.new(secretKey, AES.MODE_GCM)
    ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
    return (kdfSalt, ciphertext, aesCipher.nonce, authTag)

def decrypt_AES_GCM(encryptedMsg, password):
    (kdfSalt, ciphertext, nonce, authTag) = encryptedMsg
    secretKey = scrypt.hash(password, kdfSalt, N=16384, r=8, p=1, buflen=32)
    aesCipher = AES.new(secretKey, AES.MODE_GCM, nonce)
    plaintext = aesCipher.decrypt_and_verify(ciphertext, authTag)
    return plaintext

msg = b'Message for AES-256-GCM + Scrypt encryption'
password = b's3kr3tp4ssw0rd'
encryptedMsg = encrypt_AES_GCM(msg, password)
print("encryptedMsg", {
    'kdfSalt': binascii.hexlify(encryptedMsg[0]),
    'ciphertext': binascii.hexlify(encryptedMsg[1]),
    'aesIV': binascii.hexlify(encryptedMsg[2]),
    'authTag': binascii.hexlify(encryptedMsg[3])
})

decryptedMsg = decrypt_AES_GCM(encryptedMsg, password)
print("decryptedMsg", decryptedMsg)

运行上面的代码示例:https://repl.it/@nakov/AES-256-GCM-with-Scrypt-in-Python

上述代码使用 AES-256-GCM,以给定文本密码加密给定文本消息

  • 加密过程中,使用带固定参数的 Scrypt KDF 函数从密码中派生秘密密钥。为密钥派生随机生成的 KDF 盐会与加密消息一同保存,并在解密时使用。随后使用秘密密钥对输入消息进行 AES 加密,输出包括密文 + IV(随机 nonce)+ authTag。最终输出包含这 3 个值以及 KDF 盐
  • 解密过程中,使用参数相同的 Scrypt 密钥派生,根据加密密码KDF 盐(加密时随机生成)派生出同一个秘密密钥。随后使用秘密密钥、IV(nonce)和 authTag 对密文进行 AES 解密。成功时,结果为解密后的原始明文;出错时,认证标签验证失败,并会抛出异常

上述代码的输出如下:

encryptedMsg {'kdfSalt': b'2dd0b783290747ba62a63fc53591170d', 'ciphertext': b'223ed888dcd216dcd40c47ff7cdaa7fd7eab65f4f0405350a43c5cad5b6b47b527c709edec29d7d6967518', 'aesIV': b'7f114d946c77508ed2e6afe652c78f21', 'authTag': b'e84a14b9542320a0b1473141c989c48f'}
decryptedMsg b'Message for AES-256-GCM + Scrypt encryption'

如果运行相同代码,由于存在随机性(随机 KDF 盐 + 随机 AES nonce),输出会有所不同。

results matching ""

    No results matching ""