将前端JavaScript代码与AES加密集成时出现ValueError: 数据必须填充到CBC模式的16字节边界
我现在正在用Python在后台实现AES加密,但在确保前端和后台兼容性方面遇到了一些问题。我需要帮助把前端的JavaScript代码和它结合起来。
我的后台Python代码:
class Crypt():
def pad(self, data):
BLOCK_SIZE = 16
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length)*length)
def unpad(self, data):
return data[:-(data[-1] if type(data[-1]) == int else ord(data[-1]))]
def bytes_to_key(self, data, salt, output=48):
assert len(salt) == 8, len(salt)
data += salt
key = sha256(data).digest()
final_key = key
while len(final_key) < output:
key = sha256(key + data).digest()
final_key += key
return final_key[:output]
def bytes_to_key_md5(self, data, salt, output=48):
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def encrypt(self, message):
passphrase = "<secret passpharse value>".encode()
salt = Random.new().read(8)
key_iv = self.bytes_to_key_md5(passphrase, salt, 32+16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return base64.b64encode(b"Salted__" + salt + aes.encrypt(self.pad(message).encode()))
def decrypt(self, encrypted):
passphrase ="<secret passpharse value>".encode()
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = self.bytes_to_key_md5(passphrase, salt, 32+16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return self.unpad(aes.decrypt(encrypted[16:])).decode().strip('"')
def base64_decoding(self, encoded):
base64decode = base64.b64decode(encoded)
return base64decode.decode()
crypt = Crypt()
test = "secret message to be send over network"
encrypted_message = crypt.encrypt(test)
print("Encryp msg:", encrypted_message)
decrypted_message = crypt.decrypt(encrypted_message)
print("Decryp:", decrypted_message)
这是我在前端使用React和CryptoJS尝试的内容:
import React from "react";
import CryptoJS from 'crypto-js';
const DecryptEncrypt = () => {
function bytesToKey(passphrase, salt, output = 48) {
if (salt.length !== 8) {
throw new Error('Salt must be 8 characters long.');
}
let data = CryptoJS.enc.Latin1.parse(passphrase + salt);
let key = CryptoJS.SHA256(data).toString(CryptoJS.enc.Latin1);
let finalKey = key;
while (finalKey.length < output) {
data = CryptoJS.enc.Latin1.parse(key + passphrase + salt);
key = CryptoJS.SHA256(data).toString(CryptoJS.enc.Latin1);
finalKey += key;
}
return finalKey.slice(0, output);
}
const decryptData = (encryptedData, key) => {
const decodedEncryptedData = atob(encryptedData);
const salt = CryptoJS.enc.Hex.parse(decodedEncryptedData.substring(8, 16));
const ciphertext = CryptoJS.enc.Hex.parse(decodedEncryptedData.substring(16));
const keyIv = bytesToKey(key, salt.toString(), 32 + 16);
const keyBytes = CryptoJS.enc.Hex.parse(keyIv.substring(0, 32));
const iv = CryptoJS.enc.Hex.parse(keyIv.substring(32));
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: ciphertext },
keyBytes,
{ iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
);
return decrypted.toString(CryptoJS.enc.Utf8);
};
const encryptData = (data, key) => {
const salt = CryptoJS.lib.WordArray.random(8); // Generate random salt
const keyIv = bytesToKey(key, salt.toString(), 32 + 16);
const keyBytes = CryptoJS.enc.Hex.parse(keyIv.substring(0, 32));
const iv = CryptoJS.enc.Hex.parse(keyIv.substring(32));
const encrypted = CryptoJS.AES.encrypt(data, keyBytes, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const ciphertext = encrypted.ciphertext.toString(CryptoJS.enc.Hex);
const saltedCiphertext = "Salted__" + salt.toString(CryptoJS.enc.Hex) + ciphertext;
return btoa(saltedCiphertext);
};
const dataToEncrypt = 'Data to be sent over network';
const encryptionKey = "<secret passpharse value>";
const encryptedData = encryptData(dataToEncrypt, encryptionKey);
console.log("Encrypted data:", encryptedData);
const decryptedData = decryptData(encryptedData, encryptionKey);
console.log("Decrypted data:", decryptedData);
return (<>
Check
</>);
}
export default DecryptEncrypt;
我在确保前端和后台兼容性方面遇到了一些问题。具体来说,我在正确生成密钥和初始化向量(IV)以及加密/解密数据时遇到了困难,想要确保这些操作和后台实现一致。当我尝试将加密文本发送到后台时,解密时出现了以下错误:
packages\Crypto\Cipher\_mode_cbc.py", line 246, in decrypt
raise ValueError("Data must be padded to %d byte boundary in CBC mode" % self.block_size)
ValueError: Data must be padded to 16 byte boundary in CBC mode
我对在全栈应用中实现AES有点陌生,所以在学习和尝试,但仍然被这个问题困住。有没有遇到过类似问题或者在JavaScript中实现过加密/解密的人能给我一些指导或建议,告诉我如何修改我的前端代码以实现与后台的兼容性?
1 个回答
这比你想象的要简单得多。
这段Python代码实现了符合OpenSSL标准的加密和解密:
- 密钥生成使用的是
EVP_BytesToKey()
,它用MD5作为摘要,迭代次数为1,并且使用了8字节的盐值。 - 加密和解密使用的是AES-256的CBC模式,并且采用了PKCS#7填充。
- 结果以Base64编码的OpenSSL格式返回。这个格式由
Salted__
的ASCII编码、8字节的盐值和实际的密文拼接而成。
CryptoJS与OpenSSL兼容(可以在这里查看),并默认支持上述的加密和解密。你只需要将密码短语作为字符串传入即可(详见这里)。
需要注意的是,当在Python代码中使用bytes_to_key()
而不是bytes_to_key_md5()
时,CryptoJS那边必须明确指定SHA256作为摘要,因为MD5是默认的。
CryptoJS示例代码:
// MD5 sample
var ciphertextFromPython = "U2FsdGVkX18lJwVCQIbRWqiIycIZg4LRZFHq+ORvygkE/umH1Il3m/yzgu3n9jVQhUikwXeURBW9yAjMawTk3A==";
var passphrase = "<secret passpharse value>";
var decrypted = CryptoJS.AES.decrypt(ciphertextFromPython, passphrase);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
var plaintext = "secret message to be send over network";
var passphrase = "<secret passpharse value>";
var ciphertextForPython = CryptoJS.AES.encrypt(plaintext, passphrase);
console.log(ciphertextForPython.toString()); // e.g. U2FsdGVkX18/aYM99XaqbT/GjFDAuNlGBMd2Wd7Vuum120DkmeItS7tJndPLbxDyNzEUBF28AOG5pOwLGvpSSA==
// SHA-256 sample
CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.SHA256.create();
var ciphertextFromPython = "U2FsdGVkX189ft5ncnmOK/rJIB2fkdrfdWQCbf6DgbXkWMXw7yjX2oRXbDgZTIt4LibWBPamalnKCZl3l1VnWQ==";
var passphrase = "<secret passpharse value>";
var decrypted = CryptoJS.AES.decrypt(ciphertextFromPython, passphrase);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
var plaintext = "secret message to be send over network";
var passphrase = "<secret passpharse value>";
var ciphertextForPython = CryptoJS.AES.encrypt(plaintext, passphrase);
console.log(ciphertextForPython.toString()); // e.g. U2FsdGVkX188W7G1Xis9KZogKpVCvCVbDQHc1AIul+CSTjS8m+zdc4pPQ9jlunIP4jbTD49q82GV9ic/4HVNNA==
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
在上面的CryptoJS代码中,ciphertextFromPython
是用提供的Python代码生成的,而ciphertextForPython
可以用提供的Python代码解密。
第一个示例使用MD5作为摘要(默认),对应的密钥生成函数是bytes_to_key_md5()
,第二个示例使用SHA256(明确指定),对应的是bytes_to_key()
。
安全性:
密钥生成函数EVP_BytesToKey()
,特别是与已经被破解的MD5摘要和迭代次数为1结合使用,被认为是不安全的。应该使用更可靠的密钥生成函数(至少是PBKDF2,CryptoJS也支持这个)。
需要注意的是,CryptoJS已经被停止维护,最后一个版本是4.2.0。