rust_keyvault/
export.rs

1//! Key import/export functionality for secure key exchange
2
3use crate::{key::SecretKey, Algorithm, KeyMetadata, Result};
4use serde::{Deserialize, Serialize};
5use std::time::SystemTime;
6
7/// Current version of the export format
8pub const EXPORT_FORMAT_VERSION: u32 = 1;
9
10/// A cryptographic key exported in a secure, portable format
11///
12/// The key material is encrypted using a password-derived key (Argon2id).
13/// The format is versioned to support future compatibility.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ExportedKey {
16    /// Format version for compatibility
17    pub format_version: u32,
18
19    /// When the key was exported
20    pub exported_at: SystemTime,
21
22    /// Algorithm used to wrap/encrypt the key
23    pub wrapping_algorithm: Algorithm,
24
25    /// Salt used for password derivation (32 bytes)
26    pub salt: Vec<u8>,
27
28    /// Argon2 parameters used for derivation
29    pub argon2_params: ExportArgon2Params,
30
31    /// Encrypted key material (nonce + ciphertext + tag)
32    pub encrypted_key: Vec<u8>,
33
34    /// Key metadata (stored in plaintext for validation)
35    pub metadata: KeyMetadata,
36
37    /// Optional comment/description
38    pub comment: Option<String>,
39}
40
41/// Argon2 parameters used for export encryption
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ExportArgon2Params {
44    /// Memory size in KiB
45    pub memory_kib: u32,
46    /// Number of iterations
47    pub time_cost: u32,
48    /// Degree of parallelism
49    pub parallelism: u32,
50}
51
52impl ExportedKey {
53    /// Create a new exported key structure
54    pub fn new(
55        key: &SecretKey,
56        metadata: KeyMetadata,
57        password: &[u8],
58        wrapping_algorithm: Algorithm,
59    ) -> Result<Self> {
60        use crate::crypto::{NonceGenerator, RandomNonceGenerator, RuntimeAead, AEAD};
61        use argon2::{Algorithm as Argon2Algorithm, Argon2, Params, Version};
62        use rand_chacha::ChaCha20Rng;
63        use rand_core::{RngCore, SeedableRng};
64
65        // Generate random salt for this export
66        let mut rng = ChaCha20Rng::from_entropy();
67        let mut salt = vec![0u8; 32];
68        rng.fill_bytes(&mut salt);
69
70        // High-security Argon2 parameters for export
71        let argon2_params = ExportArgon2Params {
72            memory_kib: 65536, // 64 MiB
73            time_cost: 4,
74            parallelism: 4,
75        };
76
77        // Derive wrapping key from password
78        let params = Params::new(
79            argon2_params.memory_kib,
80            argon2_params.time_cost,
81            argon2_params.parallelism,
82            Some(32),
83        )
84        .map_err(|e| {
85            crate::Error::crypto("export_key", &format!("invalid Argon2 params: {}", e))
86        })?;
87
88        let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
89        let mut wrapping_key_bytes = [0u8; 32];
90        argon2
91            .hash_password_into(password, &salt, &mut wrapping_key_bytes)
92            .map_err(|e| {
93                crate::Error::crypto("export_key", &format!("Argon2 derivation failed: {}", e))
94            })?;
95
96        let wrapping_key = SecretKey::from_bytes(wrapping_key_bytes.to_vec(), wrapping_algorithm)?;
97
98        // Encrypt the key material
99        let aead = RuntimeAead;
100        let nonce_size = match wrapping_algorithm {
101            Algorithm::XChaCha20Poly1305 => 24,
102            _ => 12,
103        };
104
105        let mut nonce_gen = RandomNonceGenerator::new(ChaCha20Rng::from_entropy(), nonce_size);
106
107        let nonce = nonce_gen.generate_nonce(b"key-export")?;
108        let ciphertext = aead.encrypt(
109            &wrapping_key,
110            &nonce,
111            key.expose_secret(),
112            b"rust-keyvault-export-v1",
113        )?;
114
115        // Prepend nonce to ciphertext
116        let mut encrypted_key = nonce;
117        encrypted_key.extend_from_slice(&ciphertext);
118
119        Ok(Self {
120            format_version: EXPORT_FORMAT_VERSION,
121            exported_at: SystemTime::now(),
122            wrapping_algorithm,
123            salt,
124            argon2_params,
125            encrypted_key,
126            metadata,
127            comment: None,
128        })
129    }
130
131    /// Decrypt and extract the key material
132    pub fn decrypt(&self, password: &[u8]) -> Result<SecretKey> {
133        use crate::crypto::{RuntimeAead, AEAD};
134        use argon2::{Algorithm as Argon2Algorithm, Argon2, Params, Version};
135
136        // Verify format version
137        if self.format_version != EXPORT_FORMAT_VERSION {
138            return Err(crate::Error::SerializationError {
139                operation: "import_key".to_string(),
140                message: format!(
141                    "unsupported export format version: {} (expected {})",
142                    self.format_version, EXPORT_FORMAT_VERSION
143                ),
144            });
145        }
146
147        // Derive wrapping key from password
148        let params = Params::new(
149            self.argon2_params.memory_kib,
150            self.argon2_params.time_cost,
151            self.argon2_params.parallelism,
152            Some(32),
153        )
154        .map_err(|e| {
155            crate::Error::crypto("import_key", &format!("invalid Argon2 params: {}", e))
156        })?;
157
158        let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
159        let mut wrapping_key_bytes = [0u8; 32];
160        argon2
161            .hash_password_into(password, &self.salt, &mut wrapping_key_bytes)
162            .map_err(|e| {
163                crate::Error::crypto("import_key", &format!("Argon2 derivation failed: {}", e))
164            })?;
165
166        let wrapping_key =
167            SecretKey::from_bytes(wrapping_key_bytes.to_vec(), self.wrapping_algorithm)?;
168
169        // Determine nonce size
170        let nonce_size = match self.wrapping_algorithm {
171            Algorithm::XChaCha20Poly1305 => 24,
172            _ => 12,
173        };
174
175        if self.encrypted_key.len() < nonce_size {
176            return Err(crate::Error::crypto(
177                "import_key",
178                "encrypted key too short",
179            ));
180        }
181
182        // Split nonce and ciphertext
183        let (nonce, ciphertext) = self.encrypted_key.split_at(nonce_size);
184
185        // Decrypt the key material
186        let aead = RuntimeAead;
187        let key_bytes =
188            aead.decrypt(&wrapping_key, nonce, ciphertext, b"rust-keyvault-export-v1")?;
189
190        // Reconstruct the secret key
191        SecretKey::from_bytes(key_bytes, self.metadata.algorithm)
192    }
193
194    /// Add a comment to the exported key
195    pub fn with_comment<S: Into<String>>(mut self, comment: S) -> Self {
196        self.comment = Some(comment.into());
197        self
198    }
199
200    /// Serialize to JSON
201    pub fn to_json(&self) -> Result<String> {
202        serde_json::to_string_pretty(self).map_err(|e| crate::Error::SerializationError {
203            operation: "export_to_json".to_string(),
204            message: format!("JSON serialization failed: {}", e),
205        })
206    }
207
208    /// Deserialize from JSON
209    pub fn from_json(json: &str) -> Result<Self> {
210        serde_json::from_str(json).map_err(|e| crate::Error::SerializationError {
211            operation: "import_from_json".to_string(),
212            message: format!("JSON deserialization failed: {}", e),
213        })
214    }
215
216    /// Serialize to bytes (using bincode or similar)
217    pub fn to_bytes(&self) -> Result<Vec<u8>> {
218        serde_json::to_vec(self).map_err(|e| crate::Error::SerializationError {
219            operation: "export_to_bytes".to_string(),
220            message: format!("serialization failed: {}", e),
221        })
222    }
223
224    /// Deserialize from bytes
225    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
226        serde_json::from_slice(bytes).map_err(|e| crate::Error::SerializationError {
227            operation: "import_from_bytes".to_string(),
228            message: format!("deserialization failed: {}", e),
229        })
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::KeyId;
237    use std::time::SystemTime;
238
239    #[test]
240    fn test_export_import_roundtrip() {
241        // Create a test key
242        let key = SecretKey::generate(Algorithm::ChaCha20Poly1305).unwrap();
243        let key_id = KeyId::generate_base().unwrap();
244
245        let metadata = KeyMetadata {
246            id: key_id.clone(),
247            base_id: key_id.clone(),
248            algorithm: Algorithm::ChaCha20Poly1305,
249            created_at: SystemTime::now(),
250            expires_at: None,
251            state: crate::KeyState::Active,
252            version: 1,
253        };
254
255        // Export with password
256        let password = b"super-secret-export-password";
257        let exported = ExportedKey::new(
258            &key,
259            metadata.clone(),
260            password,
261            Algorithm::ChaCha20Poly1305,
262        )
263        .unwrap()
264        .with_comment("Test export");
265
266        // Verify exported structure
267        assert_eq!(exported.format_version, EXPORT_FORMAT_VERSION);
268        assert_eq!(exported.wrapping_algorithm, Algorithm::ChaCha20Poly1305);
269        assert_eq!(exported.metadata.algorithm, Algorithm::ChaCha20Poly1305);
270        assert!(exported.comment.is_some());
271
272        // Decrypt with correct password
273        let decrypted = exported.decrypt(password).unwrap();
274        assert_eq!(decrypted.expose_secret(), key.expose_secret());
275        assert_eq!(decrypted.algorithm(), key.algorithm());
276    }
277
278    #[test]
279    fn test_wrong_password_fails() {
280        let key = SecretKey::generate(Algorithm::Aes256Gcm).unwrap();
281        let key_id = KeyId::generate_base().unwrap();
282
283        let metadata = KeyMetadata {
284            id: key_id.clone(),
285            base_id: key_id,
286            algorithm: Algorithm::Aes256Gcm,
287            created_at: SystemTime::now(),
288            expires_at: None,
289            state: crate::KeyState::Active,
290            version: 1,
291        };
292
293        let exported =
294            ExportedKey::new(&key, metadata, b"correct-password", Algorithm::Aes256Gcm).unwrap();
295
296        // Try to decrypt with wrong password
297        let result = exported.decrypt(b"wrong-password");
298        assert!(result.is_err());
299    }
300
301    #[test]
302    fn test_json_serialization() {
303        let key = SecretKey::generate(Algorithm::XChaCha20Poly1305).unwrap();
304        let key_id = KeyId::generate_base().unwrap();
305
306        let metadata = KeyMetadata {
307            id: key_id.clone(),
308            base_id: key_id,
309            algorithm: Algorithm::XChaCha20Poly1305,
310            created_at: SystemTime::now(),
311            expires_at: None,
312            state: crate::KeyState::Active,
313            version: 1,
314        };
315
316        let exported =
317            ExportedKey::new(&key, metadata, b"password", Algorithm::XChaCha20Poly1305).unwrap();
318
319        // Serialize to JSON
320        let json = exported.to_json().unwrap();
321        assert!(json.contains("format_version"));
322        assert!(json.contains("encrypted_key"));
323
324        // Deserialize from JSON
325        let imported = ExportedKey::from_json(&json).unwrap();
326        assert_eq!(imported.format_version, exported.format_version);
327        assert_eq!(imported.metadata.algorithm, exported.metadata.algorithm);
328
329        // Verify decryption still works
330        let decrypted = imported.decrypt(b"password").unwrap();
331        assert_eq!(decrypted.expose_secret(), key.expose_secret());
332    }
333
334    #[test]
335    fn test_metadata_preserved() {
336        let key = SecretKey::generate(Algorithm::ChaCha20Poly1305).unwrap();
337        let key_id = KeyId::generate_base().unwrap();
338
339        let original_metadata = KeyMetadata {
340            id: key_id.clone(),
341            base_id: key_id,
342            algorithm: Algorithm::ChaCha20Poly1305,
343            created_at: SystemTime::now(),
344            expires_at: Some(SystemTime::now()),
345            state: crate::KeyState::Rotating,
346            version: 42,
347        };
348
349        let exported = ExportedKey::new(
350            &key,
351            original_metadata.clone(),
352            b"pass",
353            Algorithm::ChaCha20Poly1305,
354        )
355        .unwrap();
356
357        // Verify metadata is preserved
358        assert_eq!(exported.metadata.version, 42);
359        assert_eq!(exported.metadata.state, crate::KeyState::Rotating);
360        assert!(exported.metadata.expires_at.is_some());
361    }
362}