解决前后端AES加密集成中的Data must be padded错误
问题描述
在实现前后端AES加密集成时,后端Python代码抛出如下错误:
ValueError: Data must be padded to 16 byte boundary in CBC mode
这表明前端加密后的数据格式与后端期望的格式不匹配,主要问题出现在密钥派生、初始化向量(IV)处理以及数据填充方式上。
后端Python实现
后端使用OpenSSL兼容的加密方案,核心点包括:
- 密钥派生使用
EVP_BytesToKey()方法,默认采用MD5摘要算法,单次迭代,8字节盐值 - 加密算法为AES-256-CBC,配合PKCS#7填充
- 输出格式为Base64编码的OpenSSL格式:
Salted__+ 8字节盐值 + 密文
class CryptoHandler():
def apply_padding(self, data):
block_size = 16
pad_len = block_size - (len(data) % block_size)
return data + (chr(pad_len) * pad_len)
def remove_padding(self, data):
return data[:-(data[-1] if isinstance(data[-1], int) else ord(data[-1]))]
def derive_key_iv_md5(self, password, salt, length=48):
assert len(salt) == 8
combined = password + salt
key = hashlib.md5(combined).digest()
result = key
while len(result) < length:
key = hashlib.md5(key + combined).digest()
result += key
return result[:length]
def derive_key_iv_sha256(self, password, salt, length=48):
assert len(salt) == 8
combined = password + salt
key = hashlib.sha256(combined).digest()
result = key
while len(result) < length:
key = hashlib.sha256(key + combined).digest()
result += key
return result[:length]
# 加密函数
def encrypt_aes(self, plaintext, passphrase, use_md5=True):
salt = os.urandom(8)
if use_md5:
key_iv = self.derive_key_iv_md5(passphrase.encode(), salt)
else:
key_iv = self.derive_key_iv_sha256(passphrase.encode(), salt)
aes_key = key_iv[:32]
aes_iv = key_iv[32:]
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
padded_data = self.apply_padding(plaintext).encode()
encrypted = cipher.encrypt(padded_data)
return base64.b64encode(b"Salted__" + salt + encrypted).decode()
# 解密函数
def decrypt_aes(self, ciphertext_b64, passphrase, use_md5=True):
raw = base64.b64decode(ciphertext_b64)
assert raw[:8] == b"Salted__"
salt = raw[8:16]
if use_md5:
key_iv = self.derive_key_iv_md5(passphrase.encode(), salt)
else:
key_iv = self.derive_key_iv_sha256(passphrase.encode(), salt)
aes_key = key_iv[:32]
aes_iv = key_iv[32:]
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
decrypted = cipher.decrypt(raw[16:])
return self.remove_padding(decrypted).decode().strip('"')
# 使用示例
handler = CryptoHandler()
original = "需要加密传输的敏感数据"
encrypted = handler.encrypt_aes(original, "my_secret_passphrase")
print("加密结果:", encrypted)
decrypted = handler.decrypt_aes(encrypted, "my_secret_passphrase")
print("解密结果:", decrypted)
前端JavaScript兼容实现(使用CryptoJS)
CryptoJS默认兼容OpenSSL格式,因此无需手动处理密钥派生和IV,直接传入密码短语即可。根据后端使用的摘要算法选择对应配置:
方案一:MD5摘要(与bytes_to_key_md5对应)
// 注意:MD5是CryptoJS默认摘要算法,无需额外配置
const backendCipherMD5 = "U2FsdGVkX18lJwVCQIbRWqiIycIZg4LRZFHq+ORvygkE/umH1Il3m/yzgu3n9jVQhUikwXeURBW9yAjMawTk3A==";
const passphrase = "my_secret_passphrase";
const decryptedMD5 = CryptoJS.AES.decrypt(backendCipherMD5, passphrase);
console.log("解密结果(MD5):", decryptedMD5.toString(CryptoJS.enc.Utf8));
const plaintextMD5 = "需要加密传输的敏感数据";
const encryptedForBackendMD5 = CryptoJS.AES.encrypt(plaintextMD5, passphrase);
console.log("加密结果(MD5):", encryptedForBackendMD5.toString());
方案二:SHA256摘要(与bytes_to_key对应)
// 显式指定使用SHA256
CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.SHA256.create();
const backendCipherSHA = "U2FsdGVkX189ft5ncnmOK/rJIB2fkdrfdWQCbf6DgbXkWMXw7yjX2oRXbDgZTIt4LibWBPamalnKCZl3l1VnWQ==";
const decryptedSHA = CryptoJS.AES.decrypt(backendCipherSHA, passphrase);
console.log("解密结果(SHA256):", decryptedSHA.toString(CryptoJS.enc.Utf8));
const plaintextSHA = "需要加密传输的敏感数据";
const encryptedForBackendSHA = CryptoJS.AES.encrypt(plaintextSHA, passphrase);
console.log("加密结果(SHA256):", encryptedForBackendSHA.toString());
React组件完整示例
import React from 'react';
import CryptoJS from 'crypto-js';
const SecureCommunication = () => {
const passphrase = "my_secret_passphrase";
const encryptToBackend = (data, useSHA256 = false) => {
if (useSHA256) {
CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.SHA256.create();
} else {
CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.MD5.create();
}
const cipher = CryptoJS.AES.encrypt(data, passphrase);
return cipher.toString();
};
const decryptFromBackend = (ciphertext, useSHA256 = false) => {
if (useSHA256) {
CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.SHA256.create();
} else {
CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.MD5.create();
}
const decrypted = CryptoJS.AES.decrypt(ciphertext, passphrase);
return decrypted.toString(CryptoJS.enc.Utf8);
};
const testCommunication = () => {
// 测试MD5方案
const testData = "测试数据123";
const encryptedMD5 = encryptToBackend(testData, false);
console.log("MD5加密结果:", encryptedMD5);
console.log("MD5解密结果:", decryptFromBackend(encryptedMD5, false));
};
return (
<div>
<button onClick={testCommunication}>测试加密通信</button>
</div>
);
};
export default SecureCommunication;
关键要点
- 兼容性根源:前后端必须使用相同的密钥派生算法(摘要类型相同)、相同的盐值处理方式和相同的填充标准(PKCS#7)
- 调试建议:
- 确保前后端使用完全相同的密码短语
- 验证Base64编码/解码过程无误
- 检查密文是否以"Salted__"开头
- 安全性警告:EVP_BytesToKey()搭配MD5和单次迭代是不安全的,生产环境建议升级到PBKDF2或bcrypt等现代密钥派生函数
- 注意事项:CryptoJS已停止维护(4.2.0为最终版本),新项目建议使用Web Crypto API或更现代的安全库