Skip to main content

cachekit_core/
byte_storage.rs

1//! LZ4 compression and xxHash3 checksums for raw byte storage.
2//!
3//! Provides integrity-protected byte storage with security validation:
4//! - 512MB size limits for decompression bomb protection
5//! - 1000x max compression ratio enforcement
6//! - xxHash3-64 checksums for corruption detection (19x faster than Blake3)
7
8use crate::metrics::OperationMetrics;
9#[cfg(feature = "compression")]
10use lz4_flex;
11use serde::{Deserialize, Serialize};
12use std::sync::{Arc, Mutex};
13#[cfg(not(target_arch = "wasm32"))]
14use std::time::Instant;
15use thiserror::Error;
16
17/// Error types for ByteStorage operations
18#[derive(Debug, Error, Clone, PartialEq)]
19pub enum ByteStorageError {
20    #[error("input exceeds maximum size")]
21    InputTooLarge,
22
23    #[error("decompression ratio exceeds safety limit")]
24    DecompressionBomb,
25
26    #[error("integrity check failed")]
27    ChecksumMismatch,
28
29    #[error("compression failed")]
30    CompressionFailed,
31
32    #[error("decompression failed")]
33    DecompressionFailed,
34
35    #[error("size validation failed")]
36    SizeValidationFailed,
37
38    #[error("serialization failed: {0}")]
39    SerializationFailed(String),
40
41    #[error("deserialization failed: {0}")]
42    DeserializationFailed(String),
43}
44
45// Security constants - Production-safe limits
46const MAX_UNCOMPRESSED_SIZE: usize = 512 * 1024 * 1024; // 512MB limit
47const MAX_COMPRESSED_SIZE: usize = 512 * 1024 * 1024; // 512MB limit
48/// Maximum allowed compression ratio (1000:1)
49/// Uses u64 for integer-only arithmetic to prevent floating-point precision bypass attacks
50const MAX_COMPRESSION_RATIO: u64 = 1000;
51
52/// Storage envelope for raw byte storage
53/// Contains compressed data with integrity checking
54#[derive(Serialize, Deserialize)]
55pub struct StorageEnvelope {
56    /// Compressed payload data
57    ///
58    /// Serialized as msgpack `bin` (protocol 1.1, `envelope-bin-encoding.md`).
59    /// Scope is strictly this field: `checksum` stays array-of-ints by
60    /// normative exclusion (crypto-adjacent surface — MUST NOT flip).
61    #[serde(with = "serde_bytes")]
62    pub compressed_data: Vec<u8>,
63    /// xxHash3-64 checksum for integrity (8 bytes)
64    pub checksum: [u8; 8],
65    /// Original size for validation
66    pub original_size: u32,
67    /// Format identifier (e.g., "msgpack")
68    pub format: String,
69}
70
71impl StorageEnvelope {
72    /// Create new envelope with data compression and checksum.
73    ///
74    /// Takes the input by shared slice: it is only compressed and hashed (both
75    /// borrow), never retained, so there is no reason to own it. Avoids a
76    /// full-payload copy (up to `MAX_UNCOMPRESSED_SIZE`) on the write path.
77    #[cfg(all(feature = "compression", feature = "checksum"))]
78    pub fn new(data: &[u8], format: String) -> Result<Self, ByteStorageError> {
79        // Security: Check input size before compression
80        if data.len() > MAX_UNCOMPRESSED_SIZE {
81            return Err(ByteStorageError::InputTooLarge);
82        }
83
84        let original_size = data.len() as u32;
85
86        // Compress with LZ4
87        let compressed_data = lz4_flex::compress(data);
88
89        // Security: Check compressed size
90        if compressed_data.len() > MAX_COMPRESSED_SIZE {
91            return Err(ByteStorageError::InputTooLarge);
92        }
93
94        // Single canonical xxHash3-64 definition (see crate::checksum)
95        let checksum = crate::checksum::checksum(data);
96
97        Ok(StorageEnvelope {
98            compressed_data,
99            checksum,
100            original_size,
101            format,
102        })
103    }
104
105    /// Extract and validate data from envelope
106    #[cfg(all(feature = "compression", feature = "checksum"))]
107    pub fn extract(&self) -> Result<Vec<u8>, ByteStorageError> {
108        // Security: Validate envelope structure first
109        if self.compressed_data.len() > MAX_COMPRESSED_SIZE {
110            return Err(ByteStorageError::InputTooLarge);
111        }
112
113        if self.original_size as usize > MAX_UNCOMPRESSED_SIZE {
114            return Err(ByteStorageError::InputTooLarge);
115        }
116
117        // Security: Check compression ratio for decompression bomb protection
118        // Uses integer arithmetic to prevent floating-point precision bypass attacks
119        let compressed_size = self.compressed_data.len() as u64;
120
121        // Step 1: Zero check - empty compressed data with non-zero original is a bomb
122        if compressed_size == 0 {
123            return Err(ByteStorageError::DecompressionBomb);
124        }
125
126        // Step 2: Checked multiplication - overflow = bomb (fail-safe)
127        let max_allowed_original = MAX_COMPRESSION_RATIO
128            .checked_mul(compressed_size)
129            .ok_or(ByteStorageError::DecompressionBomb)?;
130
131        // Step 3: Compare original_size against computed maximum
132        if (self.original_size as u64) > max_allowed_original {
133            return Err(ByteStorageError::DecompressionBomb);
134        }
135
136        // Decompress (with validated sizes)
137        let decompressed = lz4_flex::decompress(&self.compressed_data, self.original_size as usize)
138            .map_err(|_| ByteStorageError::DecompressionFailed)?;
139
140        // Verify checksum (checksum validation happens AFTER decompression to prevent processing corrupted data)
141        // false -> ChecksumMismatch (preserve the error variant; verify_checksum returns bool)
142        if !crate::checksum::verify_checksum(&decompressed, &self.checksum) {
143            return Err(ByteStorageError::ChecksumMismatch);
144        }
145
146        // Verify size (final safety check)
147        if decompressed.len() != self.original_size as usize {
148            return Err(ByteStorageError::SizeValidationFailed);
149        }
150
151        Ok(decompressed)
152    }
153}
154
155/// Raw byte storage engine (pure Rust core)
156/// Simple store/retrieve interface with no type awareness
157pub struct ByteStorage {
158    default_format: String,
159    /// Last operation metrics (interior mutability for observability)
160    last_metrics: Arc<Mutex<OperationMetrics>>,
161}
162
163impl ByteStorage {
164    /// Create new ByteStorage instance
165    pub fn new(default_format: Option<String>) -> Self {
166        ByteStorage {
167            default_format: default_format.unwrap_or_else(|| "msgpack".to_string()),
168            last_metrics: Arc::new(Mutex::new(OperationMetrics::new())),
169        }
170    }
171
172    /// Store arbitrary bytes with compression and checksums
173    ///
174    /// Returns serialized StorageEnvelope bytes
175    #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
176    pub fn store(&self, data: &[u8], format: Option<String>) -> Result<Vec<u8>, ByteStorageError> {
177        // Security: Check input size before processing
178        if data.len() > MAX_UNCOMPRESSED_SIZE {
179            return Err(ByteStorageError::InputTooLarge);
180        }
181
182        let format = format.unwrap_or_else(|| self.default_format.clone());
183
184        // Time compression operation (wasm32: Instant unavailable, use 0)
185        #[cfg(not(target_arch = "wasm32"))]
186        let compression_start = Instant::now();
187        let original_size = data.len();
188
189        let envelope = StorageEnvelope::new(data, format)?;
190
191        #[cfg(not(target_arch = "wasm32"))]
192        let compression_micros = compression_start.elapsed().as_micros() as u64;
193        #[cfg(target_arch = "wasm32")]
194        let compression_micros = 0u64;
195        let compressed_size = envelope.compressed_data.len();
196
197        // Serialize envelope with MessagePack
198        let envelope_bytes = rmp_serde::to_vec(&envelope)
199            .map_err(|e| ByteStorageError::SerializationFailed(e.to_string()))?;
200
201        // Security: Final check on serialized envelope size
202        if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
203            return Err(ByteStorageError::InputTooLarge);
204        }
205
206        // Update metrics for observability
207        if let Ok(mut metrics) = self.last_metrics.lock() {
208            *metrics = OperationMetrics::new().with_compression(
209                compression_micros,
210                original_size,
211                compressed_size,
212            );
213        }
214
215        Ok(envelope_bytes)
216    }
217
218    /// Retrieve and validate stored bytes
219    ///
220    /// Returns (original_data, format_identifier)
221    #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
222    pub fn retrieve(&self, envelope_bytes: &[u8]) -> Result<(Vec<u8>, String), ByteStorageError> {
223        // Security: Check envelope size before deserializing
224        if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
225            return Err(ByteStorageError::InputTooLarge);
226        }
227
228        // Deserialize envelope
229        let envelope: StorageEnvelope = rmp_serde::from_slice(envelope_bytes)
230            .map_err(|e| ByteStorageError::DeserializationFailed(e.to_string()))?;
231
232        // Time decompression and checksum operations (wasm32: Instant unavailable, use 0)
233        #[cfg(not(target_arch = "wasm32"))]
234        let decompress_start = Instant::now();
235
236        // Extract and validate data (all security checks happen inside extract())
237        let data = envelope.extract()?;
238
239        #[cfg(not(target_arch = "wasm32"))]
240        let decompress_micros = decompress_start.elapsed().as_micros() as u64;
241        #[cfg(target_arch = "wasm32")]
242        let decompress_micros = 0u64;
243
244        // Calculate compression ratio from stored metadata
245        let compressed_size = envelope.compressed_data.len();
246        let original_size = envelope.original_size as usize;
247
248        // Update metrics for observability
249        if let Ok(mut metrics) = self.last_metrics.lock() {
250            *metrics = OperationMetrics::new().with_compression(
251                decompress_micros,
252                original_size,
253                compressed_size,
254            );
255        }
256
257        Ok((data, envelope.format))
258    }
259
260    /// Get compression ratio for given data
261    #[cfg(feature = "compression")]
262    pub fn estimate_compression(&self, data: &[u8]) -> Result<f64, ByteStorageError> {
263        // Security: Check size before compression
264        if data.len() > MAX_UNCOMPRESSED_SIZE {
265            return Err(ByteStorageError::InputTooLarge);
266        }
267
268        let compressed = lz4_flex::compress(data);
269
270        Ok(data.len() as f64 / compressed.len() as f64)
271    }
272
273    /// Validate envelope without extracting data
274    #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
275    pub fn validate(&self, envelope_bytes: &[u8]) -> bool {
276        // Security: Check size before validating
277        if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
278            return false; // Invalid due to size limit
279        }
280
281        match rmp_serde::from_slice::<StorageEnvelope>(envelope_bytes) {
282            Ok(envelope) => envelope.extract().is_ok(),
283            Err(_) => false,
284        }
285    }
286
287    /// Get metrics from last operation
288    ///
289    /// Returns a snapshot of metrics from the most recent store() or retrieve() call
290    pub fn get_last_metrics(&self) -> OperationMetrics {
291        self.last_metrics
292            .lock()
293            .map(|metrics| metrics.clone())
294            .unwrap_or_else(|_| OperationMetrics::new())
295    }
296
297    /// Get security limits
298    pub fn max_uncompressed_size(&self) -> usize {
299        MAX_UNCOMPRESSED_SIZE
300    }
301
302    pub fn max_compressed_size(&self) -> usize {
303        MAX_COMPRESSED_SIZE
304    }
305
306    pub fn max_compression_ratio(&self) -> u64 {
307        MAX_COMPRESSION_RATIO
308    }
309}
310
311impl Default for ByteStorage {
312    fn default() -> Self {
313        Self::new(None)
314    }
315}
316
317#[cfg(all(
318    test,
319    feature = "compression",
320    feature = "checksum",
321    feature = "messagepack"
322))]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn envelope_embeds_canonical_checksum() {
328        let data = b"DRY-guard payload";
329        let envelope = StorageEnvelope::new(data, "test".to_string()).unwrap();
330        assert_eq!(envelope.checksum, crate::checksum::checksum(data));
331    }
332
333    #[test]
334    fn test_storage_envelope_roundtrip() {
335        let data = b"Hello, World! This is test data for compression.".to_vec();
336        let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
337        let extracted = envelope.extract().unwrap();
338        assert_eq!(data, extracted);
339    }
340
341    #[test]
342    fn test_compression_works() {
343        let data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(); // Highly compressible
344        let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
345        assert!(envelope.compressed_data.len() < data.len());
346    }
347
348    #[test]
349    fn test_checksum_validation() {
350        let mut envelope = StorageEnvelope::new(b"test", "test".to_string()).unwrap();
351        // Corrupt the checksum
352        envelope.checksum[0] = !envelope.checksum[0];
353        // Fail-open guard: the DRY refactor must still surface the specific
354        // ChecksumMismatch variant (not silently succeed on a flipped checksum).
355        assert!(matches!(
356            envelope.extract(),
357            Err(ByteStorageError::ChecksumMismatch)
358        ));
359    }
360
361    #[test]
362    fn test_raw_persistence_roundtrip() {
363        let storage = ByteStorage::new(None);
364        let test_data = b"test data for persistence";
365
366        let stored = storage.store(test_data, None).unwrap();
367        let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
368        assert_eq!(test_data, retrieved_data.as_slice());
369        assert_eq!("msgpack", format);
370    }
371
372    #[test]
373    fn test_size_limits_input() {
374        let storage = ByteStorage::new(None);
375
376        // Create data larger than MAX_UNCOMPRESSED_SIZE (512MB)
377        let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
378
379        let result = storage.store(&large_data, None);
380        assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
381    }
382
383    #[test]
384    fn test_size_limits_envelope() {
385        // Create data exactly at the limit
386        let max_data = vec![0u8; MAX_UNCOMPRESSED_SIZE];
387        let envelope_result = StorageEnvelope::new(&max_data, "test".to_string());
388
389        // Should succeed at exactly the limit
390        assert!(envelope_result.is_ok());
391    }
392
393    #[test]
394    fn test_compression_ratio_bomb_protection() {
395        // Simulate a decompression bomb scenario
396        let malicious_envelope = StorageEnvelope {
397            compressed_data: vec![0u8; 1000], // Small compressed size
398            checksum: [0u8; 8],               // Fake checksum
399            original_size: 200 * 1024 * 1024, // Claims 200MB original (200x expansion)
400            format: "test".to_string(),
401        };
402
403        let result = malicious_envelope.extract();
404        assert!(matches!(result, Err(ByteStorageError::DecompressionBomb)));
405    }
406
407    // ============================================================================
408    // Decompression Bomb Edge Case Tests (Task 6.1)
409    // ============================================================================
410
411    #[test]
412    fn test_decompression_bomb_zero_compressed_size() {
413        // WHY: Empty compressed data claiming non-zero original is always a bomb
414        let malicious_envelope = StorageEnvelope {
415            compressed_data: vec![], // Zero compressed size
416            checksum: [0u8; 8],
417            original_size: 1000, // Claims 1KB original
418            format: "test".to_string(),
419        };
420
421        let result = malicious_envelope.extract();
422        assert!(
423            matches!(result, Err(ByteStorageError::DecompressionBomb)),
424            "Zero compressed size should be rejected as decompression bomb"
425        );
426    }
427
428    #[test]
429    fn test_decompression_bomb_extreme_ratio() {
430        // WHY: Test extreme ratio that exceeds 1000:1
431        // compressed_size=1, original_size=2000 → 2000:1 ratio exceeds limit
432        // Note: u32::MAX would be caught by InputTooLarge first (> MAX_UNCOMPRESSED_SIZE)
433        let malicious_envelope = StorageEnvelope {
434            compressed_data: vec![0u8; 1], // 1 byte compressed
435            checksum: [0u8; 8],
436            original_size: 2000, // 2000:1 ratio (exceeds 1000:1 limit)
437            format: "test".to_string(),
438        };
439
440        let result = malicious_envelope.extract();
441        assert!(
442            matches!(result, Err(ByteStorageError::DecompressionBomb)),
443            "Extreme ratio should be rejected as bomb: {:?}",
444            result
445        );
446    }
447
448    #[test]
449    fn test_decompression_u32_max_original_size() {
450        // WHY: u32::MAX original_size exceeds MAX_UNCOMPRESSED_SIZE
451        // Should fail with InputTooLarge, not DecompressionBomb
452        // This validates the check order (size limits before ratio)
453        let malicious_envelope = StorageEnvelope {
454            compressed_data: vec![0u8; 1000],
455            checksum: [0u8; 8],
456            original_size: u32::MAX, // ~4GB exceeds 512MB limit
457            format: "test".to_string(),
458        };
459
460        let result = malicious_envelope.extract();
461        assert!(
462            matches!(result, Err(ByteStorageError::InputTooLarge)),
463            "u32::MAX should be rejected as InputTooLarge (exceeds 512MB limit): {:?}",
464            result
465        );
466    }
467
468    #[test]
469    fn test_decompression_exactly_at_threshold() {
470        // WHY: Exactly 1000:1 ratio should be accepted (pass ratio check)
471        // The test verifies the ratio check passes - subsequent failures are expected
472        // (invalid LZ4 data will fail at decompression or checksum)
473        let envelope = StorageEnvelope {
474            compressed_data: vec![0u8; 100], // 100 bytes compressed (not valid LZ4)
475            checksum: [0u8; 8],
476            original_size: 100_000, // 100KB = exactly 1000:1 ratio
477            format: "test".to_string(),
478        };
479
480        let result = envelope.extract();
481        // KEY: Should NOT fail with DecompressionBomb (ratio check should pass)
482        // Will fail with DecompressionFailed, ChecksumMismatch, or SizeValidationFailed
483        assert!(
484            !matches!(result, Err(ByteStorageError::DecompressionBomb)),
485            "Exactly 1000:1 ratio should pass bomb check: {:?}",
486            result
487        );
488        assert!(
489            result.is_err(),
490            "Invalid data should still fail after ratio check"
491        );
492    }
493
494    #[test]
495    fn test_decompression_just_over_threshold() {
496        // WHY: 1001:1 ratio should be rejected
497        let malicious_envelope = StorageEnvelope {
498            compressed_data: vec![0u8; 100], // 100 bytes compressed
499            checksum: [0u8; 8],
500            original_size: 100_001, // 100.001KB = 1000.01:1 ratio (just over)
501            format: "test".to_string(),
502        };
503
504        let result = malicious_envelope.extract();
505        assert!(
506            matches!(result, Err(ByteStorageError::DecompressionBomb)),
507            "Just over 1000:1 ratio should be rejected as bomb"
508        );
509    }
510
511    #[test]
512    fn test_decompression_bomb_integer_boundary() {
513        // WHY: Test near u64 overflow boundary
514        // MAX_COMPRESSION_RATIO (1000) * compressed_size must not overflow
515        // u64::MAX / 1000 ≈ 18,446,744,073,709,551 is max safe compressed_size
516        // But we're constrained by MAX_COMPRESSED_SIZE (512MB), so overflow is unlikely
517        // This test verifies the check works at realistic boundary
518
519        let envelope = StorageEnvelope {
520            compressed_data: vec![0u8; 1_000_000], // 1MB compressed
521            checksum: [0u8; 8],
522            original_size: 1_000_000_000, // 1GB = exactly 1000:1 ratio
523            format: "test".to_string(),
524        };
525
526        // Should fail due to size limit (1GB > MAX_UNCOMPRESSED_SIZE)
527        let result = envelope.extract();
528        assert!(
529            matches!(result, Err(ByteStorageError::InputTooLarge)),
530            "Should fail size check before ratio check: {:?}",
531            result
532        );
533    }
534
535    #[test]
536    fn test_envelope_size_validation() {
537        let storage = ByteStorage::new(None);
538
539        // Create oversized envelope bytes
540        let oversized_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
541
542        let result = storage.retrieve(&oversized_envelope);
543        assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
544    }
545
546    #[test]
547    fn test_security_limits_getters() {
548        let storage = ByteStorage::new(None);
549
550        assert_eq!(storage.max_uncompressed_size(), MAX_UNCOMPRESSED_SIZE);
551        assert_eq!(storage.max_compressed_size(), MAX_COMPRESSED_SIZE);
552        assert_eq!(storage.max_compression_ratio(), 1000u64);
553    }
554
555    #[test]
556    fn test_compression_estimate_security() {
557        let storage = ByteStorage::new(None);
558
559        // Test with oversized data
560        let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
561        let result = storage.estimate_compression(&large_data);
562        assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
563    }
564
565    #[test]
566    fn test_validate_security() {
567        let storage = ByteStorage::new(None);
568
569        // Test with oversized envelope
570        let large_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
571        let result = storage.validate(&large_envelope);
572        assert!(!result); // Should be invalid due to size
573    }
574
575    #[test]
576    fn test_edge_case_exactly_at_limits() {
577        // Test data exactly at 512MB
578        let storage = ByteStorage::new(None);
579        let max_size_data = vec![1u8; MAX_UNCOMPRESSED_SIZE]; // Fill with 1s to ensure it's compressible
580
581        // Should succeed at exactly the limit
582        let result = storage.store(&max_size_data, None);
583        assert!(result.is_ok());
584    }
585
586    #[test]
587    fn test_zero_size_edge_case() {
588        let storage = ByteStorage::new(None);
589        let empty_data = vec![];
590
591        let stored = storage.store(&empty_data, None).unwrap();
592        let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
593        assert_eq!(empty_data, retrieved_data);
594        assert_eq!("msgpack", format);
595    }
596
597    #[test]
598    fn test_metrics_collection_on_store() {
599        let storage = ByteStorage::new(None);
600        let test_data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(); // Compressible
601
602        // Store data
603        storage.store(&test_data, None).unwrap();
604        let metrics = storage.get_last_metrics();
605
606        // Verify metrics were collected
607        assert!(metrics.compression_ratio > 0.0); // Should have valid compression ratio
608    }
609
610    #[test]
611    fn test_metrics_collection_on_retrieve() {
612        let storage = ByteStorage::new(None);
613        let test_data = b"test data for retrieval metrics";
614
615        // Store then retrieve
616        let stored = storage.store(test_data, None).unwrap();
617        storage.retrieve(&stored).unwrap();
618        let metrics = storage.get_last_metrics();
619
620        // Verify retrieve metrics were collected
621        assert!(metrics.compression_ratio > 0.0); // Should have valid ratio
622    }
623}
624
625// Kani Formal Verification Proofs
626// These proofs use bounded model checking to verify critical security properties
627// Bounded to complete within 10 minutes as per specification requirements
628#[cfg(kani)]
629mod kani_proofs {
630    use super::*;
631
632    /// Verify checksum integrity (corruption detection)
633    /// Property: Any single-bit checksum corruption is always detected
634    #[kani::proof]
635    #[kani::unwind(10)] // Need 8+ for memcmp of 8-byte xxHash3-64 checksum
636    fn verify_checksum_detects_corruption() {
637        // Create symbolic checksum (xxHash3-64 produces 8 bytes)
638        let checksum_a: [u8; 8] = kani::any();
639        let mut checksum_b = checksum_a;
640
641        // Flip exactly one bit
642        let byte_index: usize = kani::any();
643        let bit_index: usize = kani::any();
644        kani::assume(byte_index < 8);
645        kani::assume(bit_index < 8);
646
647        checksum_b[byte_index] ^= 1 << bit_index;
648
649        // Property: Corrupted checksum must differ from original
650        assert_ne!(checksum_a, checksum_b);
651    }
652
653    /// Verify decompression bomb protection (compression ratio limits)
654    /// Property: Malicious compression ratios exceeding 1000x are always rejected
655    /// Uses integer arithmetic to match production implementation
656    #[kani::proof]
657    #[kani::unwind(3)]
658    fn verify_decompression_bomb_protection() {
659        // Symbolic envelope parameters
660        let compressed_size: u64 = kani::any();
661        let original_size: u64 = kani::any();
662
663        // Constrain to reasonable test ranges
664        kani::assume(compressed_size > 0 && compressed_size <= 1000);
665        kani::assume(original_size > 0);
666
667        // Simulate the 3-step check from StorageEnvelope::extract()
668        // Step 1: Zero check already covered by assume
669        // Step 2: Checked multiplication
670        let max_allowed = MAX_COMPRESSION_RATIO.checked_mul(compressed_size);
671
672        // Property: If original_size exceeds max_allowed, extraction must fail
673        if let Some(max) = max_allowed {
674            let would_reject = original_size > max;
675            let exceeds_ratio = original_size > MAX_COMPRESSION_RATIO * compressed_size;
676            assert_eq!(would_reject, exceeds_ratio);
677        } else {
678            // Overflow case: always reject (fail-safe)
679            assert!(true); // Overflow is always rejected
680        }
681    }
682
683    /// Verify size limit enforcement on input
684    /// Property: Inputs exceeding MAX_UNCOMPRESSED_SIZE are always rejected
685    #[kani::proof]
686    #[kani::unwind(3)]
687    fn verify_input_size_limits() {
688        let size: usize = kani::any();
689
690        // Test boundary conditions around the limit
691        kani::assume(size <= MAX_UNCOMPRESSED_SIZE + 100);
692
693        // Property: Size check logic is correct
694        let exceeds_limit = size > MAX_UNCOMPRESSED_SIZE;
695        let should_reject = size > MAX_UNCOMPRESSED_SIZE;
696
697        assert_eq!(exceeds_limit, should_reject);
698    }
699
700    /// Verify size limit enforcement on compressed data
701    /// Property: Compressed data exceeding MAX_COMPRESSED_SIZE is rejected
702    #[kani::proof]
703    #[kani::unwind(3)]
704    fn verify_compressed_size_limits() {
705        let compressed_size: usize = kani::any();
706
707        // Test boundary conditions
708        kani::assume(compressed_size <= MAX_COMPRESSED_SIZE + 100);
709
710        // Property: Size check logic is correct
711        let exceeds_limit = compressed_size > MAX_COMPRESSED_SIZE;
712        let should_reject = compressed_size > MAX_COMPRESSED_SIZE;
713
714        assert_eq!(exceeds_limit, should_reject);
715    }
716
717    /// Verify compression ratio calculation is safe (integer arithmetic)
718    /// Property: Ratio check never panics and correctly identifies bombs
719    #[kani::proof]
720    #[kani::unwind(3)]
721    fn verify_compression_ratio_calculation_safety() {
722        let original_size: u64 = kani::any();
723        let compressed_size: u64 = kani::any();
724
725        // Constrain to prevent division by zero and keep ranges manageable
726        kani::assume(compressed_size > 0);
727        kani::assume(compressed_size <= 10000);
728        kani::assume(original_size <= 100_000_000); // 100MB max for test
729
730        // Property 1: checked_mul never panics (it returns None on overflow)
731        let result = MAX_COMPRESSION_RATIO.checked_mul(compressed_size);
732
733        // Property 2: If multiplication succeeds, comparison is valid
734        if let Some(max_allowed) = result {
735            // The check `original_size > max_allowed` is always safe
736            let is_bomb = original_size > max_allowed;
737
738            // Verify equivalence: is_bomb == (original_size > 1000 * compressed_size)
739            // This holds when no overflow occurred
740            if original_size <= max_allowed {
741                assert!(!is_bomb);
742            } else {
743                assert!(is_bomb);
744            }
745        }
746
747        // Property 3: Zero compressed_size is handled by separate check (not tested here)
748    }
749}