diff --git a/submissions/challenge1/filip_krawczyk/.gitignore b/submissions/challenge1/filip_krawczyk/.gitignore new file mode 100644 index 0000000..e33bc03 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/.gitignore @@ -0,0 +1,2 @@ +**/.DS_Store +target/ diff --git a/submissions/challenge1/filip_krawczyk/Cargo.lock b/submissions/challenge1/filip_krawczyk/Cargo.lock new file mode 100644 index 0000000..4b93b4f --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/Cargo.lock @@ -0,0 +1,249 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "block_ciphers" +version = "0.1.0" +dependencies = [ + "aes", + "generic-array", + "rand", + "thiserror", + "urlencoding", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/submissions/challenge1/filip_krawczyk/Cargo.toml b/submissions/challenge1/filip_krawczyk/Cargo.toml new file mode 100644 index 0000000..6f19a36 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "block_ciphers" +version = "0.1.0" +edition = "2024" + +[dependencies] +aes = "0.8.4" +generic-array = "0.14.7" +rand = "0.9.2" +thiserror = "2.0.17" +urlencoding = "2.1.3" diff --git a/submissions/challenge1/filip_krawczyk/README.md b/submissions/challenge1/filip_krawczyk/README.md new file mode 100644 index 0000000..a77fd63 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/README.md @@ -0,0 +1,88 @@ +### Selected challenges + +I chose the following challenges: + +- [9. Implement PKCS#7 padding](https://cryptopals.com/sets/2/challenges/9) (easy) +- [10. CBC mode encryption](https://cryptopals.com/sets/2/challenges/10) (easy) +- [16. CBC bitflipping attack](https://cryptopals.com/sets/2/challenges/16) (medium) +- [17. CBC padding oracle attack](https://cryptopals.com/sets/3/challenges/17) (hard) + +## Solutions Overview + +1. **PKCS#7 Padding**: Implementation of the PKCS#7 padding scheme for block ciphers, including both padding and unpadding operations with validation. + +2. **CBC Mode**: Implementation of Cipher Block Chaining (CBC) mode for AES-128, including encryption and decryption with initialization vectors. + +3. **CBC Bit-Flipping Attack**: Demonstration of how to manipulate CBC-encrypted cookies by flipping bits in the IV to inject malicious content (e.g., `admin=true`). + +4. **CBC Padding Oracle Attack**: Implementation of a padding oracle attack that decrypts CBC-encrypted messages without knowing the key, by exploiting padding validation errors. + +All details are in the code and comments. + +## How to Run + +### Prerequisites + +This project requires Rust and Cargo. Install them from [rustup.rs](https://rustup.rs/) if needed. + +### Running the Bit-Flipping Attack + +Run the CBC bit-flipping attack demonstration: + +```bash +cargo run --bin bit_flipping +``` + +### Running the Padding Oracle Attack + +Run the CBC padding oracle attack: + +```bash +cargo run --bin padding_oracle +``` + +### Running tests for PKCS#7 and CBC + +```bash +cargo test +``` + +## File Structure + +### Core Library Files + +- **[`src/aes.rs`](./src/aes.rs)**: AES-128 block cipher operations. Provides functions to encrypt and decrypt individual 16-byte blocks using AES-128. It is a thin wrapper around the `aes` crate with better interface. + +- **[`src/blocks.rs`](./src/blocks.rs)**: Generic block data structure (`Blocks`) that ensures data length is always a multiple of the block size. Provides utilities for block manipulation and iteration. + +- **[`src/pkcs7.rs`](./src/pkcs7.rs)**: PKCS#7 padding implementation. Contains: + + - [`pad_mut()`](./src/pkcs7.rs#L7) / [`pad()`](./src/pkcs7.rs#L13): Adds PKCS#7 padding to data + - [`unpad_mut()`](./src/pkcs7.rs#L20) / [`unpad()`](./src/pkcs7.rs#L46): Removes and validates PKCS#7 padding + - Many [tests](./src/pkcs7.rs#L59) for standard inputs and edge cases + +- **[`src/cbc.rs`](./src/cbc.rs)**: CBC mode implementation for AES-128. Provides: + - [`encrypt()`](./src/cbc.rs#L37): Encrypts plaintext blocks using CBC mode + - [`decrypt()`](./src/cbc.rs#L52): Decrypts ciphertext blocks using CBC mode + - Many [tests](./src/cbc.rs#L73) for various number of blocks + +### Bit-Flipping Attack + +- **[`src/bit_flipping/cookie.rs`](./src/bit_flipping/cookie.rs)**: Cookie encoding and parsing utilities. Implements URL encoding/decoding for cookie values and parsing of cookie strings into key-value pairs. + +- **[`src/bit_flipping/server.rs`](./src/bit_flipping/server.rs)**: Server simulation that: + + - [`encrypt()`](./src/bit_flipping/server.rs#L26): Encrypts user cookies using CBC mode + - [`is_admin()`](./src/bit_flipping/server.rs#L33): Validates cookies and checks for admin privileges + +- **[`src/bin/bit_flipping.rs`](./src/bin/bit_flipping.rs)**: Executable that demonstrates the CBC bit-flipping attack. Shows how to manipulate the IV to inject `admin=true` into an encrypted cookie. + +### Padding Oracle Attack + +- **[`src/padding_oracle/oracle.rs`](./src/padding_oracle/oracle.rs)**: Padding oracle implementation that: + + - [`get_encrypted_message()`](./src/padding_oracle/oracle.rs#L19): Encrypts a secret message using CBC mode + - [`is_padding_valid()`](./src/padding_oracle/oracle.rs#L35): Provides an oracle function that reveals whether padding is valid + - Simulates a vulnerable server that leaks padding validation errors + +- **[`src/bin/padding_oracle.rs`](./src/bin/padding_oracle.rs)**: Executable that performs the padding oracle attack. Decrypts the secret message by exploiting the padding oracle, byte by byte. diff --git a/submissions/challenge1/filip_krawczyk/src/aes.rs b/submissions/challenge1/filip_krawczyk/src/aes.rs new file mode 100644 index 0000000..6d68bed --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/aes.rs @@ -0,0 +1,38 @@ +#![allow(deprecated)] + +use aes::Aes128; +use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit}; +use generic_array::GenericArray; +use rand::RngCore; +pub struct Aes128Key([u8; 16]); + +impl Aes128Key { + pub fn new_random() -> Self { + let mut rng = rand::rng(); + let mut key = [0_u8; 16]; + rng.fill_bytes(&mut key); + Aes128Key(key) + } +} + +pub fn encrypt_block(plaintext: &[u8; 16], key: &Aes128Key) -> Box<[u8; 16]> { + let key = GenericArray::from(key.0); + let mut block = GenericArray::from(*plaintext); + + let cipher = Aes128::new(&key); + + cipher.encrypt_block(&mut block); + + Box::new(block.into()) +} + +pub fn decrypt_block(ciphertext: &[u8; 16], key: &Aes128Key) -> Box<[u8; 16]> { + let key = GenericArray::from(key.0); + let mut block = GenericArray::from(*ciphertext); + + let cipher = Aes128::new(&key); + + cipher.decrypt_block(&mut block); + + Box::new(block.into()) +} diff --git a/submissions/challenge1/filip_krawczyk/src/bin/bit_flipping.rs b/submissions/challenge1/filip_krawczyk/src/bin/bit_flipping.rs new file mode 100644 index 0000000..29e1180 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/bin/bit_flipping.rs @@ -0,0 +1,50 @@ +use block_ciphers::{ + bit_flipping::{cookie::encode_userdata, server::Server}, + cbc::CbcEncryptedBlocks, + iv::Iv, +}; + +fn xor_arrays(a: &[u8], b: &[u8]) -> Vec { + a.iter().zip(b.iter()).map(|(a, b)| a ^ b).collect() +} + +pub fn main() { + let server = Server::new(); + + // We want to construct a cookie like this: + // admin=true;x=aaa%20MCs;userdata=;comment2=%20like%20a%20pound%20of%20bacon + // ^^^^^^^^^^^^^^^^----------------^^^^^^^^^^^^^^^^----------------^^^^^^^^^^^^^^^^ + + // But we have for example: + // comment1=cooking%20MCs;userdata=;comment2=%20like%20a%20pound%20of%20bacon + // ^^^^^^^^^^^^^^^^----------------^^^^^^^^^^^^^^^^----------------^^^^^^^^^^^^^^^^ + + let encrypted_cookie = server.get_encrypted_cookie_for_user(""); + + // We assume that encoding algorithm is known, so that we know exactly one (plaintext, ciphertext) pair. + let my_encoded_data = encode_userdata(""); + + // Compute the data in the first block in the encoded plaintext. + let first_block_data = my_encoded_data.as_bytes()[0..16].to_owned(); + + // We want to modify that first block to this value: + let desired_first_block_data = "admin=true;x=aaa".as_bytes().to_owned(); + + // Compute what value IV should be set to in order to change the first block to the desired value. + let desired_cookie_xor = xor_arrays(&first_block_data, &desired_first_block_data); + let new_iv = Iv::new_unchecked( + xor_arrays(encrypted_cookie.iv.get(), &desired_cookie_xor) + .try_into() + .unwrap(), + ); + + let crafted_cookie = CbcEncryptedBlocks { + iv: new_iv, + ciphertext: encrypted_cookie.ciphertext.clone(), + }; + + let is_admin_normal = server.is_admin(&encrypted_cookie).unwrap(); + println!("Is admin normal: {is_admin_normal}"); + let is_admin_crafted = server.is_admin(&crafted_cookie).unwrap(); + println!("Is admin crafted: {is_admin_crafted}"); +} diff --git a/submissions/challenge1/filip_krawczyk/src/bin/padding_oracle.rs b/submissions/challenge1/filip_krawczyk/src/bin/padding_oracle.rs new file mode 100644 index 0000000..b2966e6 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/bin/padding_oracle.rs @@ -0,0 +1,105 @@ +use block_ciphers::{ + blocks::AesBlocks, cbc::CbcEncryptedBlocks, padding_oracle::oracle::PaddingOracle, + pkcs7::unpad_mut, +}; + +pub fn main() { + let oracle = PaddingOracle::new(false); + + // We get an encrypted message with unknown key that we want to decrypt. + let encrypted_message = oracle.get_encrypted_message(); + + let plaintext = break_message(&oracle, &encrypted_message); + + let string = String::from_utf8(plaintext).unwrap(); + + println!("Plaintext: {string}"); +} + +fn break_message(oracle: &PaddingOracle, encrypted_message: &CbcEncryptedBlocks) -> Vec { + let mut full_message = encrypted_message.to_full_message(); + let mut plaintext = Vec::new(); + + // We start by decrypting the last block, which requires modifying 2nd last block that directly affects decrypted last block via xor. + for current_block in (1..full_message.block_count()).rev() { + let unmodified_full_message = full_message.clone(); + // We start with the last byte and work our way back to the first byte. + for current_byte in (0..=15).rev() { + let search_value = 16 - current_byte as u8; + + // We obtain the value of the current byte in the plaintext. + let byte_value = + break_byte(oracle, &mut full_message, current_block, current_byte).unwrap(); + plaintext.push(byte_value); + + // step 1: We modify `current_byte`-th last byte from value `byte_value` to `search_value + 1` (which is next padding that we will check) + // E.g. if we broke ** ** ** xx 04 04 04 04 (8 bytes for simplicity) and know that x is 0xb3, + // we need to xor it with 0xb3 ^ 0x05, so that it becomes 0x05 + *full_message + .get_byte(current_block - 1, current_byte) + .unwrap() ^= byte_value ^ (search_value + 1); + + // step 2: We increment other padding bytes by one, so for the example above: + // before step 1: ** ** ** xx 04 04 04 04 + // after step 1 : ** ** ** 05 04 04 04 04 + // after step 2 : ** ** ** 05 05 05 05 05 + for i in current_byte + 1..=15 { + *full_message.get_byte(current_block - 1, i).unwrap() ^= + search_value ^ (search_value + 1); + } + } + full_message = unmodified_full_message; + full_message.pop_block().unwrap(); + } + + plaintext.reverse(); + unpad_mut(&mut plaintext, 16).unwrap(); + plaintext +} + +fn break_byte( + oracle: &PaddingOracle, + full_message: &mut AesBlocks, + current_block: usize, + current_byte: usize, +) -> Option { + let search_value = 16 - current_byte as u8; + let mut result = None; + + // To find `current_byte`-th last byte in the plaintext, we need to find a byte `byte` such that: + for byte in 0..=255 { + // Modify the byte in question until we find a valid padding, which can tell us what the value at that byte is. + *full_message + .get_byte(current_block - 1, current_byte) + .unwrap() ^= byte; + + if oracle.is_padding_valid(&CbcEncryptedBlocks::from_full_message(full_message)) { + if current_byte != 15 { + result = Some(byte ^ search_value); + } else { + // In case of breaking the last byte, it could be the case that we got "lucky" + // and didn't get padding of length 1, but rather something longer, + // so we would get multiple valid answers. + // To filter those cases, we can just change 2nd last byte and make sure that padding is still valid. + *full_message + .get_byte(current_block - 1, current_byte - 1) + .unwrap() ^= 1; + if oracle.is_padding_valid(&CbcEncryptedBlocks::from_full_message(full_message)) { + result = Some(byte ^ search_value); + } + *full_message + .get_byte(current_block - 1, current_byte - 1) + .unwrap() ^= 1; + } + } + // Undo the modification, so that the message is untouched. + *full_message + .get_byte(current_block - 1, current_byte) + .unwrap() ^= byte; + + if result.is_some() { + return result; + } + } + None +} diff --git a/submissions/challenge1/filip_krawczyk/src/bit_flipping/cookie.rs b/submissions/challenge1/filip_krawczyk/src/bit_flipping/cookie.rs new file mode 100644 index 0000000..07755c5 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/bit_flipping/cookie.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; + +use urlencoding::{decode, encode}; + +pub fn encode_userdata(userdata: &str) -> String { + let escaped = encode(userdata); + format!("comment1=cooking%20MCs;userdata={escaped};comment2=%20like%20a%20pound%20of%20bacon") +} + +fn split_2(input: &str, separator: char) -> Option<(&str, &str)> { + let mut split = input.split(separator); + + let v1 = split.next()?; + let v2 = split.next()?; + if split.next().is_some() { + return None; + } + Some((v1, v2)) +} + +pub fn parse_cookie(cookie: &str) -> Result, ParseCookieError> { + let mut values = HashMap::new(); + for pair in cookie.split(';') { + let (key, value) = split_2(pair, '=').ok_or(ParseCookieError::InvalidCookieFormat)?; + let decoded_value = decode(value).map_err(|_| ParseCookieError::InvalidUrlEncoding)?; + values.insert(key.to_string(), decoded_value.to_string()); + } + Ok(values) +} + +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ParseCookieError { + #[error("Invalid cookie format")] + InvalidCookieFormat, + #[error("Invalid URL encoding")] + InvalidUrlEncoding, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_cookie() { + let cookie = encode_userdata("some_user_data"); + let values = parse_cookie(&cookie).unwrap(); + let expected = HashMap::from([ + ("comment1".to_string(), "cooking MCs".to_string()), + ("comment2".to_string(), " like a pound of bacon".to_string()), + ("userdata".to_string(), "some_user_data".to_string()), + ]); + assert_eq!(values, expected); + } + + #[test] + fn test_userdata_sanitization() { + let cookie = encode_userdata("something;x=y"); + let values = parse_cookie(&cookie).unwrap(); + let expected = HashMap::from([ + ("comment1".to_string(), "cooking MCs".to_string()), + ("comment2".to_string(), " like a pound of bacon".to_string()), + ("userdata".to_string(), "something;x=y".to_string()), + ]); + assert_eq!(values, expected); + } + + #[test] + fn test_invalid_cookie_multiple_equals() { + let cookie = "x=y=z".to_string(); + let values = parse_cookie(&cookie); + assert_eq!(values, Err(ParseCookieError::InvalidCookieFormat)); + } + + #[test] + fn test_invalid_cookie_no_equals() { + let cookie = "x".to_string(); + let values = parse_cookie(&cookie); + assert_eq!(values, Err(ParseCookieError::InvalidCookieFormat)); + } + + #[test] + fn test_invalid_cookie_non_utf8() { + let cookie = "x=%FF".to_string(); + let values = parse_cookie(&cookie); + assert_eq!(values, Err(ParseCookieError::InvalidUrlEncoding)); + } +} diff --git a/submissions/challenge1/filip_krawczyk/src/bit_flipping/mod.rs b/submissions/challenge1/filip_krawczyk/src/bit_flipping/mod.rs new file mode 100644 index 0000000..70bbe2b --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/bit_flipping/mod.rs @@ -0,0 +1,2 @@ +pub mod cookie; +pub mod server; diff --git a/submissions/challenge1/filip_krawczyk/src/bit_flipping/server.rs b/submissions/challenge1/filip_krawczyk/src/bit_flipping/server.rs new file mode 100644 index 0000000..b94801c --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/bit_flipping/server.rs @@ -0,0 +1,53 @@ +use crate::{ + aes::Aes128Key, + bit_flipping::cookie::{ParseCookieError, encode_userdata, parse_cookie}, + cbc::{CbcEncryptedBlocks, decrypt, encrypt}, + iv::Iv, + pkcs7::{Pkcs7PaddingError, pad, unpad}, +}; + +pub struct Server { + key: Aes128Key, +} + +impl Default for Server { + fn default() -> Self { + Self::new() + } +} + +impl Server { + pub fn new() -> Self { + Self { + key: Aes128Key::new_random(), + } + } + + pub fn get_encrypted_cookie_for_user(&self, userdata: &str) -> CbcEncryptedBlocks { + let cookie = encode_userdata(userdata); + let padded = pad::<16>(cookie.as_bytes()); + let iv = Iv::new_random(); + encrypt(&padded, &iv, &self.key) + } + + pub fn is_admin(&self, cookie: &CbcEncryptedBlocks) -> Result { + let plaintext = decrypt(cookie, &self.key); + let unpadded = unpad(&plaintext)?; + let utf8 = String::from_utf8(unpadded).map_err(|_| MessageDecodingError::InvalidUtf8)?; + let values = parse_cookie(&utf8)?; + let is_admin = values.get("admin").map(|v| v == "true").unwrap_or(false); + Ok(is_admin) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum MessageDecodingError { + #[error("Invalid UTF-8")] + InvalidUtf8, + + #[error(transparent)] + Pkcs7PaddingError(#[from] Pkcs7PaddingError), + + #[error(transparent)] + ParseCookieError(#[from] ParseCookieError), +} diff --git a/submissions/challenge1/filip_krawczyk/src/blocks.rs b/submissions/challenge1/filip_krawczyk/src/blocks.rs new file mode 100644 index 0000000..741f345 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/blocks.rs @@ -0,0 +1,86 @@ +use std::ops::{Deref, DerefMut}; + +/// Represents a sequence of blocks of a given size. +/// Its length (number of bytes) is guaranteed to be a multiple of BLOCK_SIZE_BYTES. +#[derive(Clone)] +pub struct Blocks(Vec); + +impl Blocks { + /// Creates a new Blocks instance from a vector of bytes. + /// Returns an error if the length of the vector is not a multiple of BLOCK_SIZE_BYTES. + pub fn new(data: Vec) -> Result { + if data.len() % (BLOCK_SIZE_BYTES as usize) != 0 { + return Err(BlocksError); + } + Ok(Blocks(data)) + } + + /// Returns the number of blocks in the sequence. + pub fn block_count(&self) -> usize { + self.0.len() / BLOCK_SIZE_BYTES as usize + } + + /// Returns a mutable reference to the n-th block in the sequence. + pub fn nth_block_mut(&mut self, n: usize) -> Option<&mut [u8]> { + let start_index = n * BLOCK_SIZE_BYTES as usize; + let end_index = start_index + BLOCK_SIZE_BYTES as usize; + self.0.get_mut(start_index..end_index) + } + + /// Returns a mutable reference to the byte at the given block index and byte index. + pub fn get_byte(&mut self, block_index: usize, bytes_index: usize) -> Option<&mut u8> { + self.0 + .get_mut(block_index * BLOCK_SIZE_BYTES as usize + bytes_index) + } + + /// Removes the last block from the sequence and returns it. + pub fn pop_block(&mut self) -> Option> { + if self.0.len() < BLOCK_SIZE_BYTES as usize { + return None; + } + Some( + self.0 + .drain(self.0.len() - BLOCK_SIZE_BYTES as usize..) + .collect::>() + .into_boxed_slice(), + ) + } +} + +impl Deref for Blocks { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Blocks { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::fmt::Debug for Blocks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (i, byte) in self.iter().enumerate() { + if i % BLOCK_SIZE_BYTES as usize == 0 && i != 0 { + write!(f, " | ")?; + } + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Invalid blocks")] +pub struct BlocksError; + +pub type AesBlocks = Blocks<16>; + +impl AesBlocks { + /// Returns an iterator that yields each block in the sequence as a slice of 16 bytes. + pub fn iter_blocks(&self) -> impl Iterator { + self.0.chunks(16).map(|chunk| chunk.try_into().unwrap()) + } +} diff --git a/submissions/challenge1/filip_krawczyk/src/cbc.rs b/submissions/challenge1/filip_krawczyk/src/cbc.rs new file mode 100644 index 0000000..cc88d1a --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/cbc.rs @@ -0,0 +1,298 @@ +use std::fmt::Debug; + +use crate::{ + aes::{Aes128Key, decrypt_block, encrypt_block}, + blocks::AesBlocks, + iv::Iv, +}; +#[derive(Debug)] +pub struct CbcEncryptedBlocks { + pub iv: Iv, + pub ciphertext: AesBlocks, +} + +impl CbcEncryptedBlocks { + /// Converts iv and blocks to the full message (iv + blocks). + pub fn to_full_message(&self) -> AesBlocks { + let mut full_message = self.iv.to_vec(); + full_message.extend(self.ciphertext.iter()); + AesBlocks::new(full_message).unwrap() + } + + /// Separates the full message into iv and blocks. + pub fn from_full_message(full_message: &AesBlocks) -> Self { + let mut iter = full_message.iter().copied(); + let iv = Iv::new_unchecked( + iter.by_ref() + .take(16) + .collect::>() + .try_into() + .unwrap(), + ); + let ciphertext = AesBlocks::new(iter.collect()).unwrap(); + CbcEncryptedBlocks { iv, ciphertext } + } +} + +pub fn encrypt(plaintext_blocks: &AesBlocks, iv: &Iv, key: &Aes128Key) -> CbcEncryptedBlocks { + let mut previous_ciphertext = iv.get().to_owned(); + let mut ciphertext_blocks = Vec::new(); + for block in plaintext_blocks.iter_blocks() { + let xor_block = xor_blocks(block, &previous_ciphertext); + let encrypted_block = encrypt_block(&xor_block, key); + ciphertext_blocks.extend(encrypted_block.iter()); + previous_ciphertext = *encrypted_block; + } + CbcEncryptedBlocks { + iv: iv.clone(), + ciphertext: AesBlocks::new(ciphertext_blocks).unwrap(), + } +} + +pub fn decrypt(ciphertext_blocks: &CbcEncryptedBlocks, key: &Aes128Key) -> AesBlocks { + let mut previous_ciphertext = ciphertext_blocks.iv.get().to_owned(); + let mut plaintext_blocks = Vec::new(); + for block in ciphertext_blocks.ciphertext.iter_blocks() { + let decrypted_block = decrypt_block(block, key); + let xor_block = xor_blocks(&decrypted_block, &previous_ciphertext); + plaintext_blocks.extend(xor_block.iter()); + previous_ciphertext = *block; + } + AesBlocks::new(plaintext_blocks).unwrap() +} + +fn xor_blocks(block1: &[u8; 16], block2: &[u8; 16]) -> [u8; 16] { + let mut xor_block = [0_u8; 16]; + for ((out, b1), b2) in xor_block.iter_mut().zip(block1.iter()).zip(block2.iter()) { + *out = b1 ^ b2; + } + xor_block +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_full_message_and_from_full_message_roundtrip() { + let iv = Iv::new_unchecked([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, + ]); + let ciphertext = AesBlocks::new(vec![ + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]) + .unwrap(); + + let encrypted = CbcEncryptedBlocks { + iv: iv.clone(), + ciphertext: ciphertext.clone(), + }; + + let full_message = encrypted.to_full_message(); + let reconstructed = CbcEncryptedBlocks::from_full_message(&full_message); + + assert_eq!(reconstructed.iv.get(), iv.get()); + assert_eq!(reconstructed.ciphertext.as_slice(), ciphertext.as_slice()); + } + + #[test] + fn test_to_full_message_and_from_full_message_multiple_blocks() { + let iv = Iv::new_unchecked([0xff; 16]); + let ciphertext = AesBlocks::new(vec![0u8; 64]).unwrap(); // 4 blocks + + let encrypted = CbcEncryptedBlocks { + iv: iv.clone(), + ciphertext: ciphertext.clone(), + }; + + let full_message = encrypted.to_full_message(); + assert_eq!(full_message.len(), 80); // 16 (IV) + 64 (4 blocks) + + let reconstructed = CbcEncryptedBlocks::from_full_message(&full_message); + assert_eq!(reconstructed.iv.get(), iv.get()); + assert_eq!(reconstructed.ciphertext.as_slice(), ciphertext.as_slice()); + } + + #[test] + fn test_encrypt_decrypt_single_block() { + let key = Aes128Key::new_random(); + let iv = Iv::new_random(); + let plaintext = AesBlocks::new(vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, + ]) + .unwrap(); + + let encrypted = encrypt(&plaintext, &iv, &key); + let decrypted = decrypt(&encrypted, &key); + + assert_eq!(decrypted.as_slice(), plaintext.as_slice()); + } + + #[test] + fn test_encrypt_decrypt_multiple_blocks() { + let key = Aes128Key::new_random(); + let iv = Iv::new_random(); + let plaintext = AesBlocks::new(vec![0u8; 64]).unwrap(); // 4 blocks + + let encrypted = encrypt(&plaintext, &iv, &key); + let decrypted = decrypt(&encrypted, &key); + + assert_eq!(decrypted.as_slice(), plaintext.as_slice()); + } + + #[test] + fn test_encrypt_decrypt_roundtrip_various_sizes() { + let key = Aes128Key::new_random(); + + for size in [16, 32, 48, 64, 80, 96] { + let iv = Iv::new_random(); + let plaintext = AesBlocks::new(vec![0x42u8; size]).unwrap(); + + let encrypted = encrypt(&plaintext, &iv, &key); + let decrypted = decrypt(&encrypted, &key); + + assert_eq!( + decrypted.as_slice(), + plaintext.as_slice(), + "Failed for size {size}" + ); + } + } + + #[test] + fn test_encrypt_decrypt_different_plaintexts_same_key() { + let key = Aes128Key::new_random(); + let iv = Iv::new_random(); + + let plaintext1 = AesBlocks::new(vec![0x00u8; 32]).unwrap(); + let plaintext2 = AesBlocks::new(vec![0xFFu8; 32]).unwrap(); + + let encrypted1 = encrypt(&plaintext1, &iv, &key); + let encrypted2 = encrypt(&plaintext2, &iv, &key); + + // Different plaintexts should produce different ciphertexts + assert_ne!( + encrypted1.ciphertext.as_slice(), + encrypted2.ciphertext.as_slice() + ); + + // But both should decrypt correctly + let decrypted1 = decrypt(&encrypted1, &key); + let decrypted2 = decrypt(&encrypted2, &key); + + assert_eq!(decrypted1.as_slice(), plaintext1.as_slice()); + assert_eq!(decrypted2.as_slice(), plaintext2.as_slice()); + } + + #[test] + fn test_encrypt_decrypt_different_ivs_same_plaintext() { + let key = Aes128Key::new_random(); + let plaintext = AesBlocks::new(vec![0x42u8; 32]).unwrap(); + + let iv1 = Iv::new_random(); + let iv2 = Iv::new_random(); + + let encrypted1 = encrypt(&plaintext, &iv1, &key); + let encrypted2 = encrypt(&plaintext, &iv2, &key); + + // Different IVs should produce different ciphertexts (even with same plaintext) + assert_ne!( + encrypted1.ciphertext.as_slice(), + encrypted2.ciphertext.as_slice() + ); + + // But both should decrypt to the same plaintext + let decrypted1 = decrypt(&encrypted1, &key); + let decrypted2 = decrypt(&encrypted2, &key); + + assert_eq!(decrypted1.as_slice(), plaintext.as_slice()); + assert_eq!(decrypted2.as_slice(), plaintext.as_slice()); + } + + #[test] + fn test_encrypt_decrypt_deterministic_with_same_inputs() { + // Test that encryption is deterministic: same inputs produce same outputs + let key = Aes128Key::new_random(); + let iv = Iv::new_unchecked([0u8; 16]); + let plaintext = AesBlocks::new(vec![0u8; 32]).unwrap(); + + let encrypted1 = encrypt(&plaintext, &iv, &key); + let encrypted2 = encrypt(&plaintext, &iv, &key); + + // Same inputs should produce same ciphertext + assert_eq!(encrypted1.iv.get(), encrypted2.iv.get()); + assert_eq!( + encrypted1.ciphertext.as_slice(), + encrypted2.ciphertext.as_slice() + ); + } + + #[test] + fn test_xor_blocks() { + let block1 = [0xFFu8; 16]; + let block2 = [0x00u8; 16]; + let result = xor_blocks(&block1, &block2); + assert_eq!(result, [0xFFu8; 16]); + + let block1 = [0xAAu8; 16]; + let block2 = [0x55u8; 16]; + let result = xor_blocks(&block1, &block2); + assert_eq!(result, [0xFFu8; 16]); + + let block1 = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, + ]; + let block2 = [ + 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, + 0x01, 0x00, + ]; + let result = xor_blocks(&block1, &block2); + assert_eq!( + result, + [ + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f + ] + ); + } + + #[test] + fn test_encrypt_preserves_iv() { + let key = Aes128Key::new_random(); + let iv = Iv::new_unchecked([0x42u8; 16]); + let plaintext = AesBlocks::new(vec![0u8; 32]).unwrap(); + + let encrypted = encrypt(&plaintext, &iv, &key); + + // IV should be preserved in the encrypted structure + assert_eq!(encrypted.iv.get(), iv.get()); + } + + #[test] + fn test_cbc_encryption_chaining() { + // Test that CBC mode properly chains blocks + // If we encrypt two identical blocks, they should produce different ciphertexts + let key = Aes128Key::new_random(); + let iv = Iv::new_random(); + + // Create plaintext with two identical blocks + let mut plaintext_bytes = vec![0x42u8; 16]; + plaintext_bytes.extend_from_slice(&[0x42u8; 16]); + let plaintext = AesBlocks::new(plaintext_bytes).unwrap(); + + let encrypted = encrypt(&plaintext, &iv, &key); + + // Extract the two ciphertext blocks + let blocks: Vec<_> = encrypted.ciphertext.iter_blocks().collect(); + assert_eq!(blocks.len(), 2); + + // In CBC mode, identical plaintext blocks should produce different ciphertext blocks + // (unless the first block XORed with IV equals the second block XORed with first ciphertext) + // In general, with random IV, they should be different + assert_ne!(blocks[0], blocks[1]); + } +} diff --git a/submissions/challenge1/filip_krawczyk/src/iv.rs b/submissions/challenge1/filip_krawczyk/src/iv.rs new file mode 100644 index 0000000..5948724 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/iv.rs @@ -0,0 +1,38 @@ +use rand::RngCore; +use std::{fmt::Debug, ops::Deref}; + +#[derive(Clone)] +pub struct Iv([u8; 16]); + +impl Iv { + pub fn new_random() -> Self { + let mut rng = rand::rng(); + let mut iv = [0_u8; 16]; + rng.fill_bytes(&mut iv); + Iv(iv) + } + + pub fn new_unchecked(iv: [u8; 16]) -> Self { + Iv(iv) + } + + pub fn get(&self) -> &[u8; 16] { + &self.0 + } +} + +impl Deref for Iv { + type Target = [u8; 16]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Debug for Iv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for byte in self.0.iter() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} diff --git a/submissions/challenge1/filip_krawczyk/src/lib.rs b/submissions/challenge1/filip_krawczyk/src/lib.rs new file mode 100644 index 0000000..b3eb2ed --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/lib.rs @@ -0,0 +1,7 @@ +pub mod aes; +pub mod bit_flipping; +pub mod blocks; +pub mod cbc; +pub mod iv; +pub mod padding_oracle; +pub mod pkcs7; diff --git a/submissions/challenge1/filip_krawczyk/src/padding_oracle/mod.rs b/submissions/challenge1/filip_krawczyk/src/padding_oracle/mod.rs new file mode 100644 index 0000000..6f2a45c --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/padding_oracle/mod.rs @@ -0,0 +1 @@ +pub mod oracle; diff --git a/submissions/challenge1/filip_krawczyk/src/padding_oracle/oracle.rs b/submissions/challenge1/filip_krawczyk/src/padding_oracle/oracle.rs new file mode 100644 index 0000000..4aec251 --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/padding_oracle/oracle.rs @@ -0,0 +1,45 @@ +use crate::{ + aes::Aes128Key, + cbc::{CbcEncryptedBlocks, decrypt, encrypt}, + iv::Iv, + pkcs7::{pad, unpad}, +}; + +pub struct PaddingOracle { + key: Aes128Key, + verbose: bool, +} + +impl PaddingOracle { + pub fn new(verbose: bool) -> Self { + let key = Aes128Key::new_random(); + Self { key, verbose } + } + + pub fn get_encrypted_message(&self) -> CbcEncryptedBlocks { + // This message is private and never exposed unencrypted. + let plaintext = + "Hello, world! Some longer message to show that we can decrypt multiple blocks."; + let padded = pad::<16>(plaintext.as_bytes()); + if self.verbose { + println!("[PaddingOracle] Padded plaintext: {padded:?}"); + } + let iv = Iv::new_random(); + let encrypted = encrypt(&padded, &iv, &self.key); + if self.verbose { + println!("[PaddingOracle] Encrypted: {encrypted:?}"); + } + encrypted + } + + pub fn is_padding_valid(&self, ciphertext: &CbcEncryptedBlocks) -> bool { + if self.verbose { + println!("[PaddingOracle] Checking padding validity for ciphertext: {ciphertext:?}"); + } + let plaintext = decrypt(ciphertext, &self.key); + if self.verbose { + println!("[PaddingOracle] Decrypted to: {plaintext:?}"); + } + unpad(&plaintext).is_ok() + } +} diff --git a/submissions/challenge1/filip_krawczyk/src/pkcs7.rs b/submissions/challenge1/filip_krawczyk/src/pkcs7.rs new file mode 100644 index 0000000..cc435ed --- /dev/null +++ b/submissions/challenge1/filip_krawczyk/src/pkcs7.rs @@ -0,0 +1,262 @@ +use std::iter; + +use thiserror::Error; + +use crate::blocks::Blocks; + +pub fn pad_mut(data: &mut Vec, block_size: u8) { + let last_block_fill_size = (data.len() % (block_size as usize)) as u8; // within range [0, block_size-1] + let padding_size = block_size - last_block_fill_size; // within range [1, block_size] + data.extend(iter::repeat_n(padding_size, padding_size as usize)); +} + +pub fn pad(data: &[u8]) -> Blocks { + let mut padded = data.to_vec(); + pad_mut(&mut padded, BLOCK_SIZE_BYTES); + // This is safe because the length of the padded data is guaranteed to be a multiple of BLOCK_SIZE_BYTES + Blocks::new(padded).unwrap() +} + +pub fn unpad_mut(data: &mut Vec, block_size: u8) -> Result<(), Pkcs7PaddingError> { + if data.len() % (block_size as usize) != 0 { + return Err(Pkcs7PaddingError); + } + let Some(padding_byte) = data.last() else { + // Even empty data is padded to non-empty vector, so empty padded data is invalid + return Err(Pkcs7PaddingError); + }; + if !(1..=block_size).contains(padding_byte) { + return Err(Pkcs7PaddingError); + } + if data.len() < *padding_byte as usize { + return Err(Pkcs7PaddingError); + } + if !data + .iter() + .rev() + .take(*padding_byte as usize) + .all(|b| b == padding_byte) + { + return Err(Pkcs7PaddingError); + } + data.truncate(data.len() - *padding_byte as usize); + Ok(()) +} + +pub fn unpad( + data: &Blocks, +) -> Result, Pkcs7PaddingError> { + let mut unpadded = data.to_vec(); + unpad_mut(&mut unpadded, BLOCK_SIZE_BYTES)?; + Ok(unpadded) +} + +#[derive(Debug, Error)] +#[error("Invalid PKCS#7 padding")] +pub struct Pkcs7PaddingError; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pad_mut_empty() { + let mut data = vec![]; + pad_mut(&mut data, 16); + assert_eq!(data.len(), 16); + assert!(data.iter().all(|&b| b == 16)); + } + + #[test] + fn test_pad_mut_exact_block_size() { + let mut data = vec![0u8; 16]; + pad_mut(&mut data, 16); + assert_eq!(data.len(), 32); + assert!(data[16..].iter().all(|&b| b == 16)); + } + + #[test] + fn test_pad_mut_partial_block() { + let mut data = vec![1, 2, 3]; + pad_mut(&mut data, 16); + assert_eq!(data.len(), 16); + assert_eq!(data[0..3], [1, 2, 3]); + assert!(data[3..].iter().all(|&b| b == 13)); + } + + #[test] + fn test_pad_mut_one_byte_short() { + let mut data = vec![0u8; 15]; + pad_mut(&mut data, 16); + assert_eq!(data.len(), 16); + assert_eq!(data[15], 1); + } + + #[test] + fn test_pad_mut_multiple_blocks() { + let mut data = vec![0u8; 32]; + pad_mut(&mut data, 16); + assert_eq!(data.len(), 48); + assert!(data[32..].iter().all(|&b| b == 16)); + } + + #[test] + fn test_pad_mut_different_block_sizes() { + let mut data = vec![1, 2]; + pad_mut(&mut data, 4); + assert_eq!(data.len(), 4); + assert_eq!(data, vec![1, 2, 2, 2]); + + let mut data = vec![1, 2, 3, 4, 5]; + pad_mut(&mut data, 8); + assert_eq!(data.len(), 8); + assert_eq!(data[5..], [3, 3, 3]); + } + + #[test] + fn test_pad() { + let data = vec![1, 2, 3]; + let blocks = pad::<16>(&data); + assert_eq!(blocks.len(), 16); + assert_eq!(&blocks[0..3], &[1, 2, 3]); + assert!(blocks[3..].iter().all(|&b| b == 13)); + } + + #[test] + fn test_unpad_mut_valid() { + let mut data = vec![1, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]; + unpad_mut(&mut data, 16).unwrap(); + assert_eq!(data, vec![1, 2, 3]); + } + + #[test] + fn test_unpad_mut_full_block_padding() { + let mut data = vec![16u8; 16]; + unpad_mut(&mut data, 16).unwrap(); + assert_eq!(data, vec![]); + } + + #[test] + fn test_unpad_mut_single_byte_padding() { + let mut data = vec![0u8; 15]; + data.push(1); + unpad_mut(&mut data, 16).unwrap(); + assert_eq!(data, vec![0u8; 15]); + } + + #[test] + fn test_unpad_mut_not_multiple_of_block_size() { + let mut data = vec![1, 2, 3]; + assert!(unpad_mut(&mut data, 16).is_err()); + } + + #[test] + fn test_unpad_mut_empty() { + let mut data = vec![]; + assert!(unpad_mut(&mut data, 16).is_err()); + } + + #[test] + fn test_unpad_mut_invalid_padding_byte_zero() { + let mut data = vec![0u8; 16]; + assert!(unpad_mut(&mut data, 16).is_err()); + } + + #[test] + fn test_unpad_mut_invalid_padding_byte_too_large() { + let mut data = vec![0u8; 31]; + data.push(17); // padding byte > block_size + assert!(unpad_mut(&mut data, 16).is_err()); + } + + #[test] + fn test_unpad_mut_invalid_padding_byte_mismatch() { + let mut data = vec![0u8; 14]; + data.push(3); + data.push(2); // padding bytes not all the same + assert!(unpad_mut(&mut data, 16).is_err()); + } + + #[test] + fn test_unpad_mut_padding_length_exceeds_data() { + let mut data = vec![0u8; 10]; + data.push(16); // padding byte says 16, but data is only 11 bytes + assert!(unpad_mut(&mut data, 16).is_err()); + } + + #[test] + fn test_unpad() { + let data = vec![1, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]; + let blocks = Blocks::new(data).unwrap(); + let unpadded = unpad::<16>(&blocks).unwrap(); + assert_eq!(unpadded, vec![1, 2, 3]); + } + + #[test] + fn test_round_trip_pad_unpad() { + let original = vec![1, 2, 3, 4, 5]; + let padded = pad::<16>(&original); + let unpadded = unpad::<16>(&padded).unwrap(); + assert_eq!(unpadded, original); + } + + #[test] + fn test_round_trip_empty() { + let original = vec![]; + let padded = pad::<16>(&original); + let unpadded = unpad::<16>(&padded).unwrap(); + assert_eq!(unpadded, original); + } + + #[test] + fn test_round_trip_exact_block_size() { + let original = vec![0u8; 16]; + let padded = pad::<16>(&original); + let unpadded = unpad::<16>(&padded).unwrap(); + assert_eq!(unpadded, original); + } + + #[test] + fn test_round_trip_multiple_blocks() { + let original = vec![0u8; 32]; + let padded = pad::<16>(&original); + let unpadded = unpad::<16>(&padded).unwrap(); + assert_eq!(unpadded, original); + } + + #[test] + fn test_round_trip_different_block_sizes() { + let original = vec![1, 2, 3, 4, 5]; + let padded = pad::<8>(&original); + let unpadded = unpad::<8>(&padded).unwrap(); + assert_eq!(unpadded, original); + + let original = vec![1, 2]; + let padded = pad::<4>(&original); + let unpadded = unpad::<4>(&padded).unwrap(); + assert_eq!(unpadded, original); + } + + #[test] + fn test_unpad_mut_mixed_content() { + let mut data = vec![ + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x05, 0x05, 0x05, + 0x05, 0x05, + ]; + unpad_mut(&mut data, 16).unwrap(); + assert_eq!(data, b"Hello World"); + } + + #[test] + fn test_round_trip_multiple_sizes() { + let mut data = Vec::new(); + for i in 0..100 { + let padded = pad::<16>(&data); + // At least one byte + ceil to 16 + assert_eq!(padded.len(), ((i + 1) as usize).div_ceil(16) * 16); + let unpadded = unpad::<16>(&padded).unwrap(); + assert_eq!(unpadded, data); + data.push(i as u8); + } + } +}