Python实现AES加密:从原理到实战,详解CBC/ECB模式与pycryptodome应用
1. 项目概述为什么用Python实现AES加密是必备技能如果你正在处理用户密码、配置文件、API密钥或者任何需要在存储或传输时保护的数据那么加密是你绕不开的话题。在众多加密算法中AES高级加密标准无疑是应用最广泛、最受信赖的对称加密算法。从HTTPS协议到Wi-Fi安全从文件加密到数据库字段保护AES的身影无处不在。而Python凭借其简洁的语法和强大的库生态成为了实现这些加密逻辑的理想工具。但很多开发者在初次接触时往往会被“模式”、“填充”、“编码”这些概念搞得晕头转向直接从网上复制一段代码结果发现密文对不上解密出乱码或者与其他系统如Java、PHP无法互通。这篇文章我就结合自己多年在数据安全开发中踩过的坑带你彻底搞懂如何在Python中正确、灵活地实现AES的各种加密模式。我们不止于“能用”更要追求“懂原理”、“会选型”、“能排错”。无论你是需要为本地脚本添加一个简单的加密功能还是要构建一个需要与多种后端服务进行加密交互的系统这篇文章都能给你一套清晰、可复现的解决方案。2. AES加密的核心概念与Python库选型在动手写代码之前我们必须先统一“语言”。AES加密不是一个单一的操作而是一套包含多个可配置组件的体系。理解这些组件是避免后续各种诡异问题的关键。2.1 理解AES的三大核心参数密钥、模式与填充密钥Key这是加密和解密的唯一凭证必须保密。AES标准支持三种密钥长度128位16字节、192位24字节和256位32字节。在Python中密钥必须是一个字节串bytes。一个常见的错误是直接使用字符串比如key mysecretpassword这会导致后续操作失败。正确的做法是key bmysecretpassword或者key mysecretpassword.encode(utf-8)。如果你的密码长度不够千万不要简单地用空格或0补足而应该使用密钥派生函数如PBKDF2这一点我们后面会详细讲。模式Mode这决定了AES算法如何对超过一个数据块16字节的明文进行加密。最常见的两种模式是ECB和CBC。ECB模式电子密码本最简单粗暴。它将明文分割成独立的16字节块然后每块用相同的密钥独立加密。这导致了致命弱点相同的明文块会产生相同的密文块。如果加密一张有大量纯色区域的图片ECB加密后的密文依然能看出轮廓。因此ECB模式在需要安全性的场合是绝对不推荐的。CBC模式密码块链接这是目前最常用的模式。它在加密每个明文块前会先与前一个密文块进行异或操作。对于第一个块则需要一个初始向量IV来参与异或。CBC模式有效地隐藏了明文的模式相同的明文块在不同位置或不同次加密中会产生不同的密文块安全性远高于ECB。填充PaddingAES是一种分组密码一次处理16字节的数据。但我们的明文长度几乎不可能总是16的整数倍。填充就是为了解决这个问题在明文的末尾添加一些额外的字节使其长度对齐。不同的填充规则就是不同的填充模式。PKCS#7在AES语境下常与PKCS#5混用是最通用、最推荐的标准填充方式。2.2 Python库的选择为什么是pycryptodomePython标准库没有提供现成的AES实现。社区中有几个流行的选择pycrypto已停止维护、cryptography和pycryptodome。pycryptodome这是pycrypto的一个积极维护的分支API与之兼容但修复了大量bug和安全问题。它功能全面文档清晰是大多数场景下的首选。安装时要注意如果系统里存在旧的pycrypto可能会冲突最好先卸载pip uninstall crypto pycryptodome再安装pip install pycryptodome。cryptography这是一个更现代、由专业安全团队维护的库提供了更高层次的“recipes”接口用起来更简单安全。但对于需要精细控制AES各种模式参数的学习或特定兼容性场景pycryptodome提供的底层控制更直接。本文将以pycryptodome为例进行讲解因为它能最清晰地暴露AES加密的各个细节有助于我们深入理解。在实际生产环境中根据团队习惯和项目需求两者皆可。注意导入模块时应使用from Crypto.Cipher import AES。这里的Crypto首字母是大写C这是pycryptodome的约定与旧版pycrypto一致但容易写错。3. 从零开始手把手实现AES ECB与CBC模式理论说再多不如一行代码。我们先从最基础的ECB和CBC模式开始实现一个完整的加密解密流程并解释每一个参数和步骤的用意。3.1 ECB模式基础实现与安全警示from Crypto.Cipher import AES import base64 # 1. 准备参数关键步骤 key bThisIsASecretKey # 16字节密钥 # 明文必须是16字节的倍数这里正好16字节 plaintext bHelloAESWorld123! # 16字节明文 # 2. 创建AES加密器对象 # AES.new() 接收密钥和模式常量 cipher AES.new(key, AES.MODE_ECB) # 3. 执行加密 # encrypt() 方法要求输入是字节串且长度是16的倍数 ciphertext cipher.encrypt(plaintext) print(ECB密文 (原始字节):, ciphertext) print(ECB密文 (Base64):, base64.b64encode(ciphertext).decode()) # 4. 解密重要ECB模式解密需要新建对象 # 很多新手会复用上面的cipher对象导致报错decrypt() cannot be called after encrypt() decryptor AES.new(key, AES.MODE_ECB) decrypted_text decryptor.decrypt(ciphertext) print(解密后明文:, decrypted_text)这段代码能跑通但它有两个严重问题明文长度固定我们的明文plaintext刚好16字节。如果换成b‘Hello World’11字节程序会直接抛出ValueError: Input strings must be a multiple of 16 in length。ECB模式不安全如前所述ECB会暴露明文的结构。为了直观展示ECB的不安全性我们可以做一个简单的实验加密一张纯色的PNG图片。你会发现加密后的图片虽然看起来像噪声但原始图片的大致轮廓依然可见。这证明了ECB不能用于需要保密性的场景。因此除非是与某些历史遗留系统进行兼容否则请永远避免使用ECB模式。3.2 CBC模式的正确实现与IV的奥秘CBC模式是更安全的选择它引入了一个新的关键参数初始化向量IV。from Crypto.Cipher import AES import base64 import os # 用于生成随机IV # 1. 准备参数 key bThisIsA16ByteKey!! # 16字节密钥 # 2. 生成随机IV至关重要 # IV不需要保密但必须是随机的、不可预测的且每次加密都应不同。 # 通常IV会放在密文前面一起传输。 iv os.urandom(16) # 生成16字节的强随机数作为IV plaintext b‘This is a secret message that can be any length.’ # 3. 创建CBC加密器对象 # AES.new() 在CBC模式下需要三个参数key, mode, iv cipher_encrypt AES.new(key, AES.MODE_CBC, iv) # 4. 加密 ciphertext cipher_encrypt.encrypt(plaintext) print(“IV (Hex):”, iv.hex()) print(“CBC密文 (Base64):”, base64.b64encode(ciphertext).decode()) # 5. 解密 # 解密时必须使用加密时相同的IV。 cipher_decrypt AES.new(key, AES.MODE_CBC, iv) decrypted_text cipher_decrypt.decrypt(ciphertext) print(“解密后明文:”, decrypted_text)运行这段代码你大概率会得到一个错误ValueError: Input strings must be a multiple of 16 in length。因为我们的明文长度50字节不是16的倍数。这就引出了下一个核心问题填充Padding。实操心得关于IV的“潜规则”IV的作用是确保即使相同的明文用相同的密钥加密多次也会产生完全不同的密文。它相当于加密的“盐”。有两个黄金准则绝对不要使用固定IV比如全零b‘\x00’*16。这会让CBC模式的安全性大打折扣。IV不需要加密但必须随密文一起存储或传输。常见的做法是将IV16字节直接拼接在密文前面解密时先取出前16字节作为IV剩下的部分作为密文进行解密。例如full_message iv ciphertext。4. 填充Padding策略详解与通用实现填充是连接“任意长度明文”和“固定16字节分组”的桥梁。pycryptodome库的AES.encrypt()方法本身不自动处理填充我们必须手动处理。4.1 PKCS#7填充原理与实现PKCS#7是业界最通用的填充标准。它的规则非常巧妙如果需要填充N个字节那么每个填充字节的值就是N。如果数据长度正好是16的倍数则额外填充一个完整的16字节块每个字节值为0x10十进制16。例如明文b‘Hello’5字节需要填充11字节则填充后的数据为b‘Hello’ b‘\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b’。明文b‘A’*1616字节需要填充16字节填充后为b‘A’*16 b‘\x10’*16。这样做的好处是解密后可以明确知道哪些字节是填充的并准确移除。下面我们实现PKCS#7的填充与去填充函数def pkcs7_pad(data: bytes, block_size: int 16) - bytes: “”“ 实现PKCS#7填充。 :param data: 原始字节数据 :param block_size: 分组大小AES为16 :return: 填充后的字节数据 “”“ # 计算需要填充的字节数 padding_len block_size - (len(data) % block_size) # 如果余数为0则填充一整个块 if padding_len 0: padding_len block_size # 生成填充字节值为padding_len的字节重复padding_len次 padding bytes([padding_len]) * padding_len return data padding def pkcs7_unpad(padded_data: bytes) - bytes: “”“ 移除PKCS#7填充。 :param padded_data: 已填充的字节数据 :return: 移除填充后的原始数据 “”“ # 获取最后一个字节的值即为填充长度 padding_len padded_data[-1] # 验证填充的合法性可选但推荐 # 检查填充长度是否在合理范围内1到block_size if padding_len 1 or padding_len len(padded_data): raise ValueError(“Invalid padding length.”) # 检查最后padding_len个字节的值是否都等于padding_len if padded_data[-padding_len:] ! bytes([padding_len]) * padding_len: raise ValueError(“Invalid padding bytes.”) # 移除填充 return padded_data[:-padding_len] # 测试填充函数 test_data b‘Hello AES World’ padded pkcs7_pad(test_data) print(f“原始数据: {test_data}, 长度: {len(test_data)}“) print(f“填充后: {padded}, 长度: {len(padded)}“) print(f“填充字节值: {list(padded[-10:])}“) # 查看最后几个字节 unpadded pkcs7_unpad(padded) print(f“去填充后: {unpadded}“)4.2 整合填充的完整CBC加密解密流程现在我们将填充、加密、解密整合到一个健壮的流程中from Crypto.Cipher import AES import os import base64 def aes_cbc_encrypt(key: bytes, plaintext: bytes) - (bytes, bytes): “”“ AES-CBC加密 :param key: 16/24/32字节的密钥 :param plaintext: 原始明文字节 :return: (iv, ciphertext) 元组 “”“ # 生成随机IV iv os.urandom(16) # 应用PKCS#7填充 padded_plaintext pkcs7_pad(plaintext) # 创建加密器并加密 cipher AES.new(key, AES.MODE_CBC, iv) ciphertext cipher.encrypt(padded_plaintext) return iv, ciphertext def aes_cbc_decrypt(key: bytes, iv: bytes, ciphertext: bytes) - bytes: “”“ AES-CBC解密 :param key: 16/24/32字节的密钥 :param iv: 初始化向量必须与加密时相同 :param ciphertext: 密文 :return: 原始明文字节 “”“ cipher AES.new(key, AES.MODE_CBC, iv) padded_plaintext cipher.decrypt(ciphertext) # 移除PKCS#7填充 plaintext pkcs7_unpad(padded_plaintext) return plaintext # 使用示例 key os.urandom(16) # 生产环境中应使用安全的密钥生成/派生方法 secret_message b‘This is my very secret API key: 12345-ABCDE’ print(“ 加密过程 “) iv, encrypted aes_cbc_encrypt(key, secret_message) print(f“密钥 (Hex): {key.hex()}“) print(f“IV (Hex): {iv.hex()}“) print(f“密文 (Base64): {base64.b64encode(encrypted).decode()}“) # 模拟传输通常将IV和密文拼接在一起 transmission_data iv encrypted print(f“传输数据 (IV密文, Hex): {transmission_data.hex()}“) print(“\n 解密过程 “) # 接收方从传输数据中分离IV和密文 received_iv transmission_data[:16] received_ciphertext transmission_data[16:] decrypted aes_cbc_decrypt(key, received_iv, received_ciphertext) print(f“解密出的明文: {decrypted.decode(‘utf-8’)}“)这个流程已经是一个可用于生产环境的基础版本了。它包含了随机IV、PKCS#7填充、以及完整的加密解密闭环。5. 高级主题其他加密模式、编码与密钥处理掌握了CBC和ECB你已经能解决80%的问题。但AES的世界不止于此为了应对更复杂的场景和满足更高的安全要求我们还需要了解其他模式和处理技巧。5.1 其他加密模式简介CFB模式密文反馈可以将块密码转换为自同步的流密码。它不需要填充因为加密是逐位或逐字节进行的。适合加密数据流如网络传输其中数据长度不是块大小的倍数。OFB模式输出反馈同样将块密码转换为流密码但它生成的密钥流与明文无关。这意味着可以提前生成密钥流加解密速度更快但一个位的错误会影响后续所有位。CTR模式计数器模式目前非常流行的一种模式。它也将块密码转换为流密码通过一个计数器每次加密递增来生成密钥流。CTR模式支持并行计算加密和解密可以同时处理多个块并且不需要填充。在需要高性能和随机访问加密数据的场景如磁盘加密中很有优势。使用pycryptodome实现CTR模式非常简单因为它也不需要填充from Crypto.Cipher import AES from Crypto.Util import Counter import os key os.urandom(16) plaintext b‘This is a message of any length for CTR mode.’ # CTR模式需要一个“计数器”对象这里使用随机初始值 # 参数 initial_value 可以是一个整数这里用一个随机数的前8字节 nonce os.urandom(8) # 创建一个计数器格式为nonce8字节 counter8字节从0开始 ctr Counter.new(64, prefixnonce) cipher AES.new(key, AES.MODE_CTR, counterctr) ciphertext cipher.encrypt(plaintext) print(f“CTR密文: {ciphertext.hex()}“) # 解密使用相同的计数器和密钥 # 注意CTR模式的解密和加密是同一个操作所以可以复用cipher对象但更安全的做法是新建 decipher AES.new(key, AES.MODE_CTR, counterctr) decrypted decipher.decrypt(ciphertext) print(f“解密明文: {decrypted}“)5.2 编码的“坑”Base64与Hex加密操作输入输出都是字节bytes。但在实际应用中我们经常需要处理字符串如从配置文件读取密钥或者需要将密文以文本形式存储/传输如放在JSON、URL或数据库中。这时就需要编码。Base64编码将二进制数据转换为由64个字符A-Z, a-z, 0-9, , /组成的字符串末尾可能用填充。它是最常见的用于在文本环境中安全传输二进制数据的方法因为其字符集在所有系统中都是安全的。使用base64.b64encode()和base64.b64decode()。Hex编码十六进制将每个字节转换为两个十六进制数字0-9, a-f。可读性更好但体积比Base64大数据量膨胀一倍。使用bytes.hex()和bytes.fromhex()。关键点加解密的核心逻辑永远在字节层面进行。编码/解码是外围的“包装”步骤。 一个标准的流程是明文字符串-encode(‘utf-8’)-加密-base64编码-存储/传输。 反向流程是读取Base64字符串-base64解码-解密-decode(‘utf-8’)-明文字符串。import base64 # 加密并编码 key b‘1234567890123456’ iv b‘abcdefghijklmnop’ plaintext_str “敏感数据张三的身份证号“ plaintext_bytes plaintext_str.encode(‘utf-8’) cipher AES.new(key, AES.MODE_CBC, iv) padded_bytes pkcs7_pad(plaintext_bytes) ciphertext_bytes cipher.encrypt(padded_bytes) # 将二进制密文转换为Base64字符串便于文本传输 ciphertext_b64 base64.b64encode(ciphertext_bytes).decode(‘ascii’) print(f“Base64密文: {ciphertext_b64}“) # 解密并解码 received_ciphertext_bytes base64.b64decode(ciphertext_b64) # 先解码为字节 decipher AES.new(key, AES.MODE_CBC, iv) decrypted_padded_bytes decipher.decrypt(received_ciphertext_bytes) decrypted_bytes pkcs7_unpad(decrypted_padded_bytes) decrypted_str decrypted_bytes.decode(‘utf-8’) print(f“解密后字符串: {decrypted_str}“)5.3 密钥的安全生成与派生在之前的例子中我们直接使用了硬编码的密钥或os.urandom(16)。在生产环境中这不够安全或灵活。生成强随机密钥os.urandom(16)对于生成一次性会话密钥是很好的。确保你的操作系统有足够的熵源。从密码派生密钥用户提供的密码口令通常长度和随机性都不够。绝对不能直接用作AES密钥。应该使用密钥派生函数KDF如PBKDF2Password-Based Key Derivation Function 2。from Crypto.Protocol.KDF import PBKDF2 from Crypto.Hash import SHA256 import os password “MySuperSecretPassword” # 用户输入的弱密码 # 盐Salt是随机生成的用于防止彩虹表攻击。盐不需要保密但每个密码都应使用不同的盐。 salt os.urandom(16) # 使用PBKDF2从密码和盐派生出一个32字节256位的密钥 # count是迭代次数增加次数可以大幅增加暴力破解的难度。推荐10万次以上。 key PBKDF2(password, salt, dkLen32, count100000, hmac_hash_moduleSHA256) print(f“派生出的密钥 (Hex): {key.hex()}“) print(f“使用的盐 (Hex): {salt.hex()}“) # 注意盐必须和派生出的密钥一起保存用于后续验证密码。6. 实战封装一个健壮、易用的AES工具类将上述所有知识点融合我们可以封装一个功能完整、易于使用且健壮的AES工具类。这个类将支持多种模式、自动处理填充、编码和解码。“”“ aes_utils.py - 一个全面的AES加密解密工具类 “”“ from Crypto.Cipher import AES from Crypto.Protocol.KDF import PBKDF2 from Crypto.Hash import SHA256 from Crypto.Util.Padding import pad, unpad # pycryptodome自带了pad/unpad函数但我们用自己的实现以理解原理 import base64 import os import hashlib class AESCipher: “”“ 支持CBC, ECB, CTR模式的AES加密解密工具类。 自动处理PKCS#7填充CBC/ECB模式和编解码。 “”“ def __init__(self, key: bytes, mode‘CBC’, ivNone): “”“ 初始化AES加密器。 :param key: 原始密钥字节。长度必须是16(AES-128), 24(AES-192), 或32(AES-256)。 :param mode: 加密模式可选 ‘CBC‘, ‘ECB‘, ‘CTR‘。 :param iv: 初始化向量字节。CBC模式必须提供CTR模式作为nonce的一部分。ECB模式忽略。 如果为None且为CBC模式则会自动生成。 “”“ self.key key self.mode mode.upper() self.iv iv # 验证密钥长度 if len(key) not in (16, 24, 32): raise ValueError(f“Invalid key length {len(key)}. Must be 16, 24, or 32 bytes.”) # 处理IV if self.mode ‘CBC‘: if self.iv is None: self.iv os.urandom(16) elif len(self.iv) ! 16: raise ValueError(“IV for CBC mode must be 16 bytes long.”) elif self.mode ‘CTR‘: # CTR模式需要一个nonce通常8字节来初始化计数器 if self.iv is None: self.iv os.urandom(8) # 作为nonce # 这里简化处理将iv作为nonce。实际CTR构造更灵活。 elif self.mode ‘ECB‘: self.iv None # ECB不需要IV else: raise ValueError(f“Unsupported mode: {mode}. Choose from CBC, ECB, CTR.”) staticmethod def create_key_from_password(password: str, salt: bytes None, iterations100000, key_length32): “”“ 使用PBKDF2从密码派生安全密钥。 :param password: 用户密码字符串。 :param salt: 盐字节。如果为None则自动生成。 :param iterations: PBKDF2迭代次数。 :param key_length: 派生密钥长度字节。16, 24, 32对应AES-128,192,256。 :return: (key, salt) 元组。 “”“ if salt is None: salt os.urandom(16) key PBKDF2(password, salt, dkLenkey_length, countiterations, hmac_hash_moduleSHA256) return key, salt def _get_cipher(self, for_encryptionTrue): “”“内部方法根据模式获取Cipher对象”“” if self.mode ‘CBC‘: return AES.new(self.key, AES.MODE_CBC, self.iv) elif self.mode ‘ECB‘: return AES.new(self.key, AES.MODE_ECB) elif self.mode ‘CTR‘: # 构建一个简单的计数器nonce (8字节) 计数器 (8字节从0开始) # 注意这是一个简化的示例生产环境可能需要更复杂的计数器逻辑。 from Crypto.Util import Counter # 确保self.iv是8字节作为nonce nonce self.iv if len(self.iv) 8 else self.iv[:8] ctr Counter.new(64, prefixnonce) # 64位计数器 return AES.new(self.key, AES.MODE_CTR, counterctr) else: raise RuntimeError(“Unsupported mode”) # 理论上不会发生 def encrypt(self, plaintext: bytes) - (bytes, bytes): “”“ 加密字节数据。 :param plaintext: 明文字节。 :return: 如果是CBC模式返回(iv, ciphertext)元组其他模式返回ciphertext。 “”“ if self.mode in (‘CBC‘, ‘ECB‘): # 需要填充 padded_data self._pad(plaintext) cipher self._get_cipher(for_encryptionTrue) ciphertext cipher.encrypt(padded_data) else: # CTR模式 # 不需要填充 cipher self._get_cipher(for_encryptionTrue) ciphertext cipher.encrypt(plaintext) if self.mode ‘CBC‘: return self.iv, ciphertext else: return ciphertext def decrypt(self, ciphertext: bytes, ivNone) - bytes: “”“ 解密密文。 :param ciphertext: 密文字节。 :param iv: 初始化向量字节。仅CBC模式且在初始化时未提供iv时需要。 :return: 明文字节。 “”“ # 处理CBC模式下外部传入的iv if self.mode ‘CBC‘ and iv is not None: self.iv iv cipher self._get_cipher(for_encryptionFalse) # 解密也用同样的方法获取cipher if self.mode in (‘CBC‘, ‘ECB‘): padded_plaintext cipher.decrypt(ciphertext) plaintext self._unpad(padded_plaintext) else: # CTR模式 plaintext cipher.decrypt(ciphertext) # CTR解密即加密 return plaintext def encrypt_str(self, plaintext_str: str, encoding‘utf-8’) - str: “”“加密字符串返回Base64编码的字符串CBC模式包含IV”“” plaintext_bytes plaintext_str.encode(encoding) if self.mode ‘CBC‘: iv, ciphertext_bytes self.encrypt(plaintext_bytes) # 将IV和密文拼接然后整体Base64编码 combined iv ciphertext_bytes return base64.b64encode(combined).decode(‘ascii’) else: ciphertext_bytes self.encrypt(plaintext_bytes) return base64.b64encode(ciphertext_bytes).decode(‘ascii’) def decrypt_str(self, ciphertext_b64: str, encoding‘utf-8’) - str: “”“解密Base64编码的字符串”“” ciphertext_combined base64.b64decode(ciphertext_b64) if self.mode ‘CBC‘: # 前16字节是IV后面是密文 iv ciphertext_combined[:16] ciphertext_bytes ciphertext_combined[16:] plaintext_bytes self.decrypt(ciphertext_bytes, iviv) else: ciphertext_bytes ciphertext_combined plaintext_bytes self.decrypt(ciphertext_bytes) return plaintext_bytes.decode(encoding) def _pad(self, data: bytes) - bytes: “”“PKCS#7填充”“” block_size AES.block_size padding_len block_size - (len(data) % block_size) if padding_len 0: padding_len block_size padding bytes([padding_len]) * padding_len return data padding def _unpad(self, padded_data: bytes) - bytes: “”“PKCS#7去填充”“” padding_len padded_data[-1] # 简单验证 if padding_len 1 or padding_len len(padded_data): raise ValueError(“Invalid padding”) if padded_data[-padding_len:] ! bytes([padding_len]) * padding_len: raise ValueError(“Invalid padding”) return padded_data[:-padding_len] # 使用示例 if __name__ “__main__“: print(“ 示例1: 使用固定密钥CBC模式 “) key b‘0’ * 32 # 32字节密钥AES-256 aes AESCipher(key, mode‘CBC’) secret “这是我的机密JSON: {\”token\“: \”abc123\“}“ encrypted_b64 aes.encrypt_str(secret) print(f“加密后(Base64): {encrypted_b64}“) # 解密时需要相同的密钥和模式。IV已包含在密文字符串中。 aes2 AESCipher(key, mode‘CBC’) # 新建一个对象IV会被encrypt_str生成的字符串覆盖 decrypted aes2.decrypt_str(encrypted_b64) print(f“解密后: {decrypted}“) assert decrypted secret print(“\n 示例2: 从密码派生密钥 “) password “UserInputPassword” derived_key, salt AESCipher.create_key_from_password(password, key_length32) print(f“派生密钥: {derived_key.hex()}“) print(f“使用的盐: {salt.hex()}“) aes_from_pwd AESCipher(derived_key, mode‘CBC’) encrypted_from_pwd aes_from_pwd.encrypt_str(“用密码派生的密钥加密的数据“) print(f“加密数据: {encrypted_from_pwd}“) # 为了解密我们必须使用相同的密码和盐重新派生密钥 derived_key2, _ AESCipher.create_key_from_password(password, saltsalt, key_length32) aes_from_pwd2 AESCipher(derived_key2, mode‘CBC’) decrypted_from_pwd aes_from_pwd2.decrypt_str(encrypted_from_pwd) print(f“解密数据: {decrypted_from_pwd}“) print(“\n 示例3: CTR模式无需填充 “) ctr_key os.urandom(16) aes_ctr AESCipher(ctr_key, mode‘CTR’) long_text “A” * 100 # 任意长度 encrypted_ctr aes_ctr.encrypt_str(long_text) decrypted_ctr aes_ctr.decrypt_str(encrypted_ctr) # CTR模式解密对象可以复用 print(f“CTR加密后长度: {len(encrypted_ctr)}“) print(f“CTR解密验证: {decrypted_ctr long_text}“)这个AESCipher类封装了密钥处理、模式选择、填充、编解码等繁琐细节提供了简单的字符串加密解密接口。你可以直接将它复制到你的项目中根据需要进行修改和扩展。7. 常见问题排查与实战避坑指南即使有了完善的工具在实际集成和跨系统交互时你依然会遇到各种问题。下面是我总结的常见“坑”及其解决方案。7.1 “Invalid padding” 或解密后乱码这是最常见的问题根本原因在于加密端和解密端的某个环节不一致。密钥不一致确保双方使用的密钥字节完全相同。检查是否有额外的空格、换行符或者编码不一致一个用UTF-8一个用GBK。最佳实践始终以十六进制Hex或Base64格式存储和传输密钥避免直接处理字符串。IV不一致CBC模式加密生成的IV必须原封不动地用于解密。检查传输和拼接过程是否有误。通常IV是密文的前16字节。填充模式不一致加密端用了PKCS#7填充解密端却用了ZeroPadding或没有去填充。确保双方使用相同的填充算法。pycryptodome的Crypto.Util.Padding模块提供了标准的pad和unpad函数建议使用。编码/解码环节出错在加密前你是否将字符串正确编码为字节解密后是否用相同的编码解码Base64编码解码是否正确建议流程在加密函数入口处立即将字符串转为字节在解密函数出口处再将字节转为字符串。中间所有操作都在字节层面进行。数据损坏在传输或存储过程中密文可能被截断或修改。考虑添加消息认证码MAC如HMAC来验证数据的完整性。7.2 与其他语言/平台如Java、PHP、OpenSSL的互通跨语言加密互通是个大坑核心在于确保所有参数完全一致。建立一个对照检查清单密钥长度128/192/256和字节序列。模式AES/CBC/PKCS5Padding (在Java中常叫PKCS5但实际是PKCS7)。IV生成方式随机和传递方式通常预置在密文前。填充明确指定为PKCS#7。编码明文到字节的编码如UTF-8密文到文本的编码如Base64。例如与Java的javax.crypto.Cipher互通// Java端 Cipher cipher Cipher.getInstance(“AES/CBC/PKCS5Padding”); SecretKeySpec keySpec new SecretKeySpec(keyBytes, “AES”); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); // 通常将IV和密文一起Base64输出 String result Base64.getEncoder().encodeToString(ivBytes encrypted);对应的Python端就需要使用CBC模式PKCS#7填充并正确分离IV和密文。7.3 性能与安全性考量模式选择默认使用CBC模式。对于需要并行加密或流式加密的场景考虑CTR或GCM模式GCM还提供认证功能。绝对不要使用ECB。密钥管理密钥不能硬编码在代码中。应该从环境变量、密钥管理服务如AWS KMS、HashiCorp Vault或经过加密的配置文件中读取。对于用户密码一定要使用PBKDF2、bcrypt或scrypt等算法进行密钥派生。IV的随机性每次加密都必须使用密码学安全的随机数生成器CSPRNG生成新的IV。os.urandom()在主流操作系统中是安全的。认证加密Authenticated EncryptionCBC模式本身只提供保密性不提供完整性。攻击者可能篡改密文导致解密出错误但看似合理的数据。对于高安全要求场景应使用GCM模式Galois/Counter Mode它同时提供保密性、完整性和认证。pycryptodome也支持AES.MODE_GCM。# 使用GCM模式的示例推荐用于新系统 from Crypto.Cipher import AES import os key os.urandom(16) plaintext b‘Authenticated and encrypted data’ # GCM模式需要一个nonce类似IV但不需要保密必须唯一 nonce os.urandom(12) # GCM推荐12字节nonce cipher AES.new(key, AES.MODE_GCM, noncenonce) ciphertext, tag cipher.encrypt_and_digest(plaintext) # 传输时需要 nonce, ciphertext, tag # 解密时验证tag decipher AES.new(key, AES.MODE_GCM, noncenonce) try: decrypted decipher.decrypt_and_verify(ciphertext, tag) print(“解密成功:”, decrypted) except ValueError: print(“消息被篡改或密钥错误“)最后记住加密是安全链条中的一环而非全部。安全的系统设计、最小权限原则、及时的依赖更新、以及防范侧信道攻击等同样重要。希望这篇超过五千字的详细指南能让你在Python中驾驭AES加密时从“碰运气”走向“心中有数”从容应对各种数据保护场景。