Tiny AES in CBC mode with PKCS7 padding written in C

An example on how to use Tiny AES in CBC mode with PKCS7 padding written in C

Date and Time of last update Fri 16 Oct 2020
  


The inspiration of this article comes from the fact that I needed some very efficient way to encrypt a sensitive string before passing it around. I was currently working in C for this part of the project so writing this part in C was awesome as it can be considered efficient on its own. So, I started looking for my options and then it is when I saw this marvelous tiny AES implementation made from kokke. It was undoubtedly what I was looking for, as it supports key lengths of 128/192/256 bits and the CBC mode. The only thing missing was the pkcs7 padding but we will see later how this was dealt with.

tldr;

You can find the code with links with the rest of the files required here!


Some things to have in mind for the code:

  1. The IV should be of length 16 bytes.
  2. The key and the string to be encrypted should multiples of 16 bytes. (this is where the padding comes in)
  3. I am terrible at giving names!!!

Lets start with the basics, the two files needed for this to work are the aes.c and the aes.h. The following snippet shows the initial part of the code:

#define CBC 1
#include "aes.h"

//Initialization Vector
uint8_t iv[]  = { 0x75, 0x52, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x21, 0x21 };
                             
char* report = "my super secret thing that needs to remain that way!";
char* key = "thisIstheKey";

The first line #define CBC 1, is mandatory to define the mode we want to use. The rest of the lines are pretty self explanatory, we define the IV to be 16 bytes and then we define our sensitive report to be encrypted with a key. Do note here that neither the report nor the key are multiples of 16 bytes while this is something required due to the way the algorithm works.

int dlen = strlen(report);          # the length of the report
int klen = strlen(key);             # the length of the key

int dlenu = dlen;
if (dlen % 16) {
    dlenu += 16 - (dlen % 16);      # make the length multiple of 16 bytes
}

int klenu = klen;
if (klen % 16) {
    klenu += 16 - (klen % 16);      # make the length multiple of 16 bytes
}

In the code above the dlen and the klen hold the length of the report and the key respectively. Now due to we want the report and the key to be multiples of 16 bytes we need to figure out how long both of them will need to be based on their current length. The formula to do that is pretty simple, get the modulo of the length with 16 and then subtract this from 16. The result is how many more bytes we should add to the original length. So the dlenu and the klenu in the code above hold the updated length for the report and they key, which now are multiples of 16 bytes.

// Make the uint8_t arrays
uint8_t hexarray[dlenu];
uint8_t kexarray[klenu];

// Initialize them with zeros
memset( hexarray, 0, dlenu );
memset( kexarray, 0, klenu );


// Fill the uint8_t arrays
for (int i=0;i<dlen;i++) {
    hexarray[i] = (uint8_t)report[i];
}
for (int i=0;i<klen;i++) {
    kexarray[i] = (uint8_t)key[i];
}

Since the tiny AES takes as parameters the string to be encrypted and the key as char arrays, we need to convert them from strings and pad them accordingly. The snippet above takes care of:

  • first creating two char arrays of length according to the updated length we saw earlier (being multiples of 16 bytes)
  • second initialize both char arrays to zeros
  • third fill the char arrays with the two strings (report/key)

Now at this point we have two char arrays, one holding the data to be encrypted(report) and the other the key. Both of the char arrays are padded with zeros as we initialized them with zeros!

The goal now is to have some proper padding, and for this we chose to go for the pkcs7 padding. Before continuing to implement that, lets check if it is already out there --- and it is -- a fork of the original project found here includes the pkcs7 padding we are looking for. The two files we want are the pkcs7_padding.c and the pkcs7_padding.h.

int reportPad = pkcs7_padding_pad_buffer( hexarray, dlen, sizeof(hexarray), 16 );
int keyPad = pkcs7_padding_pad_buffer( kexarray, klen, sizeof(kexarray), 16 );

The function we want to use first is the pkcs7_padding_pad_buffer which takes as input the report (char array) along with the original length of the report, it pads the char array and it returns the number of paddings it added.

Now we are completely ready to start the encryption process as the report and the key are padded properly and have a proper size!


The next step is to initialize the AES.

//start the encryption
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, kexarray, iv);
    
// encrypt
AES_CBC_encrypt_buffer(&ctx, hexarray, dlenu);

Two are the most important things to note here, the first is the AES_init_ctx_iv which initializes AES with the key and the IV and the second one is the actual encryption process with the AES_CBC_encrypt_buffer function, which takes the report char array as parameter and it is where it stores the encrypted output as well.


Before finalizing this and presenting the full code lets see also the decryption process.

// reset the iv !! important to work!
AES_ctx_set_iv(&ctx,iv);

// start decryption
AES_CBC_decrypt_buffer(&ctx, hexarray, dlenu);

size_t actualDataLength = pkcs7_padding_data_length( hexarray, dlenu, 16);

printf("the decrypted STRING = ");
for (i=0; i<actualDataLength;i++){
    printf("%02x",hexarray[i]);
}
printf("\n");

Three main things to note on the snippet above:

  1. The function AES_ctx_set_iv resets the IV and takes as parameter the ctx which already has the key.
  2. The function AES_CBC_decrypt_buffer which takes the encrypted string as a char array and returns in that char array the decrypted string.
  3. The funtion pkcs7_padding_data_length which returns the actual length of the string besides the padding, this way we know the useful data in the decrypted string which contains the padding.

As it was noted by Sarah, the function pkcs7_padding_data_length has a small bug for the cases where the report string is exactly N times the 16 bytes. This is fixed by changed the in line 41 of the file pkcs7_padding.c the returned value to be the buffer_size. Please do note that this is a valid solution given that we will provide as a report string printable characters which will always be higher in value than 16 which is our modulo. You can find the updated file here.


Now it is time to see the final result of the code.

A tree of the files in use:

.
├── aes.c
├── aes.h
├── Makefile
├── pkcs7_padding.c
├── pkcs7_padding.h
└── test.c


Following you will see the contents of the test.c file.

#include <stdio.h>
#include <string.h>
#include <stdint.h>

#define CBC 1

#include "aes.h"
#include "pkcs7_padding.c"

static void test_encrypt_cbc(void);


int main(void)
{
    int exit=0;

#if defined(AES256)
    printf("\nTesting AES256\n\n");
#elif defined(AES192)
    printf("\nTesting AES192\n\n");
#elif defined(AES128)
    printf("\nTesting AES128\n\n");
#else
    printf("You need to specify a symbol between AES128, AES192 or AES256. Exiting");
    return 0;
#endif

    test_encrypt_cbc();

    return exit;
}


static void test_encrypt_cbc(void)
{
    //Initialization Vector
    uint8_t iv[]  = { 0x75, 0x52, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x21, 0x21 };

    uint8_t i;                               
    char* report = "my super secret thing that needs to remain that way!";
    char* key = "thisIstheKey";
    int dlen = strlen(report);
    int klen = strlen(key);
    
    printf("THE PLAIN TEXT STRING = ");
    for (i=0; i<dlen;i++){
        printf("%c", report[i]);
    }
    printf("\n");
    
   
    //Proper Length of report
    int dlenu = dlen;
    if (dlen % 16) {
        dlenu += 16 - (dlen % 16);
        printf("The original length of the STRING = %d and the length of the padded STRING = %d\n", dlen, dlenu);
    }
    
    //Proper length of key
    int klenu = klen;
    if (klen % 16) {
        klenu += 16 - (klen % 16);
        printf("The original length of the KEY = %d and the length of the padded KEY = %d\n", klen, klenu);
    }
    
    // Make the uint8_t arrays
    uint8_t hexarray[dlenu];
    uint8_t kexarray[klenu];
    
    // Initialize them with zeros
    memset( hexarray, 0, dlenu );
    memset( kexarray, 0, klenu );
    
    // Fill the uint8_t arrays
    for (int i=0;i<dlen;i++) {
        hexarray[i] = (uint8_t)report[i];
    }
    for (int i=0;i<klen;i++) {
        kexarray[i] = (uint8_t)key[i];
    }                           
  
    int reportPad = pkcs7_padding_pad_buffer( hexarray, dlen, sizeof(hexarray), 16 );
    int keyPad = pkcs7_padding_pad_buffer( kexarray, klen, sizeof(kexarray), 16 );
    
    printf("The padded STRING in hex is = ");
    for (i=0; i<dlenu;i++){
        printf("%02x",hexarray[i]);
    }
    printf("\n");
    
    printf("The padded key in hex is = ");
    for (i=0; i<klenu;i++){
        printf("%02x",kexarray[i]);
    }
    printf("\n");
        
    // In case you want to check if the padding is valid
    int valid = pkcs7_padding_valid( hexarray, dlen, sizeof(hexarray), 16 );
    int valid2 = pkcs7_padding_valid( kexarray, klen, sizeof(kexarray), 16 );
    printf("Is the pkcs7 padding valid  report = %d  |  key = %d\n", valid, valid2);
    
    //start the encryption
    struct AES_ctx ctx;
    AES_init_ctx_iv(&ctx, kexarray, iv);
    
    // encrypt
    AES_CBC_encrypt_buffer(&ctx, hexarray, dlenu);
    printf("the encrypted STRING = ");
    for (i=0; i<dlenu;i++){
        printf("%02x",hexarray[i]);
    }
    printf("\n");
        
    // reset the iv !! important to work!
    AES_ctx_set_iv(&ctx,iv);
    
    // start decryption
    AES_CBC_decrypt_buffer(&ctx, hexarray, dlenu);
    
    size_t actualDataLength = pkcs7_padding_data_length( hexarray, dlenu, 16);
    printf("The actual data length (without the padding) = %ld\n", actualDataLength);
    
    printf("the decrypted STRING in hex = ");
    for (i=0; i<actualDataLength;i++){
        printf("%02x",hexarray[i]);
    }
    printf("\n");
}


An example of output is presented below

Testing AES128

THE PLAIN TEXT STRING = my super secret thing that needs to remain that way!
The original length of the STRING = 52 and the length of the padded STRING = 64
The original length of the KEY = 12 and the length of the padded KEY = 16
The padded STRING in hex is = 6d7920737570657220736563726574207468696e672074686174206e6565647320746f2072656d61696e207468617420776179210c0c0c0c0c0c0c0c0c0c0c0c
The padded key in hex is = 7468697349737468654b657904040404
Is the pkcs7 padding valid  report = 1  |  key = 1
the encrypted STRING = cdc4244c4828ed73e78c75a5db94d577d1f69472140204d7a6ce89f6f1d42d2962031470e3dd3d4b99a735504b4b9d8a277ba6bda54a06c2291380fae26f0fd0
The actual data length (without the padding) = 52
the decrypted STRING in hex = 6d7920737570657220736563726574207468696e672074686174206e6565647320746f2072656d61696e20746861742077617921


The makefile used to compile this was the one from the original github project found here. Simply run make and then run ./test.elf.

In case you would like to have a different key length you can do that by uncommenting the corresponding line in the aes.h file.

Conclusion

The result of this implementation of AES appeared to be super efficient and works like a charm. In the project I have used it, it serves its purpose perfectly that is why I thought it is something I should share.

Disclaimer: All credits about the implementation of the AES or the PKCS7 padding go to their original authors mentioned in the article. The cryptographic security provided by the above must be validated in all cases.


Updated : to fix a bug on pkcs7_padding.c reported by Sarah - Thank you Sarah!