diff --git a/src/decryption/mod.rs b/src/decryption/mod.rs index 1ab2318..bfcefae 100644 --- a/src/decryption/mod.rs +++ b/src/decryption/mod.rs @@ -1,4 +1,5 @@ mod tests; +pub mod unpad; use openssl::symm; diff --git a/src/decryption/tests.rs b/src/decryption/tests.rs index c425b42..352236e 100644 --- a/src/decryption/tests.rs +++ b/src/decryption/tests.rs @@ -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 = 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()); + } +} diff --git a/src/decryption/unpad.rs b/src/decryption/unpad.rs new file mode 100644 index 0000000..197d683 --- /dev/null +++ b/src/decryption/unpad.rs @@ -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) -> 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 +where + I: ExactSizeIterator + FusedIterator, +{ + inner: I, + current_block: [u8; B], + blocks_left: usize, + index: usize, +} + +impl UnpadByValue +where + I: ExactSizeIterator + FusedIterator, +{ + pub fn new(iter: I) -> Self { + let len = iter.len(); + Self { + inner: iter, + current_block: [0; B], + blocks_left: len, + index: B, + } + } +} + +impl Iterator for UnpadByValue +where + I: ExactSizeIterator + FusedIterator, +{ + type Item = u8; + + fn next(&mut self) -> Option { + 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) { + 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: + ExactSizeIterator + FusedIterator + Sized +{ + /// Creates a new iterator of bytes from a padded iterator of blocks. + fn unpad(self) -> UnpadByValue { + UnpadByValue::new(self) + } +} + +impl FusedIterator for UnpadByValue where + I: ExactSizeIterator + 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 { + 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 _) +} diff --git a/src/posting/tests.rs b/src/posting/tests.rs index c425b42..fb1b980 100644 --- a/src/posting/tests.rs +++ b/src/posting/tests.rs @@ -1,3 +1,4 @@ #![cfg(test)] +#[allow(unused_imports)] use super::*; diff --git a/src/scraping/tests.rs b/src/scraping/tests.rs index c425b42..fb1b980 100644 --- a/src/scraping/tests.rs +++ b/src/scraping/tests.rs @@ -1,3 +1,4 @@ #![cfg(test)] +#[allow(unused_imports)] use super::*;