Search code examples
c++opensslcryptographycryptojscrypto++

RSA-CBC decrypt it using C++


I'm stuck with the problem of decrypting AES-CBC ecrypted string. I have JS code which decrypt that string but I need do that in C++. Key is string of SHA512 hash, and message is string of Base64. The JS code for decrypt:

CryptoJS.algo.AES.keySize = 32,
 CryptoJS.algo.EvpKDF.cfg.iterations = 10000,
  CryptoJS.algo.EvpKDF.cfg.keySize = 32;
var r = CryptoJS.AES.decrypt(message, key.toString());

My C++ code doesn't work

std::string generateIV(std::string key)
{
    std::string iv(CryptoPP::AES::BLOCKSIZE, 0);
    CryptoPP::SHA1().CalculateDigest((byte*)iv.data(), (byte*)key.data(), key.size());
    return iv;
}
std::string decrypt(std::string &message, std::string &key) {

    std::string decrypted;
    std::string iv(generateIV(key));

    // Create the AES decryption object
    CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption aesDecryption;
    aesDecryption.SetKeyWithIV((byte*)key.data(), key.size(), (byte*)iv.data(), iv.size());

    // Decrypt the message
    CryptoPP::StringSource ss(message, true,
        new CryptoPP::StreamTransformationFilter(aesDecryption,
            new CryptoPP::StringSink(decrypted)
        )
    );
    return decrypted;
}

Maybe I should use OpenSSL?


Solution

  • The ciphertext generated by the posted CryptoJS code cannot be decrypted by any AES compliant library. This is due to the line

    CryptoJS.algo.AES.keySize = 32
    

    which defines a keysize of 32 words = 32 * 4 = 128 bytes for key derivation. This is not a valid AES keysize and the derived number of rounds is not defined for AES at all (38 rounds for 128 bytes, see here; AES defines only 10, 12 and 14 rounds depending on the key size). The ciphertext is therefore not AES compliant. It can be decrypted with CryptoJS, but not by any AES compliant library, see also this CryptoJS issue #293. For the generated ciphertext to be AES compatible, one of the allowed AES key sizes must be used, e.g. a keysize of 8 words = 32 bytes:

    CryptoJS.algo.AES.keySize = 8
    

    Furthermore, note that line

    CryptoJS.algo.EvpKDF.cfg.iterations = 10000
    

    leads to incomapatability with the OpenSSL CLI, which by default uses an iteration count of 1 in key derivation (which is one of the reasons why this key derivation is weak, see here).

    By the way, the line

    CryptoJS.algo.EvpKDF.cfg.keySize = 32
    

    is completely ignored by the processing and can also be omitted.


    If a valid AES key size is used, e.g. 8 words = 32 bytes:

    CryptoJS.algo.AES.keySize = 8, // 8 words = 32 bytes
     CryptoJS.algo.EvpKDF.cfg.iterations = 10000,
      CryptoJS.algo.EvpKDF.cfg.keySize = 32;
    var r = CryptoJS.AES.decrypt(message, key.toString());
    

    the ciphertext can be decrypted programmatically. As already mentioned in the comments, CryptoJS uses the OpenSSL propritary key derivation function EVP_BytesToKey() if the key material is passed as string. This generates an 8 bytes salt during encryption and uses the salt and password to derive key and IV. These are used to encrypt in CBC mode with PKCS#7 padding by default. OpenSSL formats the result of the encryption as a concatenation of the ASCII encoding of Salted__, followed by the 8 bytes salt and finally by the actual ciphertext, usually Base64 encoded.

    For decryption, the salt and ciphertext must be separated. Then, based on salt and password, key and IV are to be determined, with which finally the ciphertext is decrypted.
    Thus, for decryption an implementation for EVP_BytesToKey() is needed. Such an implementation can be found for Crypto++ here in the Crypto++ docs, and a code with which the ciphertext of the CryptoJS code can be decrypted (after fixing the keysize issue) is e.g.:

    #include "aes.h"
    #include "modes.h"
    #define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1
    #include "md5.h"
    #include "base64.h"
    #include "secblock.h"
    
    static int OPENSSL_PKCS5_SALT_LEN = 8;
    
    int OPENSSL_EVP_BytesToKey(CryptoPP::HashTransformation& hash, const unsigned char* salt, const unsigned char* data, int dlen, unsigned int count, unsigned char* key, unsigned int ksize, unsigned char* iv, unsigned int vsize);
    
    int main(int, char**) {
    
        // Pass data and parameter
        std::string passphrase = "my passphrase";
        std::string encryptedB64 = "U2FsdGVkX18AuE7abdK11z8Cgn3Nc+2cELB1sWIPhAJXBZGhnw45P4l58o33IEiJ8fV4oEid2L8wKXpAntPrAQ=="; // CryptoJS ciphertext for a 32 bytes keysize 
        std::string encrypted;
        int iterationCount = 10000;
        int keySize = 32;
    
        // Base64 decode
        CryptoPP::StringSource ssB64decodeCt(encryptedB64, true,
            new CryptoPP::Base64Decoder(
                new CryptoPP::StringSink(encrypted)
            )
        );
    
        // Separate
        std::string salt(encrypted.substr(8, 8));
        std::string ciphertext(encrypted.substr(16));
    
        // Derive key
        CryptoPP::SecByteBlock key(keySize), iv(16);
        CryptoPP::Weak::MD5 md5;
        OPENSSL_EVP_BytesToKey(md5, (const unsigned char*)salt.data(), (const unsigned char*)passphrase.data(), passphrase.size(), iterationCount, key.data(), key.size(), iv.data(), iv.size());
    
        // Decryption
        std::string decryptedText;
        CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption decryption(key.data(), key.size(), iv.data());
        CryptoPP::StringSource ssDecryptCt(
            ciphertext,
            true,
            new CryptoPP::StreamTransformationFilter(
                decryption,
                new CryptoPP::StringSink(decryptedText),
                CryptoPP::BlockPaddingSchemeDef::BlockPaddingScheme::PKCS_PADDING
            )
        );
    
        // Output
        std::cout << decryptedText << std::endl; // The quick brown fox jumps over the lazy dog
    
        return 0;
    }
    
    // from: https://www.cryptopp.com/wiki/OPENSSL_EVP_BytesToKey
    int OPENSSL_EVP_BytesToKey(CryptoPP::HashTransformation& hash, const unsigned char* salt, const unsigned char* data, int dlen, unsigned int count, unsigned char* key, unsigned int ksize, unsigned char* iv, unsigned int vsize)
    {
        if (data == NULL) return (0);
    
        unsigned int nkey = ksize;
        unsigned int niv = vsize;
        unsigned int nhash = hash.DigestSize();
        CryptoPP::SecByteBlock digest(nhash);
    
        unsigned int addmd = 0, i;
    
        for (;;)
        {
            hash.Restart();
    
            if (addmd++)
                hash.Update(digest.data(), digest.size());
    
            hash.Update(data, dlen);
    
            if (salt != NULL)
                hash.Update(salt, OPENSSL_PKCS5_SALT_LEN);
    
            hash.TruncatedFinal(digest.data(), digest.size());
    
            for (i = 1; i < count; i++)
            {
                hash.Restart();
                hash.Update(digest.data(), digest.size());
                hash.TruncatedFinal(digest.data(), digest.size());
            }
    
            i = 0;
            if (nkey)
            {
                for (;;)
                {
                    if (nkey == 0) break;
                    if (i == nhash) break;
                    if (key != NULL)
                        *(key++) = digest[i];
                    nkey--;
                    i++;
                }
            }
            if (niv && (i != nhash))
            {
                for (;;)
                {
                    if (niv == 0) break;
                    if (i == nhash) break;
                    if (iv != NULL)
                        *(iv++) = digest[i];
                    niv--;
                    i++;
                }
            }
            if ((nkey == 0) && (niv == 0)) break;
        }
    
        return ksize;
    }