pythonc++encryptionaescrypto++

loss of data when decrypting a file in Python server from CPP client


I am trying to send a file from a client written in CPP to a server written in Python.

I am using AES encryption on client side to encrypt the file and then send it to the server, the AES key is saved on the server.

Encryption in python is performed with Crypto.Cipher and Crypto++ in CPP.

However, when the file arrives to the server, I am always losing first 16 characters of the original file.

For example:

txt file content on client side > "AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC"

txt file content after arriving to server > "BBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC"

I am not sure if it is because the server is skipping the data or is it because the client skips it.

std::string encryptFile(const std::string& filename, const CryptoPP::SecByteBlock& key) {
// Step 1: Read the file to be encrypted
std::ifstream input_file(filename, std::ios::binary);
std::string plaintext((std::istreambuf_iterator<char>(input_file)),
    std::istreambuf_iterator<char>());

CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);

CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption encryptor(key, key.size(), iv);

// Step 2: Encrypt the file
std::string ciphertext;
CryptoPP::StringSource s(plaintext, true,
    new CryptoPP::StreamTransformationFilter(encryptor,
        new CryptoPP::StringSink(ciphertext)));

return ciphertext;

}

function on python server:

def decrypt_file(self, encrypted_data, aes_key, file_path):
        key = base64.b64decode(aes_key)
        iv = encrypted_data[:AES.block_size]
        cipher = AES.new(key, AES.MODE_CBC, iv=iv)
        plaintext = cipher.decrypt(encrypted_data[AES.block_size:])
        # Remove PKCS#7 padding
        padding_len = plaintext[-1]
        plaintext = plaintext[:-padding_len]

        with open(file_path, "wb") as f:
            f.write(plaintext)

What is the best way to solve this issue?


Solution

  • Commonly, when encrypting with AES and CBC, first a random 16 bytes IV is generated, then encryption is performed, and then IV and ciphertext are concatenated.
    During decryption, the IV and ciphertext are separated based on the known length of the IV and decryption is performed. The disclosure of the IV is no problem, because the IV is not secret.
    Note that the use of a static IV is not an option for security reasons.

    While the Python code follows this procedure (i.e. separates IV and ciphertext, performs decryption), the C code lacks the concatenation of IV and ciphertext.
    Therefore, in the Python code the first block is used as IV and the rest as ciphertext. As a result, the first block is missing in the decrypted plaintext (from the CBC flowchart it can be seen that the first ciphertext block is indeed the IV of the second ciphertext block).

    As fix, in the C code IV and ciphertext must be concatenated in the order iv|ciphertext:

    return std::string(iv.begin(), iv.end()) + ciphertext; 
    

    Furthermore, the initialization of the IV with random values is missing in the C code. This can be achieved as follows (see AutoSeededRandomPool):

    #include "osrng.h"
    ...
    CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);
    CryptoPP::AutoSeededRandomPool prng;
    prng.GenerateBlock(iv, iv.size());
    

    The C code uses PKCS#7 padding by default (see StreamTransformationFilter, by default DEFAULT_PADDING is applied to the 3rd parameter, which is PKCS_PADDING for block ciphers).
    This is compatible with the manual unpadding in the Python code. However, it is more robust to use the corresponding PyCryptodome module Crypto.Util.Padding for unpadding.

    from Crypto.Util.Padding import unpad
    ...
    plaintext = unpad(plaintext, 16)