hexz_core/algo/encryption/aes_gcm.rs
1//! AES-256-GCM authenticated encryption for snapshot blocks.
2//!
3//! This module provides block-level encryption for Hexz snapshots using the AES-256-GCM
4//! (Galois/Counter Mode) authenticated encryption algorithm. It implements the `Encryptor`
5//! trait to provide transparent encryption and decryption of individual snapshot blocks with
6//! cryptographic authentication to detect tampering or corruption.
7//!
8//! # Algorithm Overview
9//!
10//! **AES-256-GCM** is a widely-adopted AEAD (Authenticated Encryption with Associated Data)
11//! cipher that provides both confidentiality and integrity protection:
12//!
13//! - **Cipher**: AES (Advanced Encryption Standard) with 256-bit keys
14//! - **Mode**: GCM (Galois/Counter Mode) for authenticated encryption
15//! - **Authentication**: 128-bit authentication tag appended to ciphertext
16//! - **Key Derivation**: PBKDF2-HMAC-SHA256 for password-based key derivation
17//! - **Nonce Strategy**: Deterministic 96-bit nonces derived from block indices
18//!
19//! # Security Properties and Guarantees
20//!
21//! ## Confidentiality
22//!
23//! AES-256 provides strong confidentiality guarantees under the assumption that the key
24//! remains secret. With a 256-bit keyspace, brute-force attacks are computationally
25//! infeasible with current and foreseeable technology (estimated 2^256 operations required).
26//!
27//! ## Integrity and Authentication
28//!
29//! The GCM authentication tag ensures that:
30//! - **Tampering Detection**: Any modification to ciphertext or nonce is detected with
31//! probability 1 - 2^-128 (effectively certain for 128-bit tags)
32//! - **No Silent Corruption**: Authentication failures abort decryption rather than
33//! returning corrupted plaintext
34//! - **Block Binding**: Each block is bound to its index, preventing block reordering or
35//! duplication attacks
36//!
37//! ## Key Derivation Security
38//!
39//! PBKDF2-HMAC-SHA256 provides password-based key derivation with the following properties:
40//! - **Salt**: 128-bit random salt prevents rainbow table attacks and ensures key uniqueness
41//! - **Iterations**: Default 600,000 iterations (per OWASP 2023 recommendations) slows
42//! brute-force attacks to ~500ms per guess on modern CPUs
43//! - **Determinism**: Same (password, salt, iterations) triple always produces the same key,
44//! enabling snapshot decryption without storing the key
45//!
46//! # Performance Characteristics
47//!
48//! ## Throughput
49//!
50//! On modern x86-64 CPUs with AES-NI hardware acceleration:
51//! - **Encryption**: ~3-5 GB/s per core for large blocks (>64 KiB)
52//! - **Decryption**: ~3-5 GB/s per core (symmetric with encryption)
53//! - **Key Derivation**: ~500 ms for 600,000 PBKDF2 iterations (one-time cost per snapshot)
54//!
55//! ## Overhead
56//!
57//! - **Ciphertext Expansion**: +16 bytes per block (128-bit authentication tag)
58//! - **Computational Cost**: ~10-20% CPU overhead vs. unencrypted compression on fast storage
59//! - **Memory**: Negligible (single AES context, ~240 bytes)
60//!
61//! ## Interaction with Compression
62//!
63//! Encryption is applied **after** compression in the write pipeline:
64//! ```text
65//! Plaintext → Compress → Encrypt → Write
66//! Read → Decrypt → Decompress → Plaintext
67//! ```
68//!
69//! This ordering is critical because:
70//! - Ciphertext has high entropy and is incompressible
71//! - Compressing first maximizes space savings
72//! - Authentication tag covers compressed data, detecting compression-layer corruption
73//!
74//! ## Deduplication Interaction
75//!
76//! **Encryption disables block-level deduplication** because:
77//! - Each block uses a unique nonce (derived from block index)
78//! - Identical plaintext blocks produce different ciphertexts
79//! - This is a fundamental security requirement (nonce reuse would break GCM security)
80//!
81//! # When to Use Encryption
82//!
83//! ## Use Encryption When:
84//!
85//! - **Storing snapshots on untrusted media** (cloud storage, external drives, backups)
86//! - **Regulatory/compliance requirements** mandate encryption at rest (GDPR, HIPAA, PCI-DSS)
87//! - **Protecting sensitive VM data** (databases, application secrets, user data)
88//! - **Multi-tenant environments** where snapshots may be accessible to untrusted parties
89//!
90//! ## Do NOT Use Encryption When:
91//!
92//! - **Performance is critical** and data is already protected (local encrypted filesystems)
93//! - **Deduplication is essential** and data is not sensitive (compression-only provides
94//! better space efficiency)
95//! - **Key management is impractical** (no secure way to store/transmit passwords)
96//! - **Data is already encrypted** at the application layer (double encryption adds overhead
97//! without security benefit)
98//!
99//! # Cryptographic Details
100//!
101//! ## Nonce/IV Generation Strategy
102//!
103//! GCM security critically depends on **never reusing a (key, nonce) pair**. This implementation
104//! uses deterministic nonce generation based on block indices:
105//!
106//! ```text
107//! Nonce (96 bits / 12 bytes):
108//! ┌──────────────┬────────────────────────────┐
109//! │ Reserved │ Block Index (u64) │
110//! │ (32 bits) │ (64 bits, big-endian) │
111//! └──────────────┴────────────────────────────┘
112//! Bytes: 0-3 (zeros) 4-11 (block_idx)
113//! ```
114//!
115//! **Uniqueness Guarantee**: Each block in a snapshot has a unique index (0 to 2^64-1), ensuring
116//! unique nonces under a given key. The 32-bit reserved field allows future extensions (e.g.,
117//! snapshot versioning) while maintaining backward compatibility.
118//!
119//! **Determinism**: The same block index always produces the same nonce, enabling stateless
120//! decryption without storing nonces in metadata.
121//!
122//! ## Key Derivation and Expansion
123//!
124//! ```text
125//! Password (arbitrary bytes)
126//! │
127//! ├─→ PBKDF2-HMAC-SHA256(password, salt, iterations=600,000)
128//! │
129//! └─→ 256-bit Derived Key
130//! │
131//! └─→ AES-256-GCM Key Schedule (14 rounds, 15 subkeys)
132//! ```
133//!
134//! The derived key is expanded into AES round keys using the AES key schedule algorithm.
135//! This expansion happens once during `AesGcmEncryptor::new()` and the expanded keys are
136//! stored in the cipher context for reuse.
137//!
138//! ## Authentication Tag Handling
139//!
140//! GCM produces a 128-bit (16-byte) authentication tag that is **appended** to the ciphertext:
141//!
142//! ```text
143//! Ciphertext Layout:
144//! ┌─────────────────────────┬──────────────────┐
145//! │ Encrypted Data │ Auth Tag (128b) │
146//! │ (same length as input) │ (16 bytes) │
147//! └─────────────────────────┴──────────────────┘
148//! ```
149//!
150//! During decryption, the tag is:
151//! 1. Separated from the ciphertext
152//! 2. Recomputed from the ciphertext and nonce
153//! 3. Compared in constant time to prevent timing attacks
154//! 4. Decryption aborts if tags do not match
155//!
156//! ## Block Cipher Mode Specifics
157//!
158//! GCM combines CTR (Counter) mode encryption with GHASH-based authentication:
159//!
160//! - **CTR Mode**: Converts AES into a stream cipher, allowing parallel encryption/decryption
161//! - **GHASH**: Polynomial hash over GF(2^128) for authentication
162//! - **Parallelism**: Encryption/decryption can process blocks in parallel (hardware dependent)
163//! - **Nonce Sensitivity**: Reusing a nonce with the same key catastrophically breaks security
164//! (allows key recovery and forgery)
165//!
166//! ## Thread Safety of Cryptographic Operations
167//!
168//! The `AesGcmEncryptor` struct is **thread-safe** and implements `Send + Sync`:
169//!
170//! - **Immutable Cipher State**: The AES key schedule is initialized once and never modified
171//! - **No Internal Mutation**: Encryption/decryption are pure functions of inputs (no RNG, no state)
172//! - **Deterministic Nonces**: Derived from block indices, not random generation
173//! - **Shared Encryptor**: A single `AesGcmEncryptor` can be safely shared across threads
174//! via `Arc<AesGcmEncryptor>` for concurrent block encryption
175//!
176//! # Security Considerations
177//!
178//! ## Key Management Best Practices
179//!
180//! - **Password Strength**: Use high-entropy passwords (>128 bits, e.g., 20+ random characters)
181//! or key files to resist brute-force attacks
182//! - **Password Storage**: Never store passwords in plaintext; use secure key management systems
183//! (hardware tokens, password managers, environment variables with restricted access)
184//! - **Salt Storage**: Salt is stored in the snapshot header and must be preserved; losing the
185//! salt makes decryption impossible even with the correct password
186//! - **Key Rotation**: Re-encrypting snapshots with new keys requires full rewrite (no in-place
187//! key rotation)
188//!
189//! ## Nonce Reuse Dangers and Mitigation
190//!
191//! **CRITICAL**: Reusing a (key, nonce) pair in GCM is catastrophic:
192//! - Allows attackers to recover the authentication key
193//! - Enables forgery of arbitrary ciphertexts
194//! - Leaks plaintext XOR for messages encrypted with the same nonce
195//!
196//! **Mitigation**: Block-index-based nonces ensure uniqueness as long as:
197//! - Each block index is used at most once per snapshot
198//! - The same snapshot is never encrypted with the same key but different data
199//! (snapshots are immutable after creation)
200//!
201//! ## Performance Impact of Encryption
202//!
203//! - **Throughput**: 10-20% reduction in read/write speeds on fast NVMe storage
204//! - **Latency**: Negligible for large blocks (>16 KiB); ~1-2 µs overhead for small blocks
205//! - **Key Derivation**: One-time ~500ms cost when opening encrypted snapshots
206//! - **CPU Utilization**: Encryption is CPU-bound; benefits from AES-NI hardware acceleration
207//!
208//! ## Limitations and Attack Surfaces
209//!
210//! ### Known Limitations
211//!
212//! - **No Forward Secrecy**: Compromising the password allows decryption of all historical data
213//! - **Metadata Leakage**: Snapshot size, block count, and compression ratios are not encrypted
214//! - **Side Channels**: Implementation does not protect against cache-timing or power analysis
215//! attacks (assumes trusted execution environment)
216//! - **Block Index Limit**: Maximum 2^64 blocks per snapshot (impractical limitation: ~1 ZB at
217//! 64 KiB blocks)
218//!
219//! ### Attack Surfaces
220//!
221//! - **Weak Passwords**: Low-entropy passwords can be brute-forced despite PBKDF2
222//! - **Key Derivation Parameters**: Reducing PBKDF2 iterations weakens brute-force resistance
223//! - **Memory Dumps**: Key material is held in process memory and could be extracted by
224//! privileged attackers
225//! - **Filesystem Metadata**: Block offsets and sizes leak access patterns
226//!
227//! ## Compliance Considerations
228//!
229//! ### Standards Compliance
230//!
231//! - **NIST SP 800-38D**: AES-GCM implementation follows NIST recommendations for GCM mode
232//! - **NIST SP 800-132**: PBKDF2 parameters meet NIST password-based key derivation guidelines
233//! - **FIPS 140-2/3**: Underlying AES and SHA-256 algorithms are FIPS-approved (implementation
234//! is not FIPS-certified but uses FIPS-approved primitives from `aes-gcm` and `sha2` crates)
235//!
236//! ### Regulatory Considerations
237//!
238//! - **GDPR**: Encryption at rest satisfies "state of the art" technical measures for personal
239//! data protection (Article 32)
240//! - **HIPAA**: Qualifies as "encryption as specified in the HIPAA Security Rule" for ePHI
241//! - **PCI-DSS**: Meets Requirement 3.4 for encryption of cardholder data at rest
242//!
243//! **NOTE**: Compliance also requires proper key management, access controls, and audit logging
244//! beyond what this module provides.
245//!
246//! # Integration Details
247//!
248//! ## Block Index Constraints and Alignment
249//!
250//! - **Index Range**: Block indices must be in `0..=u64::MAX-1` (u64::MAX may be reserved for
251//! sentinel values in the snapshot format)
252//! - **Uniqueness**: Each index must correspond to a unique logical block position
253//! - **No Alignment**: Block indices need not be contiguous or sequential; sparse indices are
254//! supported
255//! - **Encryption/Decryption Symmetry**: The same `block_idx` used for encryption must be
256//! passed to decryption
257//!
258//! ## Encryption and Compression Interaction
259//!
260//! Encryption integrates into the snapshot write pipeline as:
261//!
262//! ```text
263//! write_block(chunk, compressor, encryptor):
264//! 1. compressed = compressor.compress(chunk) # Compress first
265//! 2. encrypted = encryptor.encrypt(compressed, idx) # Then encrypt
266//! 3. write(encrypted)
267//! ```
268//!
269//! And the read pipeline as:
270//!
271//! ```text
272//! read_block(idx, encryptor, compressor):
273//! 1. encrypted = read_from_storage(idx)
274//! 2. compressed = encryptor.decrypt(encrypted, idx) # Decrypt first
275//! 3. plaintext = compressor.decompress(compressed) # Then decompress
276//! ```
277//!
278//! **Critical**: Decryption failure (wrong key, corrupted data, wrong index) aborts the read
279//! and returns an error; no partial or corrupted data is passed to decompression.
280//!
281//! ## Memory Overhead
282//!
283//! - **AesGcmEncryptor**: ~240 bytes (AES-256-GCM cipher context with expanded key schedule)
284//! - **Per-Operation**: Temporary allocation for ciphertext (plaintext.len() + 16 bytes)
285//! - **No Buffering**: Encryption/decryption are stateless single-pass operations
286//!
287//! ## I/O Patterns
288//!
289//! Encryption does not change I/O patterns but affects sizes:
290//! - **Encrypted Block Size**: `compressed_size + 16` (fixed 16-byte tag overhead)
291//! - **Random Access**: Encryption is block-independent; random reads do not require decrypting
292//! other blocks
293//! - **Parallel I/O**: Multiple blocks can be encrypted/decrypted concurrently (thread-safe)
294//!
295//! # Examples
296//!
297//! ## Basic Encryption/Decryption Workflow
298//!
299//! ```rust
300//! use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
301//!
302//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
303//! // Derive key from password and salt (stored in snapshot header)
304//! let password = b"correct_horse_battery_staple";
305//! let salt = b"random_16byte_sa"; // 16 bytes, cryptographically random
306//! let iterations = 100_000;
307//!
308//! let encryptor = AesGcmEncryptor::new(password, salt, iterations)?;
309//!
310//! // Encrypt a block (e.g., compressed data from block 42)
311//! let plaintext = b"Compressed block data...";
312//! let block_idx = 42;
313//! let ciphertext = encryptor.encrypt(plaintext, block_idx)?;
314//!
315//! // Ciphertext is 16 bytes longer (authentication tag)
316//! assert_eq!(ciphertext.len(), plaintext.len() + 16);
317//!
318//! // Decrypt the block (same index required)
319//! let decrypted = encryptor.decrypt(&ciphertext, block_idx)?;
320//! assert_eq!(decrypted, plaintext);
321//! # Ok(())
322//! # }
323//! ```
324//!
325//! ## Secure Snapshot Encryption
326//!
327//! ```rust
328//! use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
329//! use rand::RngCore;
330//!
331//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
332//! // Generate cryptographically random salt
333//! let mut salt = [0u8; 16];
334//! rand::thread_rng().fill_bytes(&mut salt);
335//!
336//! // Get password from secure source (environment, prompt, key file)
337//! let password = std::env::var("HEXZ_ENCRYPTION_PASSWORD")
338//! .expect("HEXZ_ENCRYPTION_PASSWORD not set")
339//! .into_bytes();
340//!
341//! // Create encryptor with strong parameters
342//! let encryptor = AesGcmEncryptor::new(&password, &salt, 100_000)?;
343//!
344//! // Encrypt multiple blocks
345//! let blocks = vec![b"block0", b"block1", b"block2"];
346//! let mut encrypted_blocks = Vec::new();
347//!
348//! for (idx, block) in blocks.iter().enumerate() {
349//! let ciphertext = encryptor.encrypt(*block, idx as u64)?;
350//! encrypted_blocks.push(ciphertext);
351//! }
352//!
353//! // Store salt in snapshot header for later decryption
354//! // (salt is not secret, but must be preserved exactly)
355//! # Ok(())
356//! # }
357//! ```
358//!
359//! ## Handling Decryption Failures
360//!
361//! ```rust
362//! use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
363//!
364//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
365//! let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
366//! let plaintext = b"Important data";
367//! let ciphertext = encryptor.encrypt(plaintext, 0)?;
368//!
369//! // Wrong password -> decryption fails
370//! let wrong_encryptor = AesGcmEncryptor::new(b"wrong_password", b"salt12345678salt", 100_000)?;
371//! match wrong_encryptor.decrypt(&ciphertext, 0) {
372//! Ok(_) => panic!("Should have failed with wrong password"),
373//! Err(e) => println!("Authentication failed (expected): {}", e),
374//! }
375//!
376//! // Wrong block index -> decryption fails
377//! match encryptor.decrypt(&ciphertext, 999) {
378//! Ok(_) => panic!("Should have failed with wrong index"),
379//! Err(e) => println!("Authentication failed (expected): {}", e),
380//! }
381//!
382//! // Corrupted ciphertext -> decryption fails
383//! let mut corrupted = ciphertext.clone();
384//! corrupted[5] ^= 0xFF; // Flip bits
385//! match encryptor.decrypt(&corrupted, 0) {
386//! Ok(_) => panic!("Should have detected corruption"),
387//! Err(e) => println!("Authentication failed (expected): {}", e),
388//! }
389//! # Ok(())
390//! # }
391//! ```
392//!
393//! ## Thread-Safe Concurrent Encryption
394//!
395//! ```rust
396//! use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
397//! use std::sync::Arc;
398//! use std::thread;
399//!
400//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
401//! // Create encryptor and share across threads
402//! let encryptor = Arc::new(AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?);
403//!
404//! let mut handles = Vec::new();
405//! for idx in 0..10 {
406//! let enc = Arc::clone(&encryptor);
407//! let handle = thread::spawn(move || {
408//! let data = format!("Block {}", idx).into_bytes();
409//! enc.encrypt(&data, idx as u64).unwrap()
410//! });
411//! handles.push(handle);
412//! }
413//!
414//! // Collect encrypted blocks
415//! let encrypted: Vec<_> = handles.into_iter()
416//! .map(|h| h.join().unwrap())
417//! .collect();
418//! # Ok(())
419//! # }
420//! ```
421
422use crate::algo::encryption::Encryptor;
423use aes_gcm::{
424 Aes256Gcm, Key,
425 aead::{Aead, KeyInit, consts::U12, generic_array::GenericArray},
426};
427use hexz_common::constants::{AES_KEY_LENGTH, AES_NONCE_LENGTH};
428use hexz_common::{Error, Result};
429use hmac::Hmac;
430use pbkdf2::pbkdf2;
431use sha2::Sha256;
432use std::fmt;
433
434/// AES-256-GCM encryptor with PBKDF2-derived keys for block-level authenticated encryption.
435///
436/// This struct wraps an AES-256-GCM cipher instance with a key derived from a password using
437/// PBKDF2-HMAC-SHA256. It provides stateless, thread-safe encryption and decryption of
438/// snapshot blocks using deterministic nonces based on block indices.
439///
440/// # Structure
441///
442/// The encryptor holds:
443/// - **Expanded AES Key Schedule**: 14 rounds of 128-bit subkeys derived from the 256-bit key
444/// - **GCM Precomputed Tables**: Multiplication tables for GHASH authentication (hardware
445/// dependent; may use PCLMULQDQ on x86-64)
446///
447/// Total memory footprint: ~240 bytes
448///
449/// # Thread Safety
450///
451/// `AesGcmEncryptor` is **fully thread-safe** (`Send + Sync`) because:
452/// - The cipher state is immutable after construction
453/// - Encryption/decryption use deterministic nonces (no RNG or shared mutable state)
454/// - The underlying `aes_gcm` crate guarantees thread-safe operations
455///
456/// A single encryptor instance can be safely shared across threads via `Arc<AesGcmEncryptor>`
457/// for concurrent block encryption without locking.
458///
459/// # Security Notes
460///
461/// - **Key Material in Memory**: The expanded AES key schedule remains in process memory for
462/// the lifetime of this struct. It is **not** zeroed on drop (Rust does not guarantee
463/// secure memory wiping). Processes with access to memory dumps (debuggers, swap, core dumps)
464/// can potentially extract keys.
465/// - **No Key Rotation**: Keys are fixed at construction; re-keying requires creating a new
466/// `AesGcmEncryptor` instance.
467/// - **Immutable After Creation**: The cipher cannot be reconfigured; all blocks must use the
468/// same key material.
469///
470/// # Implementation Details
471///
472/// Internally uses the `aes-gcm` crate (RustCrypto), which provides:
473/// - **Hardware Acceleration**: AES-NI and PCLMULQDQ instructions on x86-64 (if available)
474/// - **Constant-Time Operations**: Timing-attack resistant implementation (subject to CPU
475/// microarchitecture side channels)
476/// - **NIST Compliance**: Follows NIST SP 800-38D recommendations for GCM mode
477///
478/// # Examples
479///
480/// ## Creating an Encryptor
481///
482/// ```rust
483/// use hexz_core::algo::encryption::AesGcmEncryptor;
484///
485/// // Derive key from password and salt
486/// let password = b"strong_random_password_here";
487/// let salt = b"16_byte_salt____"; // Exactly 16 bytes
488/// let iterations = 100_000;
489///
490/// let encryptor = AesGcmEncryptor::new(password, salt, iterations)?;
491/// // Encryptor is now ready for encrypting/decrypting blocks
492/// # Ok::<(), hexz_common::Error>(())
493/// ```
494///
495/// ## Sharing Across Threads
496///
497/// ```rust
498/// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
499/// use std::sync::Arc;
500/// use std::thread;
501///
502/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
503/// let encryptor = Arc::new(AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?);
504///
505/// let handles: Vec<_> = (0..4).map(|i| {
506/// let enc = Arc::clone(&encryptor);
507/// thread::spawn(move || {
508/// enc.encrypt(b"data", i).unwrap()
509/// })
510/// }).collect();
511///
512/// for handle in handles {
513/// let ciphertext = handle.join().unwrap();
514/// // Process ciphertext...
515/// }
516/// # Ok(())
517/// # }
518/// ```
519pub struct AesGcmEncryptor {
520 /// The AES-256-GCM cipher instance with expanded key schedule.
521 ///
522 /// This field holds the core cryptographic state including:
523 /// - 14 rounds of 128-bit AES subkeys (derived from the 256-bit master key)
524 /// - GCM precomputed authentication tables (for GHASH polynomial multiplication)
525 ///
526 /// The cipher is initialized once during `new()` and remains immutable, enabling
527 /// thread-safe concurrent use without locking.
528 cipher: Aes256Gcm,
529}
530
531impl fmt::Debug for AesGcmEncryptor {
532 /// Renders a redacted debug representation of the encryptor for logging and debugging.
533 ///
534 /// This implementation provides a safe debug representation that **does not leak
535 /// cryptographic key material** into logs, debug output, or error messages. Only the
536 /// algorithm name is exposed.
537 ///
538 /// # Output Format
539 ///
540 /// ```text
541 /// AesGcmEncryptor { cipher: "Aes256Gcm" }
542 /// ```
543 ///
544 /// The output indicates:
545 /// - **Struct Type**: `AesGcmEncryptor` (the wrapper type)
546 /// - **Algorithm**: `"Aes256Gcm"` (string literal, not the actual cipher state)
547 ///
548 /// **No Key Material**: The expanded AES key schedule, derived key, or any cryptographic
549 /// state is omitted to prevent accidental key exposure through:
550 /// - Debug logs (`println!("{:?}", encryptor)`)
551 /// - Error messages that capture encryptor state
552 /// - Crash dumps or panic backtraces
553 /// - Debug tooling or profilers
554 ///
555 /// # Security Rationale
556 ///
557 /// Exposing key material in debug output is a serious security vulnerability:
558 /// - **Log Files**: Logs are often stored persistently and may be less protected than
559 /// memory (world-readable files, centralized logging systems).
560 /// - **Error Reporting**: Automatic error reporting tools might transmit debug output to
561 /// external services.
562 /// - **Debugging Sessions**: Developers might share debug output without realizing it
563 /// contains sensitive data.
564 ///
565 /// By redacting key material, this implementation follows the principle of **secure by
566 /// default**: keys cannot leak via debug formatting even if developers mistakenly log
567 /// encryptor instances.
568 ///
569 /// # Usage Notes
570 ///
571 /// - **Not for Parsing**: The debug output is for human inspection only and must not be
572 /// parsed programmatically. The format may change without notice.
573 /// - **Not for Identification**: The output does not include key fingerprints, IDs, or any
574 /// way to distinguish between different encryptor instances. Use separate metadata if
575 /// encryptor identification is required.
576 /// - **Performance**: Allocates a small string for formatting but does not access the
577 /// cipher state (zero cryptographic overhead).
578 ///
579 /// # Examples
580 ///
581 /// ```rust
582 /// use hexz_core::algo::encryption::AesGcmEncryptor;
583 ///
584 /// let encryptor = AesGcmEncryptor::new(b"secret_password", b"salt12345678salt", 100_000)?;
585 ///
586 /// // Safe to log: no key material is exposed
587 /// println!("{:?}", encryptor);
588 /// // Output: AesGcmEncryptor { cipher: "Aes256Gcm" }
589 ///
590 /// // Format works in error contexts
591 /// let result: Result<(), String> = Err(format!("Encryptor: {:?}", encryptor));
592 /// // Safe: error message does not contain keys
593 /// # Ok::<(), hexz_common::Error>(())
594 /// ```
595 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596 f.debug_struct("AesGcmEncryptor")
597 .field("cipher", &"Aes256Gcm")
598 .finish()
599 }
600}
601
602impl AesGcmEncryptor {
603 /// Derives an AES-256-GCM key from a password and initializes a new encryptor.
604 ///
605 /// This constructor uses PBKDF2-HMAC-SHA256 to derive a 256-bit AES key from the provided
606 /// password and salt, then initializes an AES-256-GCM cipher with the derived key. The
607 /// resulting encryptor can be used to encrypt and decrypt snapshot blocks.
608 ///
609 /// # Parameters
610 ///
611 /// - `password`: Arbitrary-length byte slice containing the password or key material.
612 /// **Security**: Use high-entropy passwords (≥128 bits, ~20 random characters) to resist
613 /// brute-force attacks. Weak passwords undermine the security of PBKDF2.
614 ///
615 /// - `salt`: Byte slice containing the cryptographic salt (recommended: 16 bytes / 128 bits).
616 /// **Critical**: The salt must be:
617 /// - Randomly generated using a CSPRNG (e.g., `rand::thread_rng()`)
618 /// - Stored in the snapshot header for later decryption
619 /// - Unique per snapshot (prevents rainbow table attacks and key reuse)
620 /// The salt is **not secret** but must be preserved exactly; losing it makes decryption
621 /// impossible even with the correct password.
622 ///
623 /// - `iterations`: Number of PBKDF2 iterations (recommended: 600,000 per OWASP 2023).
624 /// **Tradeoff**: Higher iterations increase brute-force resistance but slow key derivation:
625 /// - 100,000 iterations: ~100ms, weak against GPU attacks
626 /// - 600,000 iterations: ~500ms, current recommended minimum
627 /// - 1,000,000 iterations: ~1s, strong protection
628 /// **Note**: Iteration count is stored in the snapshot header; decryption must use the
629 /// same count.
630 ///
631 /// # Returns
632 ///
633 /// A new `AesGcmEncryptor` instance ready to encrypt or decrypt blocks. The encryptor is
634 /// thread-safe and can be shared across threads via `Arc`.
635 ///
636 /// # Performance
637 ///
638 /// - **Key Derivation Time**: Proportional to `iterations`; ~500ms for 600,000 iterations
639 /// on a modern CPU. This is a one-time cost per encryptor creation.
640 /// - **Memory Allocation**: Temporary 32-byte stack buffer for the derived key (zeroed
641 /// after key expansion, though Rust does not guarantee secure wiping).
642 /// - **Subsequent Operations**: After construction, encryption/decryption are fast
643 /// (~3-5 GB/s throughput on AES-NI hardware).
644 ///
645 /// # Security Considerations
646 ///
647 /// ## Determinism
648 ///
649 /// PBKDF2 is deterministic: the same `(password, salt, iterations)` always produces the
650 /// same key. This is intentional and required for decrypting snapshots, but means:
651 /// - Key material is reproducible from the password (password compromise = key compromise)
652 /// - No forward secrecy: old snapshots remain decryptable if password is disclosed
653 ///
654 /// ## Parameter Storage
655 ///
656 /// The snapshot header must store:
657 /// - `salt`: Required for key derivation (not secret, but must be exact)
658 /// - `iterations`: Required for key derivation (not secret)
659 /// - Password: **NEVER** stored; must be provided by user on decryption
660 ///
661 /// ## Weak Parameters
662 ///
663 /// - **Low iterations** (<100,000): Vulnerable to brute-force on GPUs/ASICs
664 /// - **Weak password** (dictionary words, short, low entropy): Negates PBKDF2 protection
665 /// - **Reused salt**: Allows rainbow table attacks across snapshots
666 ///
667 /// # Panics
668 ///
669 /// This function does **not** panic under normal circumstances. The internal PBKDF2
670 /// call uses `expect()` on HMAC initialization, which is infallible for HMAC-SHA256
671 /// (HMAC accepts any key length). The panic would only occur if the `hmac` or `sha2`
672 /// crate has a critical bug.
673 ///
674 /// # Examples
675 ///
676 /// ## Secure Encryptor Creation
677 ///
678 /// ```rust
679 /// use hexz_core::algo::encryption::AesGcmEncryptor;
680 /// use rand::RngCore;
681 ///
682 /// // Generate cryptographically random salt
683 /// let mut salt = [0u8; 16];
684 /// rand::thread_rng().fill_bytes(&mut salt);
685 ///
686 /// // Get password from secure source (environment, user prompt, key file)
687 /// let password = "secure_passphrase";
688 ///
689 /// // Create encryptor with recommended parameters
690 /// let encryptor = AesGcmEncryptor::new(
691 /// password.as_bytes(),
692 /// &salt,
693 /// 100_000 // OWASP 2023 recommendation
694 /// )?;
695 ///
696 /// // Store salt in snapshot header for later use
697 /// // (iterations count should also be stored)
698 /// # Ok::<(), hexz_common::Error>(())
699 /// ```
700 ///
701 /// ## Reproducible Key Derivation (for Decryption)
702 ///
703 /// ```rust
704 /// use hexz_core::algo::encryption::AesGcmEncryptor;
705 ///
706 /// // Read parameters from snapshot header
707 /// let stored_salt: [u8; 16] = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
708 /// 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
709 /// let stored_iterations: u32 = 100_000; // From header
710 ///
711 /// // Prompt user for password
712 /// let password = "user_provided_password";
713 ///
714 /// // Derive same key as during encryption
715 /// let encryptor = AesGcmEncryptor::new(
716 /// password.as_bytes(),
717 /// &stored_salt,
718 /// stored_iterations
719 /// )?;
720 ///
721 /// // Encryptor can now decrypt blocks from the snapshot
722 /// # Ok::<(), hexz_common::Error>(())
723 /// ```
724 ///
725 /// ## Testing with Fast Parameters
726 ///
727 /// ```rust
728 /// use hexz_core::algo::encryption::AesGcmEncryptor;
729 ///
730 /// // For unit tests, use lower iterations to speed up test execution
731 /// // (DO NOT use in production)
732 /// let encryptor = AesGcmEncryptor::new(
733 /// b"test_password",
734 /// b"test_salt_16byte",
735 /// 100_000 // Minimum allowed; use 100_000 in production
736 /// )?;
737 /// # Ok::<(), hexz_common::Error>(())
738 /// ```
739 /// Minimum allowed PBKDF2 iteration count for production security.
740 const MIN_ITERATIONS: u32 = 100_000;
741
742 /// Minimum allowed salt length in bytes.
743 const MIN_SALT_LENGTH: usize = 8;
744
745 pub fn new(password: &[u8], salt: &[u8], iterations: u32) -> Result<Self> {
746 use zeroize::Zeroize;
747
748 if salt.len() < Self::MIN_SALT_LENGTH {
749 return Err(Error::Encryption(format!(
750 "Salt too short: {} bytes (minimum {})",
751 salt.len(),
752 Self::MIN_SALT_LENGTH,
753 )));
754 }
755 if iterations < Self::MIN_ITERATIONS {
756 return Err(Error::Encryption(format!(
757 "PBKDF2 iterations too low: {} (minimum {})",
758 iterations,
759 Self::MIN_ITERATIONS,
760 )));
761 }
762
763 let mut key = [0u8; AES_KEY_LENGTH];
764 pbkdf2::<Hmac<Sha256>>(password, salt, iterations, &mut key)
765 .map_err(|e| Error::Encryption(format!("Key derivation failed: {}", e)))?;
766 let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
767 key.zeroize();
768 Ok(Self { cipher })
769 }
770
771 /// Computes a deterministic 96-bit nonce from a block index for GCM mode.
772 ///
773 /// This internal function generates a unique nonce for each block by encoding the block
774 /// index into a 12-byte (96-bit) array. The nonce is used as the Initialization Vector (IV)
775 /// for AES-GCM encryption and must never be reused with the same key for different
776 /// plaintexts.
777 ///
778 /// # Nonce Construction
779 ///
780 /// The 96-bit nonce is structured as:
781 ///
782 /// ```text
783 /// Bytes: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
784 /// ├────────────┼──────────────────────────────────┤
785 /// │ Reserved │ Block Index (u64) │
786 /// │ (32 bits) │ (64 bits, big-endian) │
787 /// └────────────┴──────────────────────────────────┘
788 /// Values: 0x00000000 block_idx.to_be_bytes()
789 /// ```
790 ///
791 /// - **Bytes 0-3**: Reserved field (all zeros). Available for future extensions such as
792 /// snapshot versioning, algorithm identifiers, or secondary indices while maintaining
793 /// backward compatibility.
794 /// - **Bytes 4-11**: 64-bit block index in big-endian byte order. Supports 2^64 unique
795 /// blocks (impractical limit: ~1 ZB at 64 KiB blocks).
796 ///
797 /// # Parameters
798 ///
799 /// - `block_idx`: The logical block index within the snapshot (0 to u64::MAX-1). Each
800 /// index must be unique within a snapshot to ensure nonce uniqueness.
801 ///
802 /// # Returns
803 ///
804 /// A `GenericArray<u8, U12>` (12-byte array) suitable for use as a GCM nonce. The returned
805 /// nonce is deterministic: the same `block_idx` always produces the same nonce.
806 ///
807 /// # Security Rationale
808 ///
809 /// ## Why Deterministic Nonces?
810 ///
811 /// Unlike random nonces, deterministic nonces based on block indices:
812 /// - **Eliminate nonce storage**: No need to store nonces in snapshot metadata
813 /// - **Enable stateless decryption**: Decryption only requires the block index, not stored
814 /// nonce values
815 /// - **Guarantee uniqueness**: Block indices are inherently unique within a snapshot,
816 /// ensuring nonce uniqueness as long as blocks are not rewritten
817 ///
818 /// ## Security Requirements
819 ///
820 /// GCM security critically depends on **never reusing a (key, nonce) pair**. This
821 /// implementation ensures uniqueness by:
822 /// 1. Each block index is unique within a snapshot
823 /// 2. Each snapshot uses a unique (password, salt) combination, deriving a unique key
824 /// 3. Snapshots are immutable after creation (blocks are not rewritten with the same index)
825 ///
826 /// **Nonce Reuse Catastrophe**: Encrypting two different plaintexts with the same (key, nonce)
827 /// allows attackers to:
828 /// - Recover the GCM authentication key (GHASH subkey)
829 /// - Forge arbitrary authenticated ciphertexts
830 /// - Compute plaintext XOR for the two messages
831 ///
832 /// ## Why Big-Endian?
833 ///
834 /// Big-endian encoding is used for:
835 /// - **Standard compliance**: NIST SP 800-38D recommends big-endian for counter fields
836 /// - **Lexicographic ordering**: Nonces sort in the same order as block indices
837 /// - **Interoperability**: Big-endian is the standard network byte order
838 ///
839 /// # Implementation Notes
840 ///
841 /// - **Pure Function**: This function has no side effects and does not modify the cipher
842 /// state. It can be called concurrently from multiple threads.
843 /// - **Zero Allocation**: Constructs the nonce on the stack; no heap allocations.
844 /// - **Constant Time**: Executes in constant time (no data-dependent branches), though this
845 /// is not critical for nonce generation (nonces are not secret).
846 ///
847 /// # Examples
848 ///
849 /// ```rust
850 /// # use hexz_core::algo::encryption::AesGcmEncryptor;
851 /// # let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
852 /// // Internal usage (not directly callable, but conceptually):
853 /// // let nonce = encryptor.generate_nonce(42);
854 /// // Result: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A]
855 /// // └─── Reserved (4 bytes) ──┘ └──────── Block Index 42 (8 bytes) ──────┘
856 /// # Ok::<(), hexz_common::Error>(())
857 /// ```
858 fn generate_nonce(&self, block_idx: u64) -> GenericArray<u8, U12> {
859 let mut bytes = [0u8; AES_NONCE_LENGTH];
860 bytes[4..].copy_from_slice(&block_idx.to_be_bytes());
861 *GenericArray::from_slice(&bytes)
862 }
863}
864
865impl Encryptor for AesGcmEncryptor {
866 /// Encrypts and authenticates a block of data using AES-256-GCM.
867 ///
868 /// This method encrypts the input data and appends a 128-bit authentication tag, producing
869 /// a self-contained ciphertext that can be verified during decryption. The encryption is
870 /// bound to the `block_idx` via the nonce, ensuring that blocks cannot be reordered,
871 /// duplicated, or swapped without detection.
872 ///
873 /// # Parameters
874 ///
875 /// - `data`: The plaintext data to encrypt (typically compressed block data). Can be any
876 /// length from 0 bytes to several megabytes. Empty input is valid and produces a
877 /// ciphertext containing only the 16-byte authentication tag.
878 ///
879 /// - `block_idx`: The logical block index within the snapshot (0 to 2^64-1). This index is
880 /// encoded into the nonce to ensure each block uses a unique (key, nonce) pair.
881 /// **Critical**: Each index must be used at most once per key; reusing an index with
882 /// different plaintext catastrophically breaks GCM security.
883 ///
884 /// # Returns
885 ///
886 /// - `Ok(Vec<u8>)`: Ciphertext with appended authentication tag. Length is `data.len() + 16`.
887 /// The ciphertext can be stored and later decrypted using the same `block_idx`.
888 ///
889 /// - `Err(Error::Encryption)`: Encryption failed (extremely rare; typically indicates
890 /// a bug in the underlying `aes-gcm` crate or hardware acceleration failure).
891 ///
892 /// # Errors
893 ///
894 /// This method returns an error in the following cases:
895 ///
896 /// - **Encryption Failure**: The underlying GCM encryption operation failed. This is
897 /// exceptionally rare and typically indicates:
898 /// - Memory allocation failure (out-of-memory during ciphertext allocation)
899 /// - Hardware acceleration failure (AES-NI instruction fault)
900 /// - Critical bug in the `aes-gcm` crate
901 ///
902 /// In practice, encryption errors are almost never encountered under normal operation.
903 ///
904 /// # Performance
905 ///
906 /// - **Throughput**: ~3-5 GB/s on modern x86-64 CPUs with AES-NI hardware acceleration.
907 /// Without hardware acceleration, throughput drops to ~100-200 MB/s (software AES).
908 /// - **Latency**: <1 µs for typical 64 KiB blocks; ~10-20 ns/byte amortized overhead.
909 /// - **Memory**: Allocates `data.len() + 16` bytes for the ciphertext.
910 /// - **CPU**: Primarily limited by AES throughput; benefits from pipelined AES-NI instructions.
911 ///
912 /// # Security Guarantees
913 ///
914 /// ## Confidentiality
915 ///
916 /// The ciphertext reveals no information about the plaintext (beyond its length) to
917 /// attackers without the key, assuming:
918 /// - The key remains secret
919 /// - Nonces are never reused with the same key
920 ///
921 /// ## Authenticity
922 ///
923 /// The authentication tag ensures:
924 /// - **Tamper Detection**: Any modification to the ciphertext or nonce is detected during
925 /// decryption with probability 1 - 2^-128 (effectively certain).
926 /// - **No Forgery**: Attackers cannot create valid ciphertexts without the key.
927 /// - **Block Binding**: The ciphertext is bound to `block_idx`; attempting to decrypt with
928 /// a different index fails authentication.
929 ///
930 /// ## Nonce Uniqueness
931 ///
932 /// **CRITICAL SECURITY REQUIREMENT**: Never encrypt two different plaintexts with the same
933 /// `block_idx` under the same key. Nonce reuse allows attackers to:
934 /// - Recover the GCM authentication key
935 /// - Forge arbitrary authenticated ciphertexts
936 /// - Compute plaintext XOR for messages encrypted with the same nonce
937 ///
938 /// This implementation ensures uniqueness by:
939 /// - Using unique block indices within each snapshot
940 /// - Deriving unique keys per snapshot (via different salts or passwords)
941 /// - Snapshot immutability (blocks are not rewritten after creation)
942 ///
943 /// # Ciphertext Format
944 ///
945 /// The returned ciphertext has the following structure:
946 ///
947 /// ```text
948 /// ┌─────────────────────────────────┬──────────────────────┐
949 /// │ Encrypted Data │ Authentication Tag │
950 /// │ (same length as plaintext) │ (16 bytes) │
951 /// └─────────────────────────────────┴──────────────────────┘
952 /// 0 data.len() data.len()+16
953 /// ```
954 ///
955 /// - **Encrypted Data**: AES-CTR mode ciphertext (same length as plaintext)
956 /// - **Authentication Tag**: 128-bit GHASH polynomial evaluation over ciphertext and nonce
957 ///
958 /// # Examples
959 ///
960 /// ## Basic Encryption
961 ///
962 /// ```rust
963 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
964 ///
965 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
966 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
967 ///
968 /// // Encrypt a block (e.g., compressed data)
969 /// let plaintext = b"Compressed block data from zstd";
970 /// let block_idx = 42;
971 /// let ciphertext = encryptor.encrypt(plaintext, block_idx)?;
972 ///
973 /// // Ciphertext is 16 bytes longer (authentication tag)
974 /// assert_eq!(ciphertext.len(), plaintext.len() + 16);
975 /// # Ok(())
976 /// # }
977 /// ```
978 ///
979 /// ## Encrypting Multiple Blocks
980 ///
981 /// ```rust
982 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
983 ///
984 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
985 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
986 ///
987 /// let blocks = vec![b"block0", b"block1", b"block2"];
988 /// let mut encrypted = Vec::new();
989 ///
990 /// for (idx, block) in blocks.iter().enumerate() {
991 /// let ciphertext = encryptor.encrypt(*block, idx as u64)?;
992 /// encrypted.push(ciphertext);
993 /// }
994 ///
995 /// // All ciphertexts are unique (even if plaintexts were identical)
996 /// # Ok(())
997 /// # }
998 /// ```
999 ///
1000 /// ## Empty Block Encryption
1001 ///
1002 /// ```rust
1003 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
1004 ///
1005 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1006 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
1007 ///
1008 /// // Empty input is valid
1009 /// let ciphertext = encryptor.encrypt(b"", 0)?;
1010 ///
1011 /// // Ciphertext contains only the authentication tag
1012 /// assert_eq!(ciphertext.len(), 16);
1013 /// # Ok(())
1014 /// # }
1015 /// ```
1016 fn encrypt(&self, data: &[u8], block_idx: u64) -> Result<Vec<u8>> {
1017 let nonce = self.generate_nonce(block_idx);
1018 self.cipher
1019 .encrypt(&nonce, data)
1020 .map_err(|e| Error::Encryption(e.to_string()))
1021 }
1022
1023 /// Decrypts and verifies a block of AES-256-GCM ciphertext with authentication.
1024 ///
1025 /// This method decrypts the input ciphertext and verifies the appended authentication tag,
1026 /// ensuring that the data has not been tampered with and that the correct key and block
1027 /// index are being used. Authentication failures abort decryption and return an error,
1028 /// preventing silent data corruption.
1029 ///
1030 /// # Parameters
1031 ///
1032 /// - `data`: The ciphertext to decrypt (output from `encrypt()`). Must include the 16-byte
1033 /// authentication tag appended to the encrypted data. Minimum length is 16 bytes (empty
1034 /// plaintext + tag); shorter inputs will fail authentication.
1035 ///
1036 /// - `block_idx`: The logical block index within the snapshot, **exactly matching** the
1037 /// index used during encryption. Using a different index will cause authentication
1038 /// failure even if the key and ciphertext are correct.
1039 ///
1040 /// # Returns
1041 ///
1042 /// - `Ok(Vec<u8>)`: Successfully decrypted plaintext. Length is `data.len() - 16`
1043 /// (ciphertext length minus authentication tag). The plaintext matches the original
1044 /// input to `encrypt()`.
1045 ///
1046 /// - `Err(Error::Encryption)`: Decryption or authentication failed. This occurs when:
1047 /// - **Wrong Key**: The encryptor was created with a different password, salt, or
1048 /// iteration count than was used for encryption.
1049 /// - **Wrong Block Index**: The `block_idx` does not match the index used during
1050 /// encryption (nonce mismatch).
1051 /// - **Corrupted Ciphertext**: Any byte in the ciphertext or tag was modified (bitflip,
1052 /// truncation, etc.).
1053 /// - **Truncated Data**: The ciphertext is too short (missing tag or partial data).
1054 ///
1055 /// # Errors
1056 ///
1057 /// This method returns `Err(Error::Encryption)` when authentication fails. The error
1058 /// message is intentionally generic ("encryption error") and does **not** distinguish between:
1059 ///
1060 /// - **Wrong Password/Key**: Incorrect key derivation parameters
1061 /// - **Corruption**: Bitflips, truncation, or modification of ciphertext
1062 /// - **Wrong Index**: Block index mismatch (nonce mismatch)
1063 /// - **Forgery Attempt**: Attacker-crafted ciphertext
1064 ///
1065 /// **Security Rationale**: Distinguishing error causes could leak information to attackers
1066 /// (e.g., whether a password is correct but index is wrong). All authentication failures
1067 /// are treated identically.
1068 ///
1069 /// # Performance
1070 ///
1071 /// - **Throughput**: ~3-5 GB/s on modern x86-64 CPUs with AES-NI and PCLMULQDQ hardware
1072 /// acceleration. Without hardware support, throughput drops to ~100-200 MB/s.
1073 /// - **Latency**: <1 µs for typical 64 KiB blocks; comparable to encryption (GCM is
1074 /// symmetric).
1075 /// - **Memory**: Allocates `data.len() - 16` bytes for the plaintext.
1076 /// - **Authentication Overhead**: GHASH verification adds ~10% overhead compared to
1077 /// unauthenticated decryption.
1078 ///
1079 /// # Security Guarantees
1080 ///
1081 /// ## Authentication
1082 ///
1083 /// Decryption succeeds **only if**:
1084 /// - The ciphertext was produced by `encrypt()` with the same key
1085 /// - The same `block_idx` is provided
1086 /// - No bits in the ciphertext or tag have been modified
1087 ///
1088 /// Authentication failures are detected with probability 1 - 2^-128 (effectively certain
1089 /// for 128-bit tags). There is **no risk of silent corruption**; any error is surfaced
1090 /// immediately.
1091 ///
1092 /// ## Constant-Time Verification
1093 ///
1094 /// Tag comparison is performed in constant time (independent of tag contents) to prevent
1095 /// timing attacks that could leak information about the authentication key. However, the
1096 /// overall decryption process has data-dependent timing (e.g., cache access patterns), so
1097 /// this implementation does not protect against sophisticated side-channel attacks.
1098 ///
1099 /// ## No Partial Decryption
1100 ///
1101 /// If authentication fails, **no plaintext is returned**. The decryption operation is atomic:
1102 /// either the entire plaintext is returned (authenticated), or an error is returned (no data).
1103 ///
1104 /// # Failure Modes
1105 ///
1106 /// ## Wrong Password or Key Parameters
1107 ///
1108 /// ```rust
1109 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
1110 ///
1111 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1112 /// let enc1 = AesGcmEncryptor::new(b"password1", b"salt12345678salt", 100_000)?;
1113 /// let enc2 = AesGcmEncryptor::new(b"password2", b"salt12345678salt", 100_000)?;
1114 ///
1115 /// let ciphertext = enc1.encrypt(b"data", 0)?;
1116 ///
1117 /// // Wrong password -> authentication fails
1118 /// assert!(enc2.decrypt(&ciphertext, 0).is_err());
1119 /// # Ok(())
1120 /// # }
1121 /// ```
1122 ///
1123 /// ## Wrong Block Index (Nonce Mismatch)
1124 ///
1125 /// ```rust
1126 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
1127 ///
1128 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1129 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
1130 ///
1131 /// let ciphertext = encryptor.encrypt(b"data", 42)?;
1132 ///
1133 /// // Wrong index -> authentication fails (nonce mismatch)
1134 /// assert!(encryptor.decrypt(&ciphertext, 99).is_err());
1135 /// # Ok(())
1136 /// # }
1137 /// ```
1138 ///
1139 /// ## Corrupted Ciphertext
1140 ///
1141 /// ```rust
1142 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
1143 ///
1144 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1145 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
1146 ///
1147 /// let mut ciphertext = encryptor.encrypt(b"data", 0)?;
1148 ///
1149 /// // Flip a bit (simulate corruption)
1150 /// ciphertext[5] ^= 0xFF;
1151 ///
1152 /// // Corruption detected -> authentication fails
1153 /// assert!(encryptor.decrypt(&ciphertext, 0).is_err());
1154 /// # Ok(())
1155 /// # }
1156 /// ```
1157 ///
1158 /// # Examples
1159 ///
1160 /// ## Basic Decryption
1161 ///
1162 /// ```rust
1163 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
1164 ///
1165 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1166 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
1167 ///
1168 /// // Encrypt a block
1169 /// let plaintext = b"Original data";
1170 /// let ciphertext = encryptor.encrypt(plaintext, 0)?;
1171 ///
1172 /// // Decrypt the block
1173 /// let decrypted = encryptor.decrypt(&ciphertext, 0)?;
1174 ///
1175 /// assert_eq!(decrypted, plaintext);
1176 /// # Ok(())
1177 /// # }
1178 /// ```
1179 ///
1180 /// ## Handling Decryption Failures
1181 ///
1182 /// ```rust
1183 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
1184 ///
1185 /// # fn example() {
1186 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000).unwrap();
1187 /// let ciphertext = encryptor.encrypt(b"data", 0).unwrap();
1188 ///
1189 /// match encryptor.decrypt(&ciphertext, 999) {
1190 /// Ok(plaintext) => {
1191 /// // Success: use plaintext
1192 /// },
1193 /// Err(e) => {
1194 /// // Authentication failed: could be wrong key, wrong index, or corruption
1195 /// eprintln!("Decryption failed: {}", e);
1196 /// // Abort block read; do not proceed with corrupted data
1197 /// }
1198 /// }
1199 /// # }
1200 /// ```
1201 ///
1202 /// ## Decrypting Multiple Blocks
1203 ///
1204 /// ```rust
1205 /// use hexz_core::algo::encryption::{Encryptor, AesGcmEncryptor};
1206 ///
1207 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1208 /// let encryptor = AesGcmEncryptor::new(b"password", b"salt12345678salt", 100_000)?;
1209 ///
1210 /// // Encrypt blocks
1211 /// let blocks = vec![b"block0", b"block1", b"block2"];
1212 /// let ciphertexts: Vec<_> = blocks.iter()
1213 /// .enumerate()
1214 /// .map(|(i, b)| encryptor.encrypt(*b, i as u64).unwrap())
1215 /// .collect();
1216 ///
1217 /// // Decrypt blocks
1218 /// for (idx, ciphertext) in ciphertexts.iter().enumerate() {
1219 /// let plaintext = encryptor.decrypt(ciphertext, idx as u64)?;
1220 /// assert_eq!(plaintext, blocks[idx]);
1221 /// }
1222 /// # Ok(())
1223 /// # }
1224 /// ```
1225 fn decrypt(&self, data: &[u8], block_idx: u64) -> Result<Vec<u8>> {
1226 let nonce = self.generate_nonce(block_idx);
1227 self.cipher
1228 .decrypt(&nonce, data)
1229 .map_err(|e| Error::Encryption(e.to_string()))
1230 }
1231
1232 fn encrypt_into(&self, data: &[u8], block_idx: u64, out: &mut Vec<u8>) -> Result<()> {
1233 use aes_gcm::aead::AeadInPlace;
1234
1235 let nonce = self.generate_nonce(block_idx);
1236 out.clear();
1237 out.extend_from_slice(data);
1238 self.cipher
1239 .encrypt_in_place(&nonce, b"", out)
1240 .map_err(|e| Error::Encryption(e.to_string()))
1241 }
1242
1243 fn decrypt_into(&self, data: &[u8], block_idx: u64, out: &mut Vec<u8>) -> Result<()> {
1244 use aes_gcm::aead::AeadInPlace;
1245
1246 let nonce = self.generate_nonce(block_idx);
1247 out.clear();
1248 out.extend_from_slice(data);
1249 self.cipher
1250 .decrypt_in_place(&nonce, b"", out)
1251 .map_err(|e| Error::Encryption(e.to_string()))
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258
1259 #[test]
1260 fn test_basic_encrypt_decrypt() {
1261 let encryptor =
1262 AesGcmEncryptor::new(b"test_password", b"salt_16_bytes___", 100_000).unwrap();
1263
1264 let plaintext = b"Hello, World!";
1265 let ciphertext = encryptor.encrypt(plaintext, 0).expect("Encryption failed");
1266
1267 // Ciphertext should be 16 bytes longer (auth tag)
1268 assert_eq!(ciphertext.len(), plaintext.len() + 16);
1269
1270 // Decrypt should recover original plaintext
1271 let decrypted = encryptor
1272 .decrypt(&ciphertext, 0)
1273 .expect("Decryption failed");
1274 assert_eq!(decrypted.as_slice(), plaintext);
1275 }
1276
1277 #[test]
1278 fn test_wrong_password_fails() {
1279 let enc1 = AesGcmEncryptor::new(b"password1", b"salt_16_bytes___", 100_000).unwrap();
1280 let enc2 = AesGcmEncryptor::new(b"password2", b"salt_16_bytes___", 100_000).unwrap();
1281
1282 let ciphertext = enc1.encrypt(b"secret data", 0).expect("Encryption failed");
1283
1284 // Wrong password should fail authentication
1285 assert!(enc2.decrypt(&ciphertext, 0).is_err());
1286 }
1287
1288 #[test]
1289 fn test_wrong_salt_fails() {
1290 let enc1 = AesGcmEncryptor::new(b"password", b"salt1___16bytes_", 100_000).unwrap();
1291 let enc2 = AesGcmEncryptor::new(b"password", b"salt2___16bytes_", 100_000).unwrap();
1292
1293 let ciphertext = enc1.encrypt(b"secret data", 0).expect("Encryption failed");
1294
1295 // Wrong salt should fail authentication
1296 assert!(enc2.decrypt(&ciphertext, 0).is_err());
1297 }
1298
1299 #[test]
1300 fn test_wrong_iterations_fails() {
1301 let enc1 = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1302 let enc2 = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 200_000).unwrap();
1303
1304 let ciphertext = enc1.encrypt(b"secret data", 0).expect("Encryption failed");
1305
1306 // Wrong iteration count should fail authentication
1307 assert!(enc2.decrypt(&ciphertext, 0).is_err());
1308 }
1309
1310 #[test]
1311 fn test_wrong_block_index_fails() {
1312 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1313
1314 let ciphertext = encryptor.encrypt(b"data", 42).expect("Encryption failed");
1315
1316 // Wrong block index should fail authentication (nonce mismatch)
1317 assert!(encryptor.decrypt(&ciphertext, 99).is_err());
1318 }
1319
1320 #[test]
1321 fn test_corrupted_ciphertext_fails() {
1322 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1323
1324 let mut ciphertext = encryptor
1325 .encrypt(b"important data", 0)
1326 .expect("Encryption failed");
1327
1328 // Corrupt a byte in the ciphertext
1329 ciphertext[5] ^= 0xFF;
1330
1331 // Corruption should be detected
1332 assert!(encryptor.decrypt(&ciphertext, 0).is_err());
1333 }
1334
1335 #[test]
1336 fn test_corrupted_tag_fails() {
1337 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1338
1339 let mut ciphertext = encryptor
1340 .encrypt(b"important data", 0)
1341 .expect("Encryption failed");
1342
1343 // Corrupt a byte in the authentication tag (last 16 bytes)
1344 let len = ciphertext.len();
1345 ciphertext[len - 1] ^= 0xFF;
1346
1347 // Tag corruption should be detected
1348 assert!(encryptor.decrypt(&ciphertext, 0).is_err());
1349 }
1350
1351 #[test]
1352 fn test_empty_data_encryption() {
1353 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1354
1355 let ciphertext = encryptor.encrypt(b"", 0).expect("Encryption failed");
1356
1357 // Empty plaintext produces 16-byte ciphertext (only auth tag)
1358 assert_eq!(ciphertext.len(), 16);
1359
1360 // Should decrypt back to empty
1361 let decrypted = encryptor
1362 .decrypt(&ciphertext, 0)
1363 .expect("Decryption failed");
1364 assert_eq!(decrypted.len(), 0);
1365 }
1366
1367 #[test]
1368 fn test_large_data_encryption() {
1369 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1370
1371 // Encrypt 1MB of data
1372 let large_data = vec![0xAB; 1024 * 1024];
1373 let ciphertext = encryptor
1374 .encrypt(&large_data, 0)
1375 .expect("Encryption failed");
1376
1377 // Verify size
1378 assert_eq!(ciphertext.len(), large_data.len() + 16);
1379
1380 // Decrypt and verify
1381 let decrypted = encryptor
1382 .decrypt(&ciphertext, 0)
1383 .expect("Decryption failed");
1384 assert_eq!(decrypted, large_data);
1385 }
1386
1387 #[test]
1388 fn test_different_block_indices_produce_different_ciphertexts() {
1389 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1390
1391 let plaintext = b"Same plaintext for both blocks";
1392
1393 // Encrypt same plaintext with different indices
1394 let ct1 = encryptor.encrypt(plaintext, 0).expect("Encryption failed");
1395 let ct2 = encryptor.encrypt(plaintext, 1).expect("Encryption failed");
1396
1397 // Ciphertexts should be different (different nonces)
1398 assert_ne!(ct1, ct2);
1399
1400 // But both should decrypt correctly with their respective indices
1401 let pt1 = encryptor.decrypt(&ct1, 0).expect("Decryption failed");
1402 let pt2 = encryptor.decrypt(&ct2, 1).expect("Decryption failed");
1403
1404 assert_eq!(pt1.as_slice(), plaintext);
1405 assert_eq!(pt2.as_slice(), plaintext);
1406 }
1407
1408 #[test]
1409 fn test_multiple_blocks_encryption() {
1410 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1411
1412 let blocks = [b"block0", b"block1", b"block2", b"block3"];
1413 let mut ciphertexts = Vec::new();
1414
1415 // Encrypt all blocks
1416 for (idx, block) in blocks.iter().enumerate() {
1417 let ct = encryptor
1418 .encrypt(*block, idx as u64)
1419 .expect("Encryption failed");
1420 ciphertexts.push(ct);
1421 }
1422
1423 // Decrypt all blocks
1424 for (idx, ct) in ciphertexts.iter().enumerate() {
1425 let pt = encryptor
1426 .decrypt(ct, idx as u64)
1427 .expect("Decryption failed");
1428 assert_eq!(pt.as_slice(), blocks[idx]);
1429 }
1430 }
1431
1432 #[test]
1433 fn test_deterministic_encryption() {
1434 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1435
1436 let plaintext = b"deterministic test";
1437 let block_idx = 42;
1438
1439 // Encrypt the same data twice with same index
1440 let ct1 = encryptor
1441 .encrypt(plaintext, block_idx)
1442 .expect("Encryption failed");
1443 let ct2 = encryptor
1444 .encrypt(plaintext, block_idx)
1445 .expect("Encryption failed");
1446
1447 // Should produce identical ciphertexts (deterministic nonces)
1448 assert_eq!(ct1, ct2);
1449 }
1450
1451 #[test]
1452 fn test_debug_does_not_leak_keys() {
1453 let encryptor =
1454 AesGcmEncryptor::new(b"secret_password", b"salt_16_bytes___", 100_000).unwrap();
1455
1456 let debug_str = format!("{:?}", encryptor);
1457
1458 // Debug output should not contain the password or key material
1459 assert!(!debug_str.contains("secret_password"));
1460 assert!(debug_str.contains("AesGcmEncryptor"));
1461 assert!(debug_str.contains("Aes256Gcm"));
1462 }
1463
1464 #[test]
1465 fn test_thread_safe_concurrent_encryption() {
1466 use std::sync::Arc;
1467 use std::thread;
1468
1469 let encryptor =
1470 Arc::new(AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap());
1471
1472 let mut handles = Vec::new();
1473
1474 // Spawn multiple threads that encrypt concurrently
1475 for i in 0..4 {
1476 let enc = Arc::clone(&encryptor);
1477 let handle = thread::spawn(move || {
1478 let data = format!("Thread {} data", i).into_bytes();
1479 enc.encrypt(&data, i as u64).unwrap()
1480 });
1481 handles.push(handle);
1482 }
1483
1484 // Collect results
1485 let ciphertexts: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1486
1487 // Verify all encryptions succeeded
1488 assert_eq!(ciphertexts.len(), 4);
1489
1490 // Verify each can be decrypted
1491 for (i, ct) in ciphertexts.iter().enumerate() {
1492 let expected = format!("Thread {} data", i).into_bytes();
1493 let decrypted = encryptor.decrypt(ct, i as u64).expect("Decryption failed");
1494 assert_eq!(decrypted, expected);
1495 }
1496 }
1497
1498 #[test]
1499 fn test_nonce_generation_is_unique_per_index() {
1500 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1501
1502 let plaintext = b"same plaintext";
1503
1504 // Collect ciphertexts for different indices
1505 let mut ciphertexts = Vec::new();
1506 for i in 0..10 {
1507 let ct = encryptor.encrypt(plaintext, i).expect("Encryption failed");
1508 ciphertexts.push(ct);
1509 }
1510
1511 // All ciphertexts should be different (different nonces)
1512 for i in 0..ciphertexts.len() {
1513 for j in (i + 1)..ciphertexts.len() {
1514 assert_ne!(
1515 ciphertexts[i], ciphertexts[j],
1516 "Ciphertexts at indices {} and {} should be different",
1517 i, j
1518 );
1519 }
1520 }
1521 }
1522
1523 #[test]
1524 fn test_truncated_ciphertext_fails() {
1525 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1526
1527 let ciphertext = encryptor.encrypt(b"data", 0).expect("Encryption failed");
1528
1529 // Truncate the ciphertext (remove part of tag)
1530 let truncated = &ciphertext[..ciphertext.len() - 5];
1531
1532 // Should fail authentication
1533 assert!(encryptor.decrypt(truncated, 0).is_err());
1534 }
1535
1536 #[test]
1537 fn test_encryption_with_different_passwords() {
1538 let enc1 = AesGcmEncryptor::new(b"password1", b"salt_16_bytes___", 100_000).unwrap();
1539 let enc2 = AesGcmEncryptor::new(b"password2", b"salt_16_bytes___", 100_000).unwrap();
1540
1541 let plaintext = b"test data";
1542
1543 // Encrypt same data with different passwords
1544 let ct1 = enc1.encrypt(plaintext, 0).expect("Encryption failed");
1545 let ct2 = enc2.encrypt(plaintext, 0).expect("Encryption failed");
1546
1547 // Ciphertexts should be different (different keys)
1548 assert_ne!(ct1, ct2);
1549
1550 // Each can decrypt with its own key
1551 assert_eq!(enc1.decrypt(&ct1, 0).unwrap().as_slice(), plaintext);
1552 assert_eq!(enc2.decrypt(&ct2, 0).unwrap().as_slice(), plaintext);
1553
1554 // But not with the other key
1555 assert!(enc1.decrypt(&ct2, 0).is_err());
1556 assert!(enc2.decrypt(&ct1, 0).is_err());
1557 }
1558
1559 #[test]
1560 fn test_very_short_data() {
1561 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1562
1563 // Single byte
1564 let plaintext = b"X";
1565 let ciphertext = encryptor.encrypt(plaintext, 0).expect("Encryption failed");
1566
1567 assert_eq!(ciphertext.len(), 17); // 1 byte + 16 byte tag
1568
1569 let decrypted = encryptor
1570 .decrypt(&ciphertext, 0)
1571 .expect("Decryption failed");
1572 assert_eq!(decrypted.as_slice(), plaintext);
1573 }
1574
1575 #[test]
1576 fn test_high_block_indices() {
1577 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1578
1579 // Test with maximum u64 value
1580 let max_idx = u64::MAX;
1581 let plaintext = b"data at max index";
1582
1583 let ciphertext = encryptor
1584 .encrypt(plaintext, max_idx)
1585 .expect("Encryption failed");
1586 let decrypted = encryptor
1587 .decrypt(&ciphertext, max_idx)
1588 .expect("Decryption failed");
1589
1590 assert_eq!(decrypted.as_slice(), plaintext);
1591 }
1592
1593 #[test]
1594 fn test_encryption_does_not_modify_input() {
1595 let encryptor = AesGcmEncryptor::new(b"password", b"salt_16_bytes___", 100_000).unwrap();
1596
1597 let plaintext = b"original data";
1598 let original = plaintext.to_vec();
1599
1600 let _ciphertext = encryptor.encrypt(plaintext, 0).expect("Encryption failed");
1601
1602 // Input should remain unchanged
1603 assert_eq!(plaintext, original.as_slice());
1604 }
1605
1606 #[test]
1607 fn test_different_salts_produce_different_keys() {
1608 let enc1 = AesGcmEncryptor::new(b"password", b"salt1___16bytes_", 100_000).unwrap();
1609 let enc2 = AesGcmEncryptor::new(b"password", b"salt2___16bytes_", 100_000).unwrap();
1610
1611 let plaintext = b"test data";
1612
1613 // Encrypt same plaintext with same index but different salts
1614 let ct1 = enc1.encrypt(plaintext, 0).expect("Encryption failed");
1615 let ct2 = enc2.encrypt(plaintext, 0).expect("Encryption failed");
1616
1617 // Should produce different ciphertexts
1618 assert_ne!(ct1, ct2);
1619 }
1620}