AES-128-ECB encrypt in esp32 and decrypt in node.js

81 views Asked by At

I wanted to create an URL with aes-128-ecb encrypted query paramter in esp32 and decrypt it in node.js. Here is my approach on ESP32:

uint64_t variableToEncrypt = 27483611521760; // it comes as a 64 bit unsgined int from efuse
std::string AES_KEY = "0123456789ABCDEF"; // hex format
const char* serverBaseURL = "https://testNodeJS.com/";

void generateURL(){
    // Determine the size of the variable
    int bufferSize = snprintf(NULL, 0, "%" PRIu64, variableToEncrypt);
    // Make static arrays
    char URL_BUFFER[270];
    char stringVariableBuffer[bufferSize + 1];

    // Convert the variableToEncrypt to a string
    snprintf(stringVariableBuffer, bufferSize + 1, "%" PRIu64, variableToEncrypt);

    // Encrypt the variable
    std::string encryptedHex = cipher.encrypt(stringVariableBuffer, AES_KEY);

    // Construct the URL
    snprintf(URL_BUFFER, sizeof(URL_BUFFER), "%testDecrypt?key=%s", serverBaseURL, encryptedHex.c_str());

    Serial.printf("Raw variable (uint64_t): %llu\n", variableToEncrypt);
    Serial.printf("Variable (string): %s\n", stringVariableBuffer);
    Serial.printf("Encrypted Hex: %s\n", encryptedHex.c_str());
    Serial.printf("URL Buffer: %s\n", URL_BUFFER);
}

This function produces the following outputs:

Raw variable(uint64_t): 27483611521760
Variable(string): 27483611521760
Encrypted Hex: 4221458b03cc8692c969e5aa9aac4f31
URL Buffer: https://testNodeJS.com/testDecrypt?key=4221458b03cc8692c969e5aa9aac4f31

I confirmed with an online tool that it is ok. It was able to decrypt the Encrypted hex string back to the raw variable string.

AES-128-ECB decrypt

Now if i click on this link it will take me to my node.js test environment where i want to decrypt it

const crypto = require('crypto');

function decryptAES(ciphertextHex, key) {
    try{
        // Convert ciphertext from hex string to buffer
        const ciphertext = Buffer.from(ciphertextHex, 'hex');
        // Create decipher object
        const decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from(key, 'hex'), Buffer.alloc(0));
        // Decrypt ciphertext
        let decrypted = decipher.update(ciphertext);
        decrypted = Buffer.concat([decrypted, decipher.final()]);
        // Convert decrypted buffer to string
        const plaintext = decrypted.toString();
        return plaintext;
    }catch(error){
        console.log(error);
        return null;
    }
}


app.get('/testDecrypt', (req, res) => {
    const decryptedVariable = decryptAES(req.query.key, process.env.AES_KEY);
    console.log('Decrypted variable:', decryptedVariable , "Raw query variable: ", req.query.key, "AES key: ", process.env.AES_KEY);

    if(!decryptedVariable){
        return res.status(401).json({ message: 'Wrong variable!' });
    }
    return res.json({message:"ok"});
});

But the node.js decryptAES function always return null and the following error:

RangeError: Invalid key length
    at Decipheriv.createCipherBase (node:internal/crypto/cipher:121:19)
    at Decipheriv.createCipherWithIV (node:internal/crypto/cipher:140:3)
    at new Decipheriv (node:internal/crypto/cipher:289:3)
    at Object.createDecipheriv (node:crypto:154:10)
    at decryptAES (/opt/render/project/src/server.js:44:33)
    at /opt/render/project/src/server.js:74:29
    at Layer.handle [as handle_request] (/opt/render/project/src/node_modules/express/lib/router/layer.js:95:5)
    at next (/opt/render/project/src/node_modules/express/lib/router/route.js:144:13)
    at Route.dispatch (/opt/render/project/src/node_modules/express/lib/router/route.js:114:3)
    at Layer.handle [as handle_request] (/opt/render/project/src/node_modules/express/lib/router/layer.js:95:5) {
  code: 'ERR_CRYPTO_INVALID_KEYLEN'
}
Decrypted variable: null Raw query variable:  4221458b03cc8692c969e5aa9aac4f31 AES key:  0123456789ABCDEF

process.env.AES_KEY is stored like this in env file: AES_KEY=0123456789ABCDEF

Here is my encrypt function on esp32 which uses mbedtls

#include "mbedtls/aes.h"

std::string CryptoCipher::encrypt(const std::string& plaintext, const std::string& key) {
    mbedtls_aes_context aes_ctx;
    mbedtls_aes_init(&aes_ctx);

    // Set encryption key
    mbedtls_aes_setkey_enc(&aes_ctx, reinterpret_cast<const unsigned char*>(key.c_str()), 128);

    // Pad plaintext to block size if necessary
    int padded_length = ((plaintext.length() + 15) / 16) * 16;
    std::string padded_plaintext = plaintext;
    padded_plaintext.resize(padded_length, '\0');

    // Allocate memory for ciphertext
    std::string ciphertext(padded_length, '\0');

    // Perform encryption
    for (size_t i = 0; i < padded_length; i += 16) {
        mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_ENCRYPT, reinterpret_cast<const unsigned char*>(padded_plaintext.c_str() + i), reinterpret_cast<unsigned char*>(ciphertext.data() + i));
    }

    // Convert ciphertext to hex string
    std::string hex_string;
    hex_string.reserve(ciphertext.length() * 2);
    for (unsigned char c : ciphertext) {
        char buf[3];
        snprintf(buf, sizeof(buf), "%02x", static_cast<unsigned int>(c));
        hex_string.append(buf);
    }

    mbedtls_aes_free(&aes_ctx);
    
    return hex_string;
}
1

There are 1 answers

1
Topaco On

A decryption of the ciphertext shows that Zero padding was used in the encryption, see e.g. here with CyberChef.

NodeJS, in contrast, uses PKCS#7 padding by default; Zero padding is not supported. For decryption with NodeJS, the standard PKCS#7 padding must therefore be disabled. If an unpadding has to be performed, the padding bytes must be removed manually.

In addition, the key must be ASCII/UTF-8 encoded and not hex decoded (as already noted in the comment).

Possible fix:

...
const decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from(key, 'utf8'), Buffer.alloc(0)); // Fix 1: ASCII/UTF8 encode key
decipher.setAutoPadding(false);                                                                     // Fix 2: disable PKCS#7 default padding
...
return plaintext.replace(/\x00+$/g, '');                                                            // Fix 3: unpad (if required) 
...

Note that Zero padding is unreliable in contrast to PKCS#7 padding, i.e. it is generally not possible to distinguish 0x00 padding bytes from regular 0x00 bytes at the end.