mirror of
https://github.com/kristoferssolo/cipher-workshop.git
synced 2025-12-31 13:52:29 +00:00
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
171 lines
4.7 KiB
Rust
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);
|
|
}
|
|
}
|