Skip to main content

hexz_common/
crypto.rs

1//! Cryptographic utilities for Hexz snapshot encryption.
2//!
3//! Defines key-derivation parameters (PBKDF2 salt and iteration count) used
4//! when creating or opening encrypted snapshots. These parameters are
5//! serialized into snapshot metadata so that the same password reproduces
6//! the same key on restore.
7
8use crate::constants::{PBKDF2_ITERATIONS, SALT_SIZE};
9use serde::{Deserialize, Serialize};
10
11/// Parameters for deriving an encryption key from a user-supplied secret.
12///
13/// **Architectural intent:** Encapsulates the tunable inputs for PBKDF2-based
14/// key derivation so that snapshot metadata fully describes how to reproduce
15/// the encryption key on restore.
16///
17/// **Constraints:** `salt` must be unique per snapshot to avoid key reuse, and
18/// `iterations` is chosen to be intentionally expensive (hundreds of thousands
19/// of rounds) to raise the cost of offline brute-force attacks while remaining
20/// acceptable for interactive CLI usage.
21///
22/// **Side effects:** Instances produced via `Default` consume randomness from
23/// the process RNG and implicitly fix the work factor at the compile-time
24/// constant embedded in this type.
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub struct KeyDerivationParams {
27    pub salt: [u8; SALT_SIZE],
28    pub iterations: u32,
29}
30
31impl Default for KeyDerivationParams {
32    /// Generates key-derivation parameters for a new snapshot.
33    ///
34    /// **Architectural intent:** Produces a fresh random salt and a stable
35    /// iteration count that all writers and readers agree on, so that keys can
36    /// be recomputed deterministically from the password and stored metadata.
37    ///
38    /// **Constraints:** The salt is 128 bits and sampled uniformly from the
39    /// system RNG; the iteration count is fixed to a value calibrated for this
40    /// application and must be kept in sync with password prompts.
41    ///
42    /// **Side effects:** Pulls entropy from `rand::thread_rng` and performs no
43    /// I/O; increasing the iteration count will linearly increase CPU cost for
44    /// both snapshot creation and decryption.
45    fn default() -> Self {
46        let mut salt = [0u8; SALT_SIZE];
47        rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut salt);
48        Self {
49            salt,
50            iterations: PBKDF2_ITERATIONS,
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_key_derivation_params_default() {
61        let params = KeyDerivationParams::default();
62
63        // Verify iterations match the constant
64        assert_eq!(params.iterations, PBKDF2_ITERATIONS);
65
66        // Verify salt has the correct size
67        assert_eq!(params.salt.len(), SALT_SIZE);
68    }
69
70    #[test]
71    fn test_default_salt_is_random() {
72        // Generate two separate instances
73        let params1 = KeyDerivationParams::default();
74        let params2 = KeyDerivationParams::default();
75
76        // Salts should be different (extremely unlikely to be equal if random)
77        assert_ne!(params1.salt, params2.salt);
78
79        // Verify salt is not all zeros (would indicate RNG failure)
80        assert_ne!(params1.salt, [0u8; SALT_SIZE]);
81        assert_ne!(params2.salt, [0u8; SALT_SIZE]);
82    }
83
84    #[test]
85    fn test_key_derivation_params_clone() {
86        let params = KeyDerivationParams::default();
87        let cloned = params.clone();
88
89        // Cloned instance should be equal
90        assert_eq!(params, cloned);
91        assert_eq!(params.salt, cloned.salt);
92        assert_eq!(params.iterations, cloned.iterations);
93    }
94
95    #[test]
96    fn test_key_derivation_params_equality() {
97        let params1 = KeyDerivationParams {
98            salt: [42u8; SALT_SIZE],
99            iterations: 100000,
100        };
101        let params2 = KeyDerivationParams {
102            salt: [42u8; SALT_SIZE],
103            iterations: 100000,
104        };
105        let params3 = KeyDerivationParams {
106            salt: [43u8; SALT_SIZE],
107            iterations: 100000,
108        };
109
110        // Same values should be equal
111        assert_eq!(params1, params2);
112
113        // Different salt should not be equal
114        assert_ne!(params1, params3);
115    }
116
117    #[test]
118    fn test_key_derivation_params_serialization() {
119        let params = KeyDerivationParams {
120            salt: [123u8; SALT_SIZE],
121            iterations: 200000,
122        };
123
124        // Serialize to JSON
125        let json = serde_json::to_string(&params).expect("Serialization failed");
126
127        // Deserialize back
128        let deserialized: KeyDerivationParams =
129            serde_json::from_str(&json).expect("Deserialization failed");
130
131        // Should match original
132        assert_eq!(params, deserialized);
133        assert_eq!(params.salt, deserialized.salt);
134        assert_eq!(params.iterations, deserialized.iterations);
135    }
136
137    #[test]
138    fn test_key_derivation_params_debug() {
139        let params = KeyDerivationParams {
140            salt: [1u8; SALT_SIZE],
141            iterations: 150000,
142        };
143
144        // Debug output should contain key information
145        let debug_str = format!("{:?}", params);
146        assert!(debug_str.contains("KeyDerivationParams"));
147        assert!(debug_str.contains("salt"));
148        assert!(debug_str.contains("iterations"));
149    }
150
151    #[test]
152    fn test_iterations_constant_is_reasonable() {
153        // Verify PBKDF2_ITERATIONS is in a reasonable range
154        // Too low: insufficient security
155        // Too high: unusable in practice
156        #[allow(clippy::assertions_on_constants)]
157        {
158            assert!(
159                PBKDF2_ITERATIONS >= 100_000,
160                "Iterations too low for security"
161            );
162        }
163        #[allow(clippy::assertions_on_constants)]
164        {
165            assert!(
166                PBKDF2_ITERATIONS <= 10_000_000,
167                "Iterations too high for usability"
168            );
169        }
170    }
171
172    #[test]
173    fn test_salt_size_is_reasonable() {
174        // Verify SALT_SIZE is sufficient for security
175        // Minimum recommended: 128 bits (16 bytes)
176        #[allow(clippy::assertions_on_constants)]
177        {
178            assert!(SALT_SIZE >= 16, "Salt size too small for security");
179        }
180        #[allow(clippy::assertions_on_constants)]
181        {
182            assert!(SALT_SIZE <= 64, "Salt size unnecessarily large");
183        }
184    }
185
186    #[test]
187    fn test_manual_construction() {
188        let custom_salt = [99u8; SALT_SIZE];
189        let custom_iterations = 500000;
190
191        let params = KeyDerivationParams {
192            salt: custom_salt,
193            iterations: custom_iterations,
194        };
195
196        assert_eq!(params.salt, custom_salt);
197        assert_eq!(params.iterations, custom_iterations);
198    }
199
200    #[test]
201    fn test_different_defaults_have_different_salts() {
202        // Create multiple default instances
203        let instances: Vec<KeyDerivationParams> =
204            (0..5).map(|_| KeyDerivationParams::default()).collect();
205
206        // All salts should be unique
207        for i in 0..instances.len() {
208            for j in (i + 1)..instances.len() {
209                assert_ne!(
210                    instances[i].salt, instances[j].salt,
211                    "Salts at indices {} and {} should be different",
212                    i, j
213                );
214            }
215        }
216    }
217}