Skip to content

Commit

Permalink
Add Module for Extracting PKCS#7 Padded Data (#7)
Browse files Browse the repository at this point in the history
* unpadding iter adapter

* slice unpadder

* continue UnpadByValue
  • Loading branch information
NCGThompson authored Apr 5, 2024
1 parent bc550ee commit f5d0a0e
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/decryption/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod tests;
pub mod unpad;

use openssl::symm;

Expand Down
105 changes: 105 additions & 0 deletions src/decryption/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,108 @@
#![cfg(test)]

use super::*;

mod test_get_padding_length {
use super::*;
use unpad::get_padding_length;

/// Tests various conditions related to the padding length, including valid and invalid scenarios.
#[test]
fn padding_length_variations() {
// Valid PKCS#7 padding
let mut valid_padding = vec![1, 2, 3, 4, 4, 4, 4];
assert_eq!(get_padding_length(&valid_padding), Some(4));

// Data with single byte padding
valid_padding = vec![1, 1];
assert_eq!(get_padding_length(&valid_padding), Some(1));

// Data consists only of padding bytes
valid_padding = vec![2, 2];
assert_eq!(get_padding_length(&valid_padding), Some(2));

// Larger padding to ensure it scales correctly
valid_padding = vec![0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8];
assert_eq!(get_padding_length(&valid_padding), Some(8));
}

/// Tests invalid padding scenarios where the padding is not according to PKCS#7 rules.
#[test]
fn invalid_padding_scenarios() {
// Data without explicit padding
let no_padding = vec![1, 2, 3, 4];
assert_eq!(get_padding_length(&no_padding), None);

// Last byte is 0, which is invalid in PKCS#7 padding
let padding_length_zero = vec![1, 2, 3, 4, 0];
assert_eq!(get_padding_length(&padding_length_zero), None);

// Padding bytes do not match the last byte's value
let mismatched_values = vec![1, 2, 3, 4, 3, 2];
assert_eq!(get_padding_length(&mismatched_values), None);

// Padding byte value suggests more padding than data, which is invalid
let longer_than_data = vec![5];
assert_eq!(get_padding_length(&longer_than_data), None);

// Empty data slice should return None
let empty_data: Vec<u8> = vec![];
assert_eq!(get_padding_length(&empty_data), None);
}
}

#[cfg(test)]
mod test_unpad_slice {
use super::*;
use unpad::unpad_slice;

#[test]
fn unpad_mixed() {
let padded_data = vec![1, 2, 3, 4, 4, 4, 4, 4];

let unpadded = unpad_slice(&padded_data, Some(4)).unwrap();
assert_eq!(unpadded, &[1, 2, 3, 4]);

// flexible block size
let unpadded = unpad_slice(&padded_data, None).unwrap();
assert_eq!(unpadded, &[1, 2, 3, 4]);

// incorrect block size
assert!(unpad_slice(&padded_data, Some(3)).is_none());
}

#[test]
fn unpad_no_padding_when_optional() {
let data_without_padding = vec![1, 2, 3, 4];
assert!(unpad_slice(&data_without_padding, None).is_none());
}

#[test]
fn unpad_invalid_padding() {
let invalid_padding = vec![1, 2, 3, 4, 5];
assert!(unpad_slice(&invalid_padding, Some(5)).is_none());
}

#[test]
fn unpad_empty_data() {
let empty_data = vec![];
assert!(unpad_slice(&empty_data, Some(8)).is_none());
}

#[test]
fn unpad_padding_longer_than_data() {
let invalid_data = vec![5];
assert!(unpad_slice(&invalid_data, Some(1)).is_none());
}

#[test]
fn unpad_big_block_size() {
let mut padded_data = vec![5; 255]; // 255 bytes in total, last 5 are padding

let unpadded = unpad_slice(&padded_data, Some(255)).unwrap();
assert_eq!(unpadded.len(), 250);

padded_data.push(5); // 256 bytes in total, last 5 are padding
assert!(unpad_slice(&padded_data, Some(256)).is_none());
}
}
175 changes: 175 additions & 0 deletions src/decryption/unpad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! ## Overview of PKCS#7 Padding
//!
//! PKCS#7 padding is applicable to data of any length and is used to ensure that the length of the
//! data is a multiple of a specific block size. The value of each padding byte is the total number
//! of padding bytes added. For instance, if the block size is 16 bytes and the data is 13 bytes long,
//! three bytes of padding will be added, each with the value `03`.
//!
//! For our purposes, the block size will always be 16 bytes (or 128 bits) because that is the
//! blocksize of AES-192.
use std::iter::{repeat, FusedIterator};

/// This function inspects the padding and,
/// if valid, returns a subslice of the original data without the padding.
///
/// # Parameters
///
/// * `padded`: The data slice that may contain PKCS#7 padding.
/// * `block_size`: An optional block size used for padding. If specified, the function checks whether
/// the padding is valid for this specific block size:
/// - If `block_size` is `Some(size)`, the function returns a subslice only if the input is correctly
/// padded according to PKCS#7 rules for the given block size. This includes ensuring that the `padded`
/// slice's length is a multiple of `block_size` and that the padding length does not exceed `block_size`.
/// - If `block_size` is `None`, the function is more lenient and only checks the padding's validity
/// according to PKCS#7 rules, without enforcing a specific block size. This can be useful when the
/// block size is unknown or variable.
///
/// # Returns
///
/// * `Some(&[u8])`: A subslice of the original data without PKCS#7 padding if the padding is valid.
/// * `None`: If the padding is invalid, the block size is not respected, or other conditions prevent
/// unpadded data from being safely returned. This includes scenarios where the padding length is zero,
/// exceeds the block size, or the padding bytes do not conform to PKCS#7 rules.
///
/// # Examples
///
/// ```
/// use libproj3::decryption::unpad::unpad_slice;
///
/// let padded_data = [1, 2, 3, 4, 4, 4, 4, 4];
/// let unpadded = unpad_slice(&padded_data, Some(8)).unwrap();
/// assert_eq!(unpadded, &[1, 2, 3, 4]);
///
/// let invalid_padding = [1, 2, 3, 4, 5];
/// assert!(unpad_slice(&invalid_padding, Some(8)).is_none());
/// ```
pub fn unpad_slice(padded: &[u8], block_size: Option<usize>) -> Option<&[u8]> {
let padding_len = get_padding_length(padded)?;

if let Some(b) = block_size {
if b > u8::MAX as _ || padded.len() % b != 0 || padding_len > b {
return None;
}
}

let (subslice, _) = padded.split_at(padded.len() - padding_len);
Some(subslice)
}

/// This iterator adapter is to allow unpadding of encryption output without keeping the
/// whole output in memory at once.
///
/// I have no idea if this works.
pub struct UnpadByValue<I, const B: usize>
where
I: ExactSizeIterator<Item = [u8; B]> + FusedIterator,
{
inner: I,
current_block: [u8; B],
blocks_left: usize,
index: usize,
}

impl<I, const B: usize> UnpadByValue<I, B>
where
I: ExactSizeIterator<Item = [u8; B]> + FusedIterator,
{
pub fn new(iter: I) -> Self {
let len = iter.len();
Self {
inner: iter,
current_block: [0; B],
blocks_left: len,
index: B,
}
}
}

impl<I, const B: usize> Iterator for UnpadByValue<I, B>
where
I: ExactSizeIterator<Item = [u8; B]> + FusedIterator,
{
type Item = u8;

fn next(&mut self) -> Option<Self::Item> {
if self.index >= B {
self.index = 0;
self.current_block = self.inner.next()?;
self.blocks_left -= 1;
debug_assert_eq!(self.blocks_left, self.inner.len());
}
if self.blocks_left == 0 {
if self.index >= B - get_padding_length(&self.current_block).unwrap() {
self.index = B;
return None;
}
}
let ret = self.current_block[self.index];
self.index += 1;
Some(ret)
}

fn size_hint(&self) -> (usize, Option<usize>) {
if self.blocks_left == 0 {
let padding_len = get_padding_length(&self.current_block).unwrap();
let len_left = (B - self.index).saturating_sub(padding_len);
(len_left, Some(len_left))
} else {
let lo = match (self.blocks_left - 1).checked_mul(B) {
Some(x) => x.checked_add(B - self.index),
None => None,
};
match lo {
Some(x) => (x, x.checked_add(B - 1)),
None => (usize::MAX, None),
}
}
}
}

pub trait UnpadByValueIterator<const B: usize>:
ExactSizeIterator<Item = [u8; B]> + FusedIterator + Sized
{
/// Creates a new iterator of bytes from a padded iterator of blocks.
fn unpad(self) -> UnpadByValue<Self, B> {
UnpadByValue::new(self)
}
}

impl<I, const B: usize> FusedIterator for UnpadByValue<I, B> where
I: ExactSizeIterator<Item = [u8; B]> + FusedIterator
{
}

/// Calculates the length of PKCS#7 padding in a given data slice.
///
/// # Arguments
///
/// * `in_slice` - A slice of bytes that potentially contains PKCS#7 padding.
///
/// # Returns
///
/// * `Some(usize)` - The length of the padding if the input slice is correctly padded
/// according to PKCS#7 rules. The padding length is determined based on the value
/// of the last byte in the slice, and the function verifies that all padding bytes
/// have the same value.
///
/// * `None` - If the input slice is not correctly padded according to PKCS#7 rules.
/// This includes cases where the last byte is 0 (indicating an invalid padding length),
/// or the padding bytes do not all have the same value as required by PKCS#7.
pub(super) fn get_padding_length(in_slice: &[u8]) -> Option<usize> {
let last = *in_slice.last()?;
println!("{}", last);
if last == 0
|| in_slice
.iter()
.rev()
.take(last as _)
.ne(repeat(&last).take(last as _))
{
return None;
}

Some(last as _)
}
1 change: 1 addition & 0 deletions src/posting/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![cfg(test)]

#[allow(unused_imports)]
use super::*;
1 change: 1 addition & 0 deletions src/scraping/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![cfg(test)]

#[allow(unused_imports)]
use super::*;

0 comments on commit f5d0a0e

Please sign in to comment.