2

I am working on client-server communications and am stuck on making sure that both sides come up with the same encrypted token value. Cannot figure out why they are different. The keys and initialization vectors along with the message itself are all the same.

Here is the function that does encryption in client code:

int main()
{

try
    {
        std::string message = "HelloWorld";

        while ((message.size() & 0xf) != 0xf)
            message += " ";

        size_t inputslength = message.length();
        unsigned char aes_input[inputslength];

        memset(aes_input, 0, inputslength/8);

        strcpy((char*) aes_input, message.c_str());





        unsigned char iv[] = {'0','f','9','3','8','7','b','3','f','9','4','b','f','0','6','f'};

        unsigned char aes_key[] = {'Z','T','k','0','Y','T','U','5','Y','j','N','h','M','j','k','4','N','G','I','3','N','m','I','x','N','W','E','x','N','z','d','i'};

        // buffers for encryption and decryption
        const size_t encslength = ((inputslength + AES_BLOCK_SIZE) / AES_BLOCK_SIZE) * AES_BLOCK_SIZE;
        unsigned char enc_out[encslength];
        unsigned char dec_out[inputslength];
        memset(enc_out, 0, sizeof(enc_out));
        memset(dec_out, 0, sizeof(dec_out));

        AES_KEY enc_key, dec_key;
        AES_set_encrypt_key(aes_key, AES_KEYLENGTH, &enc_key);
        AES_cbc_encrypt(aes_input, enc_out, inputslength, &enc_key, iv, AES_ENCRYPT);

        AES_set_decrypt_key(aes_key, AES_KEYLENGTH, &dec_key);



        AES_cbc_encrypt(enc_out, dec_out, encslength, &dec_key, iv, AES_DECRYPT);

        printf("original:\t");
        hex_print(aes_input, sizeof(aes_input));
        printf("encrypt:\t");
        hex_print(enc_out, sizeof(enc_out));

        printf("decrypt:\t");
        hex_print(dec_out, sizeof(dec_out));

        std::stringstream ss;
        for(int i = 0; i < encslength; i++)
        {
            ss << enc_out[i];
        }
            return 0;
     }
 }

Output

original: 48 65 6C 6C 6F 57 6F 72 6C 64 20 20 20 20 20 
encrypt: 72 70 A2 0D FB A1 65 15 17 97 6E 5D 36 23 E2 FA 
decrypt: 0A 73 F7 52 AC C1 68 54 1D CA 7A 1F 70 33 F4 

Meanwhile.. on the server:

function encryptToken(token)
{
    const iv = '0f9387b3f94bf06f';
    const key = 'ZTk0YTU5YjNhMjk4NGI3NmIxNWExNzdi';

    console.log("key len: " + key.length);

    const encrypt = (value) => {
       const cipher = crypto.createCipheriv('AES-256-CBC', key, iv);
       let encrypted = cipher.update(value, 'utf8', 'hex');
       encrypted += cipher.final('hex');
       return encrypted;
    };

    console.log('Encrypteddd value: ', encrypt('HelloWorld'));
}

Output

Encrypteddd value:  0c491f8c5256b9744550688fc54926e8

Before trying CBC-256 for encryption, I tried the simpler encryption mode, ECB-128 and it all comes down to the same problem. Different encryption tokens produced on client and server side which results in not being able to decrypt what comes from the server side. Any brainstorming tips will help please. I am running out of ideas, thanks.

Update 12.26 -

After taking advice on the init vector and array length on client side.. here is my updated code with output:

int main()
{

try
    {
        std::string message = "HelloWorld";
    while ((message.size() & 0xf) != 0xf)
        message += " ";

    size_t inputslength = message.length();
    unsigned char aes_input[inputslength+1];

    memset(aes_input, 0, inputslength/8);

    strcpy((char*) aes_input, message.c_str());





    unsigned char iv[] = {0x0f, 0x93, 0x87, 0xb3, 0xf9, 0x4b, 0xf0, 0x6f};
    unsigned char aes_key[] = {'Z','T','k','0','Y','T','U','5','Y','j','N','h','M','j','k','4','N','G','I','3','N','m','I','x','N','W','E','x','N','z','d','i'};

    // buffers for encryption and decryption
    const size_t encslength = ((inputslength + AES_BLOCK_SIZE) / AES_BLOCK_SIZE) * AES_BLOCK_SIZE;
    unsigned char enc_out[encslength];
    unsigned char dec_out[inputslength];
    memset(enc_out, 0, sizeof(enc_out));
    memset(dec_out, 0, sizeof(dec_out));

    AES_KEY enc_key, dec_key;
    AES_set_encrypt_key(aes_key, AES_KEYLENGTH, &enc_key);
    AES_cbc_encrypt(aes_input, enc_out, inputslength, &enc_key, iv, AES_ENCRYPT);

    AES_set_decrypt_key(aes_key, AES_KEYLENGTH, &dec_key);



    AES_cbc_encrypt(enc_out, dec_out, encslength, &dec_key, iv, AES_DECRYPT);

    printf("original:\t");
    hex_print(aes_input, sizeof(aes_input));
    printf("encrypt:\t");
    hex_print(enc_out, sizeof(enc_out));

    printf("decrypt:\t");
    hex_print(dec_out, sizeof(dec_out));

    std::stringstream ss;
    for(int i = 0; i < encslength; i++)
    {
        ss << enc_out[i];
    }
        return 0;
 }

}

//Output:
original:   48 65 6C 6C 6F 57 6F 72 6C 64 00 
encrypt:    54 CD 98 20 59 D9 7B 2D D4 23 ED EC D0 13 97 59 

Nodejs code has not changed and this remains the output:

Encrypteddd value:  0c491f8c5256b9744550688fc54926e8
KS7X
  • 332
  • 2
  • 15
  • Buffer overrun in the C++ version (strcpy into the too-short VLA). Undefined behavior. May or may not be the issue, but until the buffer overrun is fixed nothing further can be said. The C++ version also implements manual padding, but the manual padding is obviously broken, one byte too few (which nicely corresponds to the buffer overrun). Don't see the equivalent padding in the node.js version, not familiar with its libraries to know what node.js's libraries handle the equivalent padding themselves. P.S. - the memset accomplishes absolutely nothing useful, whatsoever. – Sam Varshavchik Dec 24 '18 at 22:30
  • How is there a buffer overrun? The size of aes_input should match the length of the message string. You are right about the memset.. came from sample code but it does not really do anything. Does not affect results, though – KS7X Dec 24 '18 at 22:37
  • Your `IV's` are different. The NodeJS side is turning that hex-char sequence into a a block-size byte sequence, your C++ code does not do that. So start with that. Unrelated, join modern OpenSSL world: use the EVP family. – WhozCraig Dec 24 '18 at 22:43
  • @WhozCraig can you explain a little more please on what you mean by block-size byte sequence? – KS7X Dec 24 '18 at 22:47
  • `aes_input[inputslength+1]` (also lookup PKCS7 padding, which node crypto uses by default). – rustyx Dec 24 '18 at 22:50
  • @rustyx I just read that OpenSSL also uses PKCS7 padding by default. I took out the piece of code in my client (c++) that does the manual padding to let the openssl do it's thing. Still not getting a match on encrypted tokens. – KS7X Dec 24 '18 at 23:08
  • I am still trying to find out what @WhozCraig meant by the different sequences in IVs as that seems to be my only lead now – KS7X Dec 24 '18 at 23:08
  • `unsigned char iv[] = {0x0f, 0x93, 0x87, 0xb3, 0xf9, 0x4b, 0xf0, 0x6f};` . Still not sure about your key, as it appears to be a base64 encoding of something, so not sure what NodeJS is doing there, but iv in the C++ code is clearly wrong. Yours is a sequence sixteen characters; it needs to be a sequence of eight octets. – WhozCraig Dec 24 '18 at 23:14
  • I changed the iv to represent eight octets. I also changed the key on both sides to be of the same type. `const key = '01020304050607080910111213141516';` `unsigned char aes_key[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16};` Still no match :/ – KS7X Dec 24 '18 at 23:39
  • How there's a buffer overrun? Very easy. The very first thing that everyone learns when they start learning C and C++ is that C-style strings have a null-terminated byte. Every C-style string is followed by a `'\0'`. `message.length()` does ***not*** include the `'\0'`, so `strcpy()` ends up copying `message.length()+1` bytes into an VLA that's one byte too short. Buffer overrun. Undefined behavior. Memory corruption. – Sam Varshavchik Dec 25 '18 at 15:05

1 Answers1

2

So, here's the deal. Every call to AES_cbc_encrypt will change the value of the initialization vector. They do that so that you can chain calls to AES_*_encrypt and handle messages larger than one block. But because the encryption call changes the value of iv, the decryption call is getting a different initialization vector.

One (terrible) solution would be to make two vectors:

unsigned char iv_encrypt[] = { /* stuff */ };
unsigned char iv_decrypt[] = { /* same stuff */ };

That way you'd be passing the same data to each AES_cbc_encrypt call. That would at least show that you can decrypt to the original data. A better, flexible way to achieve your end would be to use a clone of your initialization vector for each call. Something like:

unsigned char iv[] = { /* stuff */ };
unsigned char *tmp_iv = static_cast<unsigned char*>( malloc( sizeof( iv ) ) );
...
memcpy( tmp_iv, iv, sizeof(iv) );
AES_cbc_encrypt(aes_input, enc_out, inputslength, &enc_key, tmp_iv, AES_ENCRYPT);
...
memcpy( tmp_iv, iv, sizeof(iv) );
AES_cbc_encrypt(enc_out, dec_out, inputslength, &dec_key, tmp_iv, AES_DECRYPT);
wyrm
  • 318
  • 1
  • 12
  • Oh, to start poking at the javascript, try using 'binary' as both your input and output encoding to `cypher.update` and `cypher.final`. – wyrm Dec 27 '18 at 00:02
  • Thanks for your input man. Cloning the init vector did fix the issue where it was not producing the expected decoded token. As for the javascript.. that has been a pain. Good news is that I got it working now referencing an example from the OpenSSL EVP family. – KS7X Dec 27 '18 at 23:09