1use crate::{key::SecretKey, Algorithm, KeyMetadata, Result};
4use serde::{Deserialize, Serialize};
5use std::time::SystemTime;
6
7pub const EXPORT_FORMAT_VERSION: u32 = 1;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ExportedKey {
16 pub format_version: u32,
18
19 pub exported_at: SystemTime,
21
22 pub wrapping_algorithm: Algorithm,
24
25 pub salt: Vec<u8>,
27
28 pub argon2_params: ExportArgon2Params,
30
31 pub encrypted_key: Vec<u8>,
33
34 pub metadata: KeyMetadata,
36
37 pub comment: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ExportArgon2Params {
44 pub memory_kib: u32,
46 pub time_cost: u32,
48 pub parallelism: u32,
50}
51
52impl ExportedKey {
53 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 let mut rng = ChaCha20Rng::from_entropy();
67 let mut salt = vec![0u8; 32];
68 rng.fill_bytes(&mut salt);
69
70 let argon2_params = ExportArgon2Params {
72 memory_kib: 65536, time_cost: 4,
74 parallelism: 4,
75 };
76
77 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 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 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 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 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 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 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 let (nonce, ciphertext) = self.encrypted_key.split_at(nonce_size);
184
185 let aead = RuntimeAead;
187 let key_bytes =
188 aead.decrypt(&wrapping_key, nonce, ciphertext, b"rust-keyvault-export-v1")?;
189
190 SecretKey::from_bytes(key_bytes, self.metadata.algorithm)
192 }
193
194 pub fn with_comment<S: Into<String>>(mut self, comment: S) -> Self {
196 self.comment = Some(comment.into());
197 self
198 }
199
200 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 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 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 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 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 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 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 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 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 let json = exported.to_json().unwrap();
321 assert!(json.contains("format_version"));
322 assert!(json.contains("encrypted_key"));
323
324 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 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 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}