Block Cipher Padding - PKCS#7
Table of Contents
What is Block Cipher Padding?
In a cryptography context, padding involves appending bytes to the end of a message to align it with a block cipher’s block size, 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 is a subset of PKCS#7 designed for 8-byte block ciphers (e.g., DES). PKCS#7 extends this to support block sizes up to 255 bytes.
The important rules for PKCS#7 are as follows:
- 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.
- 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.
- 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.
Security Considerations
Important Security Note: PKCS#7 padding is deterministic, which means that identical plaintext blocks will always produce identical padded results. This can leak information about the plaintext when used with insecure modes of operation like ECB. To use PKCS#7 securely:
- Always use it with a secure block cipher mode of operation:
- CBC mode with a random IV
- Authenticated modes like GCM or CCM
- Never use ECB mode in production
- Implement proper padding oracle attack mitigations
- Use constant-time comparisons when validating padding during decryption
Testing Examples
Here are some concrete examples of PKCS#7 padding with block size 8:
Input: "HELLO" (5 bytes)
Padded: "HELLO\x03\x03\x03" (8 bytes)
Input: "HELLOHELLO" (10 bytes)
Padded: "HELLOHELLO\x06\x06\x06\x06\x06\x06" (16 bytes)
Input: "12345678" (8 bytes, full block)
Padded: "12345678\x08\x08\x08\x08\x08\x08\x08\x08" (16 bytes)
Input: "" (empty string)
Padded: "\x08\x08\x08\x08\x08\x08\x08\x08" (8 bytes)
Practical Implementation
The C implementation provided is correct, but here’s a safer version with input validation:
uint8_t *pad(uint8_t *input, uint64_t input_len, uint8_t block_sz, uint64_t *padded_size) {
// Input validation
if (input == NULL || padded_size == NULL || block_sz == 0 || block_sz > 255) {
return NULL;
}
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;
// Check for integer overflow
if (final_size < input_len) {
return NULL;
}
uint8_t *padded_result = (uint8_t*) malloc(final_size);
if (padded_result == NULL) {
return NULL;
}
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;
}
The unpadding function should include validation to prevent padding oracle attacks:
uint8_t *unpad(uint8_t *input, uint64_t input_len, uint8_t block_sz, uint64_t *unpadded_size) {
// Input validation
if (input == NULL || unpadded_size == NULL ||
input_len == 0 || input_len % block_sz != 0) {
return NULL;
}
uint8_t pad_value = input[input_len - 1];
// Validate padding value
if (pad_value == 0 || pad_value > block_sz || pad_value > input_len) {
return NULL;
}
// Verify all padding bytes are correct (constant-time comparison recommended)
for (uint8_t i = 0; i < pad_value; i++) {
if (input[input_len - 1 - i] != pad_value) {
return NULL;
}
}
uint64_t final_size = input_len - pad_value;
uint8_t *unpadded_result = (uint8_t*) malloc(final_size);
if (unpadded_result == NULL) {
return NULL;
}
memcpy(unpadded_result, input, final_size);
*unpadded_size = final_size;
return unpadded_result;
}
The Rust implementation can also be enhanced with proper error handling:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PaddingError {
#[error("Invalid block size")]
InvalidBlockSize,
#[error("Invalid padding")]
InvalidPadding,
}
fn pkcs7_pad(payload: &mut Vec<u8>, block_sz: u8) -> Result<(), PaddingError> {
if block_sz == 0 || block_sz > 255 {
return Err(PaddingError::InvalidBlockSize);
}
let padding_value = if payload.len() % (block_sz as usize) == 0 {
block_sz as usize
} else {
(block_sz as usize) - (payload.len() % (block_sz as usize))
};
payload.extend(vec![padding_value as u8; padding_value]);
Ok(())
}
fn pkcs7_unpad(payload: &mut Vec<u8>) -> Result<(), PaddingError> {
if payload.is_empty() {
return Err(PaddingError::InvalidPadding);
}
let padding_value = payload[payload.len() - 1] as usize;
if padding_value == 0 || padding_value > payload.len() {
return Err(PaddingError::InvalidPadding);
}
// Verify padding (constant-time comparison recommended in production)
for i in 1..=padding_value {
if payload[payload.len() - i] != padding_value as u8 {
return Err(PaddingError::InvalidPadding);
}
}
payload.truncate(payload.len() - padding_value);
Ok(())
}