1use std::collections::HashMap;
49
50use argon2::Argon2;
51use base64::engine::general_purpose::STANDARD as B64;
52use base64::Engine;
53use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, KeyInit};
54use rand::RngCore;
55use serde::{Deserialize, Serialize};
56use zeroize::Zeroize;
57
58use crate::error::{Error, Result};
59
60const RESERVED: &[&str] = &["iroh", "ipns", "did_signing", "did_encryption"];
62
63#[derive(Serialize, Deserialize)]
66struct BundleJson {
67 iroh: String,
68 ipns: String,
69 did_signing: String,
70 did_encryption: String,
71 created_at: String,
72 #[serde(default)]
73 extra: HashMap<String, String>,
74}
75
76pub struct SecretBundle {
110 pub iroh_secret_key: [u8; 32],
112 pub ipns_secret_key: [u8; 32],
114 pub did_signing_key: [u8; 32],
116 pub did_encryption_key: [u8; 32],
118
119 pub created_at: String,
122
123 extra_keys: HashMap<String, [u8; 32]>,
126}
127
128impl Drop for SecretBundle {
129 fn drop(&mut self) {
130 self.iroh_secret_key.zeroize();
131 self.ipns_secret_key.zeroize();
132 self.did_signing_key.zeroize();
133 self.did_encryption_key.zeroize();
134 for v in self.extra_keys.values_mut() {
135 v.zeroize();
136 }
137 }
138}
139
140impl Clone for SecretBundle {
141 fn clone(&self) -> Self {
142 Self {
143 iroh_secret_key: self.iroh_secret_key,
144 ipns_secret_key: self.ipns_secret_key,
145 did_signing_key: self.did_signing_key,
146 did_encryption_key: self.did_encryption_key,
147 created_at: self.created_at.clone(),
148 extra_keys: self.extra_keys.clone(),
149 }
150 }
151}
152
153impl SecretBundle {
154 pub fn generate() -> Self {
156 let mut rng = rand::rngs::OsRng;
157 let mut b = Self {
158 iroh_secret_key: [0u8; 32],
159 ipns_secret_key: [0u8; 32],
160 did_signing_key: [0u8; 32],
161 did_encryption_key: [0u8; 32],
162 created_at: crate::doc::now_iso_utc(),
163 extra_keys: HashMap::new(),
164 };
165 rng.fill_bytes(&mut b.iroh_secret_key);
166 rng.fill_bytes(&mut b.ipns_secret_key);
167 rng.fill_bytes(&mut b.did_signing_key);
168 rng.fill_bytes(&mut b.did_encryption_key);
169 b
170 }
171
172 pub fn add_key(&mut self, name: &str, key: [u8; 32]) -> Result<()> {
179 validate_key_name(name)?;
180 self.extra_keys.insert(name.to_string(), key);
181 Ok(())
182 }
183
184 pub fn generate_key(&mut self, name: &str) -> Result<[u8; 32]> {
188 validate_key_name(name)?;
189 let mut key = [0u8; 32];
190 rand::rngs::OsRng.fill_bytes(&mut key);
191 self.extra_keys.insert(name.to_string(), key);
192 Ok(key)
193 }
194
195 pub fn get_key(&self, name: &str) -> Option<&[u8; 32]> {
197 self.extra_keys.get(name)
198 }
199
200 pub fn remove_key(&mut self, name: &str) -> Option<[u8; 32]> {
202 self.extra_keys.remove(name)
203 }
204
205 pub fn extra_key_names(&self) -> impl Iterator<Item = &str> {
207 self.extra_keys.keys().map(String::as_str)
208 }
209
210 fn to_json_bytes(&self) -> Result<Vec<u8>> {
213 let wire = BundleJson {
214 iroh: B64.encode(self.iroh_secret_key),
215 ipns: B64.encode(self.ipns_secret_key),
216 did_signing: B64.encode(self.did_signing_key),
217 did_encryption: B64.encode(self.did_encryption_key),
218 created_at: self.created_at.clone(),
219 extra: self
220 .extra_keys
221 .iter()
222 .map(|(k, v)| (k.clone(), B64.encode(v)))
223 .collect(),
224 };
225 serde_json::to_vec(&wire).map_err(|e| Error::Secrets(e.to_string()))
226 }
227
228 fn from_json_bytes(mut data: Vec<u8>) -> Result<Self> {
229 let wire: BundleJson = serde_json::from_slice(&data)
230 .map_err(|e| Error::Secrets(format!("failed to parse bundle JSON: {e}")))?;
231
232 data.zeroize();
233
234 let decode = |s: &str, field: &str| -> Result<[u8; 32]> {
235 let bytes = B64
236 .decode(s)
237 .map_err(|e| Error::Secrets(format!("base64 decode error in '{field}': {e}")))?;
238 bytes
239 .as_slice()
240 .try_into()
241 .map_err(|_| Error::Secrets(format!("'{field}' must be exactly 32 bytes")))
242 };
243
244 let mut extra_keys = HashMap::with_capacity(wire.extra.len());
245 for (name, val) in &wire.extra {
246 extra_keys.insert(name.clone(), decode(val, name)?);
247 }
248
249 Ok(Self {
250 iroh_secret_key: decode(&wire.iroh, "iroh")?,
251 ipns_secret_key: decode(&wire.ipns, "ipns")?,
252 did_signing_key: decode(&wire.did_signing, "did_signing")?,
253 did_encryption_key: decode(&wire.did_encryption, "did_encryption")?,
254 created_at: wire.created_at,
255 extra_keys,
256 })
257 }
258
259 pub fn encrypt(&self, passphrase: &str) -> Result<Vec<u8>> {
265 let mut salt = [0u8; 16];
266 rand::rngs::OsRng.fill_bytes(&mut salt);
267
268 let mut key_bytes = [0u8; 32];
269 Argon2::default()
270 .hash_password_into(passphrase.as_bytes(), &salt, &mut key_bytes)
271 .map_err(|e| Error::Secrets(e.to_string()))?;
272
273 let mut nonce_bytes = [0u8; 12];
274 rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
275 let nonce = *chacha20poly1305::Nonce::from_slice(&nonce_bytes);
276
277 let cipher = ChaCha20Poly1305::new_from_slice(&key_bytes)
278 .map_err(|e| Error::Secrets(e.to_string()))?;
279
280 let mut plaintext = self.to_json_bytes()?;
281 let ciphertext = cipher
282 .encrypt(&nonce, plaintext.as_slice())
283 .map_err(|e| Error::Secrets(e.to_string()))?;
284
285 plaintext.zeroize();
286 key_bytes.zeroize();
287
288 let mut out = Vec::with_capacity(16 + 12 + ciphertext.len());
289 out.extend_from_slice(&salt);
290 out.extend_from_slice(&nonce_bytes);
291 out.extend_from_slice(&ciphertext);
292 Ok(out)
293 }
294
295 pub fn decrypt(data: &[u8], passphrase: &str) -> Result<Self> {
300 if data.len() < 28 {
301 return Err(Error::Secrets("secret bundle too short".to_string()));
302 }
303
304 let salt = &data[0..16];
305 let nonce_bytes: [u8; 12] = data[16..28]
306 .try_into()
307 .map_err(|_| Error::Secrets("malformed bundle nonce".to_string()))?;
308 let ciphertext = &data[28..];
309
310 let mut key_bytes = [0u8; 32];
311 Argon2::default()
312 .hash_password_into(passphrase.as_bytes(), salt, &mut key_bytes)
313 .map_err(|e| Error::Secrets(e.to_string()))?;
314
315 let nonce = *chacha20poly1305::Nonce::from_slice(&nonce_bytes);
316 let cipher = ChaCha20Poly1305::new_from_slice(&key_bytes)
317 .map_err(|e| Error::Secrets(e.to_string()))?;
318 let plaintext = cipher
319 .decrypt(&nonce, ciphertext)
320 .map_err(|_| Error::Secrets("decryption failed (wrong passphrase?)".to_string()))?;
321
322 key_bytes.zeroize();
323
324 Self::from_json_bytes(plaintext)
325 }
326
327 #[cfg(not(target_arch = "wasm32"))]
329 pub fn load(path: &std::path::Path, passphrase: &str) -> Result<Self> {
330 let data = std::fs::read(path)
331 .map_err(|e| Error::Secrets(format!("failed to read {}: {e}", path.display())))?;
332 Self::decrypt(&data, passphrase)
333 }
334
335 #[cfg(not(target_arch = "wasm32"))]
337 pub fn save(&self, path: &std::path::Path, passphrase: &str) -> Result<()> {
338 let encrypted = self.encrypt(passphrase)?;
339 super::write_secure(path, &encrypted)
340 }
341
342 pub fn generate_passphrase() -> String {
344 use rand::distributions::{Alphanumeric, DistString};
345 Alphanumeric.sample_string(&mut rand::rngs::OsRng, 43)
346 }
347
348 pub fn generate_identity(&self) -> Result<crate::GeneratedIdentity> {
357 use crate::{
358 identity::build_identity_from_keys, ipns_from_secret, Did, EncryptionKey, SigningKey,
359 };
360 let ipns = ipns_from_secret(self.ipns_secret_key)
361 .map_err(|e| Error::Secrets(format!("ipns derivation failed: {e}")))?;
362 let sign_did = Did::new_url(&ipns, Some("sign"))
363 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
364 let enc_did = Did::new_url(&ipns, Some("enc"))
365 .map_err(|e| Error::Secrets(format!("enc did: {e}")))?;
366 let signing_key = SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
367 .map_err(|e| Error::Secrets(format!("signing key: {e}")))?;
368 let encryption_key =
369 EncryptionKey::from_private_key_bytes(enc_did, self.did_encryption_key)
370 .map_err(|e| Error::Secrets(format!("encryption key: {e}")))?;
371 let mut identity = build_identity_from_keys(&ipns, &signing_key, &encryption_key)
372 .map_err(|e| Error::Secrets(format!("identity generation failed: {e}")))?;
373 identity.document.created_at.clone_from(&self.created_at);
377 let vm = identity
378 .document
379 .get_verification_method_by_id(&identity.document.assertion_method[0].clone())
380 .map_err(|e| Error::Secrets(format!("assertion vm: {e}")))?;
381 let vm = vm.clone();
382 identity
383 .document
384 .sign(&signing_key, &vm)
385 .map_err(|e| Error::Secrets(format!("re-sign after created_at restore: {e}")))?;
386 Ok(identity)
387 }
388
389 pub fn build_document(&self, ext: crate::doc::MaExtension) -> Result<crate::Document> {
406 use crate::{ipns_from_secret, Did, SigningKey};
407 let identity = self.generate_identity()?;
408 let mut document = identity.document;
409 let ipns = ipns_from_secret(self.ipns_secret_key)
410 .map_err(|e| Error::Secrets(format!("ipns derivation: {e}")))?;
411 let sign_did = Did::new_url(&ipns, Some("sign"))
412 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
413 let signing_key = SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
414 .map_err(|e| Error::Secrets(format!("signing key: {e}")))?;
415 let vm = document
416 .get_verification_method_by_id(&document.assertion_method[0].clone())
417 .map_err(|e| Error::Secrets(format!("assertion vm: {e}")))?;
418 let vm = vm.clone();
419 document.set_ma_extension(ext);
420 document.touch();
422 document
423 .sign(&signing_key, &vm)
424 .map_err(|e| Error::Secrets(format!("sign: {e}")))?;
425 Ok(document)
426 }
427
428 pub fn signing_key(&self) -> Result<crate::SigningKey> {
434 use crate::{ipns_from_secret, Did, SigningKey};
435 let ipns = ipns_from_secret(self.ipns_secret_key)
436 .map_err(|e| Error::Secrets(format!("ipns derivation: {e}")))?;
437 let sign_did = Did::new_url(&ipns, Some("sign"))
438 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
439 SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
440 .map_err(|e| Error::Secrets(format!("signing key: {e}")))
441 }
442}
443
444fn validate_key_name(name: &str) -> Result<()> {
447 if name.is_empty() {
448 return Err(Error::Secrets("key name must not be empty".to_string()));
449 }
450 if RESERVED.contains(&name) {
451 return Err(Error::Secrets(format!(
452 "key name '{name}' is reserved for a standard key"
453 )));
454 }
455 Ok(())
456}
457
458#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn roundtrip_standard_keys() {
466 let bundle = SecretBundle::generate();
467 let passphrase = "test-passphrase-1234";
468 let encrypted = bundle.encrypt(passphrase).unwrap();
469 let restored = SecretBundle::decrypt(&encrypted, passphrase).unwrap();
470 assert_eq!(bundle.iroh_secret_key, restored.iroh_secret_key);
471 assert_eq!(bundle.ipns_secret_key, restored.ipns_secret_key);
472 assert_eq!(bundle.did_signing_key, restored.did_signing_key);
473 assert_eq!(bundle.did_encryption_key, restored.did_encryption_key);
474 }
475
476 #[test]
477 fn roundtrip_with_extra_keys() {
478 let mut bundle = SecretBundle::generate();
479 bundle.generate_key("my_service").unwrap();
480 bundle.generate_key("another_key").unwrap();
481
482 let passphrase = "extra-keys-test";
483 let encrypted = bundle.encrypt(passphrase).unwrap();
484 let restored = SecretBundle::decrypt(&encrypted, passphrase).unwrap();
485
486 assert_eq!(bundle.get_key("my_service"), restored.get_key("my_service"));
487 assert_eq!(
488 bundle.get_key("another_key"),
489 restored.get_key("another_key")
490 );
491 }
492
493 #[test]
494 fn reserved_name_rejected() {
495 let mut bundle = SecretBundle::generate();
496 assert!(bundle.add_key("iroh", [0u8; 32]).is_err());
497 assert!(bundle.add_key("did_signing", [0u8; 32]).is_err());
498 }
499
500 #[test]
501 fn empty_name_rejected() {
502 let mut bundle = SecretBundle::generate();
503 assert!(bundle.add_key("", [0u8; 32]).is_err());
504 }
505
506 #[test]
507 fn wrong_passphrase_fails() {
508 let bundle = SecretBundle::generate();
509 let encrypted = bundle.encrypt("correct").unwrap();
510 assert!(SecretBundle::decrypt(&encrypted, "wrong").is_err());
511 }
512}