envelope_cli/crypto/
key_derivation.rs

1//! Key derivation using Argon2id
2//!
3//! Derives encryption keys from user passphrases using Argon2id,
4//! a memory-hard key derivation function resistant to GPU/ASIC attacks.
5
6use argon2::{
7    password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
8    Argon2, Params,
9};
10use serde::{Deserialize, Serialize};
11
12use crate::error::{EnvelopeError, EnvelopeResult};
13
14/// Parameters for key derivation
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct KeyDerivationParams {
17    /// Salt for key derivation (base64 encoded)
18    pub salt: String,
19    /// Memory cost in KiB (default: 65536 = 64 MiB)
20    pub memory_cost: u32,
21    /// Time cost (iterations, default: 3)
22    pub time_cost: u32,
23    /// Parallelism degree (default: 4)
24    pub parallelism: u32,
25}
26
27impl Default for KeyDerivationParams {
28    fn default() -> Self {
29        Self {
30            salt: String::new(), // Will be generated on first use
31            memory_cost: 65536,  // 64 MiB
32            time_cost: 3,
33            parallelism: 4,
34        }
35    }
36}
37
38impl KeyDerivationParams {
39    /// Create new params with a random salt
40    pub fn new() -> Self {
41        let salt = SaltString::generate(&mut OsRng);
42        Self {
43            salt: salt.to_string(),
44            ..Default::default()
45        }
46    }
47
48    /// Create params with specific values
49    pub fn with_values(salt: String, memory_cost: u32, time_cost: u32, parallelism: u32) -> Self {
50        Self {
51            salt,
52            memory_cost,
53            time_cost,
54            parallelism,
55        }
56    }
57}
58
59/// A derived encryption key
60pub struct DerivedKey {
61    /// The 32-byte key for AES-256
62    key: [u8; 32],
63}
64
65impl DerivedKey {
66    /// Get the key bytes
67    pub fn as_bytes(&self) -> &[u8; 32] {
68        &self.key
69    }
70}
71
72impl Drop for DerivedKey {
73    fn drop(&mut self) {
74        // Zero out the key when dropped
75        self.key.iter_mut().for_each(|b| *b = 0);
76    }
77}
78
79/// Derive an encryption key from a passphrase
80pub fn derive_key(passphrase: &str, params: &KeyDerivationParams) -> EnvelopeResult<DerivedKey> {
81    // Parse the salt
82    let salt = SaltString::from_b64(&params.salt)
83        .map_err(|e| EnvelopeError::Encryption(format!("Invalid salt: {}", e)))?;
84
85    // Configure Argon2id with custom params
86    let argon2_params = Params::new(
87        params.memory_cost,
88        params.time_cost,
89        params.parallelism,
90        Some(32), // Output length for AES-256
91    )
92    .map_err(|e| EnvelopeError::Encryption(format!("Invalid Argon2 parameters: {}", e)))?;
93
94    let argon2 = Argon2::new(
95        argon2::Algorithm::Argon2id,
96        argon2::Version::V0x13,
97        argon2_params,
98    );
99
100    // Derive the key by hashing the password
101    let hash = argon2
102        .hash_password(passphrase.as_bytes(), &salt)
103        .map_err(|e| EnvelopeError::Encryption(format!("Key derivation failed: {}", e)))?;
104
105    // Extract the hash output (the actual derived key)
106    let hash_output = hash
107        .hash
108        .ok_or_else(|| EnvelopeError::Encryption("No hash output generated".to_string()))?;
109
110    let hash_bytes = hash_output.as_bytes();
111
112    if hash_bytes.len() < 32 {
113        return Err(EnvelopeError::Encryption(
114            "Hash output too short for AES-256 key".to_string(),
115        ));
116    }
117
118    let mut key = [0u8; 32];
119    key.copy_from_slice(&hash_bytes[..32]);
120
121    Ok(DerivedKey { key })
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_derive_key() {
130        let params = KeyDerivationParams::new();
131        let key = derive_key("test_passphrase", &params).unwrap();
132        assert_eq!(key.as_bytes().len(), 32);
133    }
134
135    #[test]
136    fn test_same_passphrase_same_key() {
137        let params = KeyDerivationParams::new();
138        let key1 = derive_key("test_passphrase", &params).unwrap();
139        let key2 = derive_key("test_passphrase", &params).unwrap();
140        assert_eq!(key1.as_bytes(), key2.as_bytes());
141    }
142
143    #[test]
144    fn test_different_passphrase_different_key() {
145        let params = KeyDerivationParams::new();
146        let key1 = derive_key("passphrase1", &params).unwrap();
147        let key2 = derive_key("passphrase2", &params).unwrap();
148        assert_ne!(key1.as_bytes(), key2.as_bytes());
149    }
150
151    #[test]
152    fn test_different_salt_different_key() {
153        let params1 = KeyDerivationParams::new();
154        let params2 = KeyDerivationParams::new();
155        let key1 = derive_key("same_passphrase", &params1).unwrap();
156        let key2 = derive_key("same_passphrase", &params2).unwrap();
157        // Different salts should produce different keys
158        assert_ne!(key1.as_bytes(), key2.as_bytes());
159    }
160
161    #[test]
162    fn test_key_zeroized_on_drop() {
163        let params = KeyDerivationParams::new();
164        let key_ptr: *const [u8; 32];
165        {
166            let key = derive_key("test_passphrase", &params).unwrap();
167            key_ptr = key.as_bytes() as *const [u8; 32];
168        }
169        // Note: This test is more of a documentation that we implement Drop
170        // In practice, the memory might not be immediately zeroed due to optimizations
171        // but we've at least attempted to clear it
172        let _ = key_ptr; // Suppress unused warning
173    }
174}