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.
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;
}

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:
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.