PKCS#7 Padding Primer

Table of Contents

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.

Practical example

Implementing PKCS#7 padding is a straightforward process, with the key focus on calculating the precise padding value. The following C code exemplifies this logic, applicable to various programming languages:

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, the code checks whether the input length (message length) is a multiple of the block size. If so, a full block is reserved for padding. It is essential to note that, for a block size of 8, this translates to 8 bytes, each with a value of 8.

After that, the final padded size is calculated by adding the padding value to the message length. This size adjustment is then applied to the message by appending the padding at its conclusion. The specific implementation details may vary depending on the chosen programming language and data structures.

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

To reverse this process without additional memory allocations and processing time, one could read the padded message up to the end, excluding the padding bytes. The final index for exclusion can be calculated using (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;
}

Despite most cryptographic libraries already having paddings implemented, such as PKCS#7, a solid understanding of it is key to navigating diverse padding protocols used by different algorithms.

To show another PKCS#7 padding implementation, this time in Rust, consider the following Rust code:

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