cipher-workshop/cipher-core/src/padding.rs
Kristofers Solo dd691cfa18
feat(cipher-core): add PKCS#7 padding support
Add pkcs7_pad and pkcs7_unpad functions for block cipher modes:
    - Pad data to block size multiples with N bytes of value N
    - Validate and remove padding on decryption
    - Add InvalidPadding variant to CipherError
2025-12-31 00:48:49 +02:00

171 lines
4.7 KiB
Rust

//! PKCS#7 padding for block ciphers.
//!
//! PKCS#7 pads data to a multiple of the block size by appending N bytes
//! of value N, where N is the number of padding bytes needed.
//!
//! # Example
//!
//! For a 16-byte block size:
//! - 15 bytes of data → add 1 byte of value `0x01`
//! - 14 bytes of data → add 2 bytes of value `0x02`
//! - 16 bytes of data → add 16 bytes of value `0x10` (full padding block)
use crate::CipherError;
/// Applies PKCS#7 padding to input data.
///
/// Pads the data to a multiple of `block_size` by appending N bytes of value N.
/// If data is already aligned, a full block of padding is added.
///
/// # Panics
///
/// Panics if `block_size` is 0 or greater than 255.
#[must_use]
pub fn pkcs7_pad(data: &[u8], block_size: usize) -> Vec<u8> {
assert!(
block_size > 0 && block_size <= 255,
"block_size must be 1-255"
);
let padding_len = block_size - (data.len() % block_size);
let mut padded = Vec::with_capacity(data.len() + padding_len);
padded.extend_from_slice(data);
#[allow(clippy::cast_possible_truncation)]
let pad_byte = padding_len as u8;
padded.resize(data.len() + padding_len, pad_byte);
padded
}
/// Removes PKCS#7 padding from decrypted data.
///
/// Validates the padding and returns the unpadded data slice.
///
/// # Errors
///
/// Returns `CipherError::InvalidPadding` if:
/// - Data is empty
/// - Padding byte value is 0 or exceeds block size
/// - Padding bytes are inconsistent
/// - There aren't enough bytes for the claimed padding
pub fn pkcs7_unpad(data: &[u8], block_size: usize) -> Result<&[u8], CipherError> {
if data.is_empty() {
return Err(CipherError::InvalidPadding("data is empty".into()));
}
let last_byte = data[data.len() - 1];
let padding_len = last_byte as usize;
// Validate padding length
if padding_len == 0 || padding_len > block_size {
return Err(CipherError::InvalidPadding(format!(
"invalid padding byte: 0x{last_byte:02X}"
)));
}
if padding_len > data.len() {
return Err(CipherError::InvalidPadding(format!(
"padding length {padding_len} exceeds data length {}",
data.len()
)));
}
// Verify all padding bytes are consistent
let padding_start = data.len() - padding_len;
for (i, &byte) in data[padding_start..].iter().enumerate() {
if byte != last_byte {
return Err(CipherError::InvalidPadding(format!(
"inconsistent padding at byte {i}: expected 0x{last_byte:02X}, got 0x{byte:02X}"
)));
}
}
Ok(&data[..padding_start])
}
#[cfg(test)]
mod tests {
use super::*;
use claims::{assert_err, assert_ok};
#[test]
fn pad_empty() {
let padded = pkcs7_pad(&[], 16);
assert_eq!(padded.len(), 16);
assert!(padded.iter().all(|&b| b == 16));
}
#[test]
fn pad_one_byte_short() {
let data = [0u8; 15];
let padded = pkcs7_pad(&data, 16);
assert_eq!(padded.len(), 16);
assert_eq!(padded[15], 1);
}
#[test]
fn pad_aligned_adds_full_block() {
let data = [0u8; 16];
let padded = pkcs7_pad(&data, 16);
assert_eq!(padded.len(), 32);
assert!(padded[16..].iter().all(|&b| b == 16));
}
#[test]
fn pad_partial_block() {
let data = b"hello";
let padded = pkcs7_pad(data, 16);
assert_eq!(padded.len(), 16);
assert_eq!(&padded[..5], b"hello");
// 11 bytes of padding with value 0x0B
assert!(padded[5..].iter().all(|&b| b == 11));
}
#[test]
fn unpad_valid() {
let data = [0u8, 0u8, 0u8, 3u8, 3u8, 3u8];
let unpadded = assert_ok!(pkcs7_unpad(&data, 16));
assert_eq!(unpadded, &[0u8, 0u8, 0u8]);
}
#[test]
fn unpad_full_block_padding() {
let mut data = vec![0u8; 16];
data.extend([16u8; 16]);
let unpadded = assert_ok!(pkcs7_unpad(&data, 16));
assert_eq!(unpadded.len(), 16);
}
#[test]
fn unpad_empty_fails() {
assert_err!(pkcs7_unpad(&[], 16));
}
#[test]
fn unpad_zero_padding_fails() {
let data = [1u8, 2u8, 0u8];
assert_err!(pkcs7_unpad(&data, 16));
}
#[test]
fn unpad_padding_exceeds_block_size_fails() {
let data = [1u8, 2u8, 17u8];
assert_err!(pkcs7_unpad(&data, 16));
}
#[test]
fn unpad_inconsistent_padding_fails() {
let data = [1u8, 2u8, 3u8, 2u8];
assert_err!(pkcs7_unpad(&data, 16));
}
#[test]
fn roundtrip() {
let original = b"The quick brown fox";
let padded = pkcs7_pad(original, 16);
let unpadded = assert_ok!(pkcs7_unpad(&padded, 16));
assert_eq!(unpadded, original);
}
}