//! Message padding to hide plaintext lengths from the server. //! //! Pads payloads to fixed bucket sizes before MLS encryption so that the //! ciphertext does not reveal the actual message length. //! //! # Wire format //! //! ```text //! [real_length: 4 bytes LE (u32)][payload: real_length bytes][random padding] //! ``` //! //! The total padded output is always one of the bucket sizes: 256, 1024, 4096, 16384 bytes. //! For payloads larger than 16380 bytes, rounds up to the nearest 16384-byte multiple. //! //! ## Uniform boundary padding (traffic analysis resistance) //! //! [`pad_uniform`] / [`unpad_uniform`] pad to a configurable byte boundary //! (default 256) instead of exponential buckets. This produces more uniform //! ciphertext sizes at the cost of slightly more padding overhead. use rand::RngCore; use crate::error::CoreError; /// Default uniform padding boundary in bytes. pub const DEFAULT_PADDING_BOUNDARY: usize = 256; /// Bucket sizes in bytes. The smallest (256) accommodates a sealed sender /// envelope (99 bytes overhead) plus a short message. const BUCKETS: &[usize] = &[256, 1024, 4096, 16384]; /// Select the smallest bucket that fits `content_len + 4` (the 4-byte length prefix). fn bucket_for(content_len: usize) -> usize { let total = content_len + 4; for &b in BUCKETS { if total <= b { return b; } } // Larger than biggest bucket: round up to nearest 16384-byte multiple. total.div_ceil(16384) * 16384 } /// Pad a payload to the next bucket boundary with cryptographic random bytes. pub fn pad(payload: &[u8]) -> Vec { let bucket = bucket_for(payload.len()); let mut out = Vec::with_capacity(bucket); out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); out.extend_from_slice(payload); let pad_len = bucket - 4 - payload.len(); if pad_len > 0 { let mut padding = vec![0u8; pad_len]; rand::rngs::OsRng.fill_bytes(&mut padding); out.extend_from_slice(&padding); } out } /// Remove padding and return the original payload. pub fn unpad(padded: &[u8]) -> Result, CoreError> { if padded.len() < 4 { return Err(CoreError::AppMessage("padded message too short".into())); } let real_len = u32::from_le_bytes([padded[0], padded[1], padded[2], padded[3]]) as usize; if 4 + real_len > padded.len() { return Err(CoreError::AppMessage( "padded real_length exceeds buffer".into(), )); } Ok(padded[4..4 + real_len].to_vec()) } /// Pad a payload to the nearest multiple of `boundary` bytes. /// /// Uses the same wire format as [`pad`]: `[real_length: 4 bytes LE][payload][random padding]`. /// The total output length is always a multiple of `boundary`. A `boundary` of 0 is /// treated as [`DEFAULT_PADDING_BOUNDARY`]. pub fn pad_uniform(payload: &[u8], boundary: usize) -> Vec { let boundary = if boundary == 0 { DEFAULT_PADDING_BOUNDARY } else { boundary }; let total = payload.len() + 4; // 4-byte length prefix let padded_len = total.div_ceil(boundary) * boundary; let mut out = Vec::with_capacity(padded_len); out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); out.extend_from_slice(payload); let pad_len = padded_len - total; if pad_len > 0 { let mut padding = vec![0u8; pad_len]; rand::rngs::OsRng.fill_bytes(&mut padding); out.extend_from_slice(&padding); } out } /// Remove uniform padding. Wire format is identical to [`unpad`]. pub fn unpad_uniform(padded: &[u8]) -> Result, CoreError> { unpad(padded) } /// Generate a decoy payload that looks identical to a real padded message. /// /// Returns random bytes of length equal to a `boundary`-aligned padded message. /// The 4-byte length prefix is set to 0, so [`unpad_uniform`] returns an empty payload. pub fn generate_decoy(boundary: usize) -> Vec { let boundary = if boundary == 0 { DEFAULT_PADDING_BOUNDARY } else { boundary }; let mut out = vec![0u8; boundary]; // Length prefix = 0 (decoy carries no real payload). // Fill the rest with random bytes so it is indistinguishable from padding. rand::rngs::OsRng.fill_bytes(&mut out[4..]); out } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use super::*; #[test] fn round_trip_small() { let msg = b"hello"; let padded = pad(msg); assert_eq!(padded.len(), 256); // smallest bucket let unpadded = unpad(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn round_trip_medium() { let msg = vec![0xAB; 300]; let padded = pad(&msg); assert_eq!(padded.len(), 1024); // second bucket let unpadded = unpad(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn round_trip_large() { let msg = vec![0xCD; 2000]; let padded = pad(&msg); assert_eq!(padded.len(), 4096); // third bucket let unpadded = unpad(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn round_trip_very_large() { let msg = vec![0xEF; 10000]; let padded = pad(&msg); assert_eq!(padded.len(), 16384); // largest bucket let unpadded = unpad(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn round_trip_oversized() { let msg = vec![0xFF; 20000]; let padded = pad(&msg); assert_eq!(padded.len(), 32768); // 2 * 16384 let unpadded = unpad(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn round_trip_empty() { let msg = b""; let padded = pad(msg); assert_eq!(padded.len(), 256); // smallest bucket let unpadded = unpad(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn exactly_at_bucket_boundary() { // 252 + 4 = 256 → fits in 256 bucket exactly let msg = vec![0x42; 252]; let padded = pad(&msg); assert_eq!(padded.len(), 256); let unpadded = unpad(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn unpad_too_short_fails() { assert!(unpad(&[0, 0]).is_err()); } #[test] fn unpad_invalid_length_fails() { // Claims 1000 bytes but only has 10 let mut bad = (1000u32).to_le_bytes().to_vec(); bad.extend_from_slice(&[0u8; 10]); assert!(unpad(&bad).is_err()); } // ── Uniform padding tests ────────────────────────────────────────────── #[test] fn uniform_round_trip_default_boundary() { let msg = b"uniform padding test"; let padded = pad_uniform(msg, DEFAULT_PADDING_BOUNDARY); assert_eq!(padded.len() % DEFAULT_PADDING_BOUNDARY, 0); assert_eq!(padded.len(), 256); // 20 + 4 = 24, rounds up to 256 let unpadded = unpad_uniform(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn uniform_custom_boundary() { let msg = vec![0xAA; 100]; let padded = pad_uniform(&msg, 128); assert_eq!(padded.len() % 128, 0); assert_eq!(padded.len(), 128); // 100 + 4 = 104, rounds up to 128 let unpadded = unpad_uniform(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn uniform_exact_boundary() { // 252 + 4 = 256, exactly on boundary let msg = vec![0xBB; 252]; let padded = pad_uniform(&msg, 256); assert_eq!(padded.len(), 256); let unpadded = unpad_uniform(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn uniform_one_over_boundary() { // 253 + 4 = 257, rounds up to 512 let msg = vec![0xCC; 253]; let padded = pad_uniform(&msg, 256); assert_eq!(padded.len(), 512); let unpadded = unpad_uniform(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn uniform_zero_boundary_uses_default() { let msg = b"zero boundary"; let padded = pad_uniform(msg, 0); assert_eq!(padded.len() % DEFAULT_PADDING_BOUNDARY, 0); let unpadded = unpad_uniform(&padded).unwrap(); assert_eq!(unpadded, msg); } #[test] fn decoy_is_boundary_aligned() { let decoy = generate_decoy(256); assert_eq!(decoy.len(), 256); assert_eq!(decoy.len() % 256, 0); } #[test] fn decoy_unpads_to_empty() { let decoy = generate_decoy(256); let payload = unpad_uniform(&decoy).unwrap(); assert!(payload.is_empty()); } #[test] fn decoy_default_boundary() { let decoy = generate_decoy(0); assert_eq!(decoy.len(), DEFAULT_PADDING_BOUNDARY); } }