I have created AES CBC ciphers via Node crypto w/ 16 byte keys - tinyAES in C++ decrypts the cipher correctly. Ciphers created in Node crypto AES CBC using 24 byte or 32 bytes are NOT correctly decrypted in tiny AES, the derived padding is incorrect and the padding value is > 16...
is this a known limitation of the tinyAES?
for context:
1 - I believe Node crypto is using OpenSSL.
2 - a Node crypto AES CBC round trip (within Node) using 16/24/32 byte keys works
3 - a tinyAES AES CBC round trip (within C++) using 16/24/32 byte keys works
4 - Node<->tinyAES 16 byte key AES CBC round trip works
5 - Node<->tinyAES 24|32 byte key AES CBC round trip does NOT work
example - AES CBC 192 bit:
std::string c_b64 = "6l9sPiHYKjK6LiIkJ7qcIg=="; // b64 cipher from Node crypto
std::string aek = "0.591.075170084239850551"; // AES key, 24 bytes
std::string iv = "0123456789abcdef"; // IV
tiny AES decrypting will NOT work but works in Node crypto
example - AES CBC 128bit:
std::string c_b64 = "0B/7mia9ITdfg4QG4AqL/w=="; // b64 cipher from Node crypto
std::string aek = "0.591.0751700842"; // AES key, 16 bytes
std::string iv = "0123456789abcdef"; // IV
tiny AES decrypting will work
is there any way to get a 192/256-bit Node/tinyAES round trip working?
update: this is my C++ test code:
#include "base64.h"
#include "aes.hpp"
#define AES192 1
// 192 bit / 24 byte data
std::string _aek = "0.591.075170084239850551"; // AES key
std::string c_b64 = "6l9sPiHYKjK6LiIkJ7qcIg=="; // cipher
std::string iv = "0123456789abcdef"; // IV
std::string cip_s = base64_decode(c_b64);
vector<uint8_t> c_buf(cip_s.begin(), cip_s.end());
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, reinterpret_cast<const uint8_t*>(_aek.data()), reinterpret_cast<const uint8_t*>(iv.data()));
// Decrypt using CBC mode
AES_CBC_decrypt_buffer(&ctx, c_buf.data(), c_buf.size());
// padding
size_t pad_v = c_buf[c_buf.size() - 1];
if (pad_v > AES_BLOCKLEN) {
std::cout << "invalid padding: " << pad_v << std::endl;
}
for (size_t i = c_buf.size() - pad_v; i < c_buf.size(); i++) {
if (c_buf[i] != pad_v) {
std::cout << "padding bytes invalid: " << std::endl;
}
}
// Calculate the actual data length
size_t rs_ln = c_buf.size() - pad_v;
// Resize the vector to remove padding
c_buf.resize(rs_ln);
// convert result text buffer to string
std::string rs_txt_dec = string(c_buf.begin(), c_buf.end());
std::cout << "AES dec: rs_txt_dec: " << rs_txt_dec << std::endl;
this outputs on my machine:
invalid padding: 158
Caught Exception: vector::_M_default_append
In the current implementation of the tinyAES library, several parameters are defined as macros in aes.h, namely the mutually exclusive constants AES256
, AES192
and AES128
. Depending on this parameter, further parameters are defined, namely AES_KEYLEN
and AES_keyExpSize
in aes.h and Nk
and Nr
in aes.c.
It is therefore not possible to specify the AES variant at runtime or to use different AES variants at the same time. If this is to be possible, some changes must be made.
The following is an example of such a customization (in C++, i.e. in my environment I have renamed aes.h, aes.c and test.c to aes.hpp, aes.cpp and test.cpp and the two .cpp files include the .hpp file).
In aes.hpp: the macros AES128
, AES192
and AES256
and the #if defined-#endif
block for the definition of AES_KEYLEN
and AES_keyExpSize
are deleted.
In aes.cpp: the #if defined-#endif
block for the definition of Nk
and Nr
is deleted.
In aes.cpp: instead of macros, functions are used to determine the required parameters from the keysize:
static int getNk(int KeyLength) {
return KeyLength / 32; // 256: 8, 192: 6, 128: 4
}
static int getNr(int KeyLength) {
return KeyLength / 32 + 6; // 256: 14, 192: 12, 128: 10
}
static int getKeyExpSize(int KeyLength) {
return (KeyLength / 8 + 28) * 4; // 256: 240, 192: 208, 128: 176
}
In aes.cpp: in KeyExpansion()
, Nk
and Nr
are determined at the beginning with:
int Nk = getNk(KeyLength);
int Nr = getNr(KeyLength);
The #if defined(AES256) && (AES256 == 1)
line and the associated #endif
line must be removed, the contained if
block must be adapted:
if (KeyLength == 256 && i % Nk == 4)
{
...
In aes.cpp: in Cipher()
and InvCipher()
, Nr
is determined at the beginning:
int Nr = getNr(KeyLength);
In aes.hpp: the declaration of the context AES_ctx
is changed as follows:
struct AES_ctx
{
uint8_t* RoundKey;
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
uint8_t Iv[AES_BLOCKLEN];
#endif
};
In aes.cpp: for the member RoundKey
, the required memory is allocated as the first instruction in AES_init_ctx()
and AES_init_ctx_iv()
:
ctx->RoundKey = new uint8_t[getKeyExpSize(KeyLength)];
In aes.cpp: to release the allocated memory, the new function AES_free_ctx()
is defined:
void AES_free_ctx(struct AES_ctx* ctx)
{
delete ctx->RoundKey;
}
Nk
, Nr
or AES_keyExpSize
, a further parameter, namely the key length in bits, is passed in the last position: int KeyLength
. These functions are in detail: KeyExpansion()
, AES_init_ctx()
, AES_init_ctx_iv()
, Cipher()
and InvCipher()
, e.g.:
static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key, int KeyLength)
{
...
AES_ECB_encrypt()
, AES_ECB_decrypt()
, AES_CBC_encrypt_buffer()
, AES_CBC_decrypt_buffer()
and AES_CTR_xcrypt_buffer()
.KeyLength
must be additionally passed.AES_init_ctx()
, AES_init_ctx_iv()
, AES_ctx_set_iv()
, AES_free_ctx()
, AES_ECB_encrypt()
, AES_ECB_decrypt()
, AES_CBC_encrypt_buffer()
, AES_CBC_decrypt_buffer()
and AES_CTR_xcrypt_buffer()
.The existing test.cpp is to be overwritten with the following code, which essentially contains the example code you posted, and which contains a decryption with AES-256, AES-192 and AES-128 as use case.
#include <string>
#include <vector>
#include <iostream>
#include "aes.hpp"
#define CBC 1
#define CTR 1
#define ECB 1
// your code
void decrypt(const std::string& key, const std::string& iv, const std::string& ct) {
std::vector<uint8_t> c_buf(ct.begin(), ct.end());
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, reinterpret_cast<const uint8_t*>(key.data()), reinterpret_cast<const uint8_t*>(iv.data()), key.size() * 8);
// Decrypt using CBC mode
AES_CBC_decrypt_buffer(&ctx, c_buf.data(), c_buf.size(), key.size() * 8);
// Clean up
AES_free_ctx(&ctx);
// Padding
size_t pad_v = c_buf[c_buf.size() - 1];
if (pad_v > AES_BLOCKLEN) {
std::cout << "invalid padding: " << pad_v << std::endl;
}
for (size_t i = c_buf.size() - pad_v; i < c_buf.size(); i++) {
if (c_buf[i] != pad_v) {
std::cout << "padding bytes invalid: " << std::endl;
}
}
// Calculate the actual data length
size_t rs_ln = c_buf.size() - pad_v;
// Resize the vector to remove padding
c_buf.resize(rs_ln);
// Convert result text buffer to string
std::string rs_txt_dec = std::string(c_buf.begin(), c_buf.end());
std::cout << "AES dec: rs_txt_dec: " << rs_txt_dec << std::endl;
}
int main(void) {
// AES-256
decrypt("0.591.075170084239850551uvwxabcd", "0123456789abcdef", { (char)0x90, 0x38, 0x12, 0x74, (char)0x87, 0x3f, (char)0xfb, 0x48, (char)0xe5, 0x4e, (char)0xd5, (char)0xd1, (char)0x88, (char)0x8b, 0x4f, (char)0xb3 });
// AES-192
decrypt("0.591.075170084239850551", "0123456789abcdef", { (char)0xea, 0x5f, 0x6c, 0x3e, 0x21, (char)0xd8, 0x2a, 0x32, (char)0xba, 0x2e, 0x22, 0x24, 0x27, (char)0xba, (char)0x9c, 0x22 });
// AES-128
decrypt("0.591.0751700842", "0123456789abcdef", { (char)0xd0, 0x1f, (char)0xfb, (char)0x9a, 0x26, (char)0xbd, 0x21, 0x37, 0x5f, (char)0x83, (char)0x84, 0x06, (char)0xe0, 0x0a, (char)0x8b, (char)0xff });
return 0;
}
When the code is executed, the three decryptions are performed successfully.