将前端JavaScript代码与AES加密集成时出现ValueError: 数据必须填充到CBC模式的16字节边界

0 投票
1 回答
61 浏览
提问于 2025-04-14 16:52

我现在正在用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 个回答

2

这比你想象的要简单得多。

这段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。

撰写回答