skip to content

PKCS#7 Padding Primer

Primer on how PKCS#7 works with code examples

Padding primer

In a cryptography context, padding is just the practice of adding some bytes to the beginning, middle, or end of a certain message, before encryption. This is widely used in block ciphers, which will split your message into multiple blocks and encrypt them.

In other contexts, it’s still similar, it’s all about adding something, somewhere, to make the resultant size predictable, you can even think about a pillow cushion that you fill to a certain point and the analogy will still work.

Scenario

For the sake of argument, let’s say we want to encrypt a message of length 16 bytes, using a block cipher in ECB (Electronic Code Book) mode. It looks easy enough, if we assume each block to be 8 bytes, we need to process 2 blocks, so far so good.

Now, what happens if the message has 17 bytes? Well, we can no longer split it into two blocks of 8, because there would be 1 missing byte. If you’re thinking padding, you’re on the right track. To encrypt this in a way that also allows for decryption, that is, when you decrypt you get the message without any additional padding, we’ll need some padding logic to accommodate this.

| If you don’t know what I meant with ECB, you can read this

Solution (PKCS#7)

There are multiple approaches to solving our padding problem, but in this post, we’ll focus on PKCS#7, which is one of the standard ones.

PKCS stands for “Public Key Cryptography Standards”. In our case, we’re focusing on a padding standard with PKCS#7. I think it’s best to read the wiki on this, as there are a lot of different versions out there.

There’s also PKCS#5 which assumes blocks of size 8, whereas #7 works with other sizes. In reality, they are the same.

The important rules for PKCS#7 are as follows:

  1. We will always have padding, even if the message to be padded is a multiple of our block size, e.g. a message of 8 bytes, with block size 8, will end up padded to 16 bytes. Alternatively, a 16-byte message will end up with 24 bytes and a 15-byte message will get padded to 16 bytes. So, at least you will always have 1 or more bytes of padding.
  2. All padded bytes will contain the same value, equal to the number of bytes of padding, e.g. 8 bytes of padding means the padded message will have 8 bytes with value 8.
  3. When unpadding, all we do is look at the last byte, get its value, and remove that number of bytes from the end of the message, e.g. you see the value 6 as the last byte, you remove the last 6 bytes.

These are the rules, and it’s pretty much it.

Coding time!

The coding for this is quite easy, the most important part is to calculate the padding value. We’ll do this in C, but you can use this logic in any programming language you want.

uint8_t pad_value = 0;
if (input_len % block_sz == 0) {
	pad_value = block_sz;
}
else {
	pad_value = block_sz - (input_len % block_sz);
}

Basically, what we’re doing is checking if the input length (message length) is a multiple of our block size. If it is, we’ll have a full block just for padding, remember, for block size 8, this would mean 8 bytes with value 8.

After that, we calculate the final padded size, by adding this padding value to our message length. When this is done, we add the padding to the end of our message. Of course, this will depend on the language and data structures you’re using.

Below’s a full implementation of a padding function, that takes an input, an input length, a block size, and a pointer to a padded size that can be used later, to read the padded message.

uint8_t *pad(uint8_t *input, uint64_t input_len, uint8_t block_sz, uint64_t *padded_size) {

	uint8_t pad_value = 0;
	if (input_len % block_sz == 0) {
		pad_value = block_sz;
	}
	else {
		pad_value = block_sz - (input_len % block_sz);
	}

	uint64_t final_size = input_len + pad_value;

	uint8_t *padded_result = (uint8_t*) malloc(final_size);
	memcpy(padded_result, input, input_len);
	for (uint8_t i = 0; i < pad_value; i++) {
		padded_result[input_len + i] = pad_value;
	}

	*padded_size = final_size;
	return padded_result;
}

Now, all we need is a way to reverse this process. If you don’t want extra memory allocations and processing time from copying values, technically, you could just read the padded message all the way to the end, excluding those padding bytes, by calculating the final index with (total size - last byte value) - 1. If you need an actual new “instance” of the unpadded message, you can do something like:

uint8_t *unpad(uint8_t *input, uint64_t input_len, uint8_t block_sz, uint64_t *padded_size) {
	uint8_t pad_value = input[input_len - 1];
	uint64_t final_size = input_len - pad_value;

	uint8_t *unpadded_result = (uint8_t*) malloc(final_size);
	memcpy(unpadded_result, input, final_size);

	*padded_size = final_size;
	return unpadded_result;
}

This is it, PKCS#7 is super easy to implement and it’s very useful. You’ll see this all the time if you’re studying cryptography.

To show you another PKCS#7 padding implementation, here’s Rust:

fn pkcs7_pad(payload: &mut Vec<u8>, block_sz: u8) {
    let padding_value: usize;
    let payload_len = payload.len();

    if payload_len % block_sz == 0 {
        padding_value = block_sz
    }
    else {
        padding_value = block_sz - (payload_len % block_sz);
    }

    let mut pad = vec![padding_value as u8; padding_value];
    payload.append(&mut pad);
}

fn pkcs7_unpad(payload: &mut Vec<u8>) {
    let padding_value = payload[payload.len() - 1];
    payload.truncate(payload.len() - padding_value as usize);
}

Thanks for reading!