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_core::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_core::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_core::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_core::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_core::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 const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
345 const ACCEPT_BELOW: u8 = 248;
347 let mut result = String::with_capacity(43);
348 let mut buf = [0u8; 64];
349 loop {
350 rand_core::OsRng.fill_bytes(&mut buf);
351 for &b in &buf {
352 if b < ACCEPT_BELOW {
353 result.push(CHARSET[(b as usize) % 62] as char);
354 if result.len() == 43 {
355 return result;
356 }
357 }
358 }
359 }
360 }
361
362 pub fn generate_identity(&self) -> Result<crate::GeneratedIdentity> {
371 use crate::{
372 identity::build_identity_from_keys, ipns_from_secret, Did, EncryptionKey, SigningKey,
373 };
374 let ipns = ipns_from_secret(self.ipns_secret_key)
375 .map_err(|e| Error::Secrets(format!("ipns derivation failed: {e}")))?;
376 let sign_did = Did::new_url(&ipns, Some("sign"))
377 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
378 let enc_did = Did::new_url(&ipns, Some("enc"))
379 .map_err(|e| Error::Secrets(format!("enc did: {e}")))?;
380 let signing_key = SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
381 .map_err(|e| Error::Secrets(format!("signing key: {e}")))?;
382 let encryption_key =
383 EncryptionKey::from_private_key_bytes(enc_did, self.did_encryption_key)
384 .map_err(|e| Error::Secrets(format!("encryption key: {e}")))?;
385 let mut identity = build_identity_from_keys(&ipns, &signing_key, &encryption_key)
386 .map_err(|e| Error::Secrets(format!("identity generation failed: {e}")))?;
387 identity.document.created_at.clone_from(&self.created_at);
391 let vm = identity
392 .document
393 .get_verification_method_by_id(&identity.document.assertion_method[0].clone())
394 .map_err(|e| Error::Secrets(format!("assertion vm: {e}")))?;
395 let vm = vm.clone();
396 identity
397 .document
398 .sign(&signing_key, &vm)
399 .map_err(|e| Error::Secrets(format!("re-sign after created_at restore: {e}")))?;
400 Ok(identity)
401 }
402
403 pub fn build_document(&self, ext: crate::doc::MaExtension) -> Result<crate::Document> {
420 use crate::{ipns_from_secret, Did, SigningKey};
421 let identity = self.generate_identity()?;
422 let mut document = identity.document;
423 let ipns = ipns_from_secret(self.ipns_secret_key)
424 .map_err(|e| Error::Secrets(format!("ipns derivation: {e}")))?;
425 let sign_did = Did::new_url(&ipns, Some("sign"))
426 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
427 let signing_key = SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
428 .map_err(|e| Error::Secrets(format!("signing key: {e}")))?;
429 let vm = document
430 .get_verification_method_by_id(&document.assertion_method[0].clone())
431 .map_err(|e| Error::Secrets(format!("assertion vm: {e}")))?;
432 let vm = vm.clone();
433 document.set_ma_extension(ext);
434 document.touch();
436 document
437 .sign(&signing_key, &vm)
438 .map_err(|e| Error::Secrets(format!("sign: {e}")))?;
439 Ok(document)
440 }
441
442 pub fn signing_key(&self) -> Result<crate::SigningKey> {
448 use crate::{ipns_from_secret, Did, SigningKey};
449 let ipns = ipns_from_secret(self.ipns_secret_key)
450 .map_err(|e| Error::Secrets(format!("ipns derivation: {e}")))?;
451 let sign_did = Did::new_url(&ipns, Some("sign"))
452 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
453 SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
454 .map_err(|e| Error::Secrets(format!("signing key: {e}")))
455 }
456}
457
458fn validate_key_name(name: &str) -> Result<()> {
461 if name.is_empty() {
462 return Err(Error::Secrets("key name must not be empty".to_string()));
463 }
464 if RESERVED.contains(&name) {
465 return Err(Error::Secrets(format!(
466 "key name '{name}' is reserved for a standard key"
467 )));
468 }
469 Ok(())
470}
471
472#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn roundtrip_standard_keys() {
480 let bundle = SecretBundle::generate();
481 let passphrase = "test-passphrase-1234";
482 let encrypted = bundle.encrypt(passphrase).unwrap();
483 let restored = SecretBundle::decrypt(&encrypted, passphrase).unwrap();
484 assert_eq!(bundle.iroh_secret_key, restored.iroh_secret_key);
485 assert_eq!(bundle.ipns_secret_key, restored.ipns_secret_key);
486 assert_eq!(bundle.did_signing_key, restored.did_signing_key);
487 assert_eq!(bundle.did_encryption_key, restored.did_encryption_key);
488 }
489
490 #[test]
491 fn roundtrip_with_extra_keys() {
492 let mut bundle = SecretBundle::generate();
493 bundle.generate_key("my_service").unwrap();
494 bundle.generate_key("another_key").unwrap();
495
496 let passphrase = "extra-keys-test";
497 let encrypted = bundle.encrypt(passphrase).unwrap();
498 let restored = SecretBundle::decrypt(&encrypted, passphrase).unwrap();
499
500 assert_eq!(bundle.get_key("my_service"), restored.get_key("my_service"));
501 assert_eq!(
502 bundle.get_key("another_key"),
503 restored.get_key("another_key")
504 );
505 }
506
507 #[test]
508 fn reserved_name_rejected() {
509 let mut bundle = SecretBundle::generate();
510 assert!(bundle.add_key("iroh", [0u8; 32]).is_err());
511 assert!(bundle.add_key("did_signing", [0u8; 32]).is_err());
512 }
513
514 #[test]
515 fn empty_name_rejected() {
516 let mut bundle = SecretBundle::generate();
517 assert!(bundle.add_key("", [0u8; 32]).is_err());
518 }
519
520 #[test]
521 fn wrong_passphrase_fails() {
522 let bundle = SecretBundle::generate();
523 let encrypted = bundle.encrypt("correct").unwrap();
524 assert!(SecretBundle::decrypt(&encrypted, "wrong").is_err());
525 }
526
527 #[test]
528 fn get_key_returns_none_for_missing_name() {
529 let bundle = SecretBundle::generate();
530 assert!(bundle.get_key("nonexistent").is_none());
531 }
532
533 #[test]
534 fn remove_key_returns_and_deletes_key() {
535 let mut bundle = SecretBundle::generate();
536 let key = bundle.generate_key("to_remove").unwrap();
537 let removed = bundle.remove_key("to_remove");
538 assert_eq!(removed, Some(key));
539 assert!(bundle.get_key("to_remove").is_none());
540 }
541
542 #[test]
543 fn extra_key_names_lists_all_added_keys() {
544 let mut bundle = SecretBundle::generate();
545 bundle.generate_key("alpha").unwrap();
546 bundle.generate_key("beta").unwrap();
547 let mut names: Vec<&str> = bundle.extra_key_names().collect();
548 names.sort_unstable();
549 assert_eq!(names, ["alpha", "beta"]);
550 }
551
552 #[test]
553 fn generate_passphrase_is_43_alphanumeric_chars() {
554 let p = SecretBundle::generate_passphrase();
555 assert_eq!(p.len(), 43);
556 assert!(p.chars().all(|c| c.is_ascii_alphanumeric()));
557 }
558
559 #[test]
560 fn truncated_blob_fails_decryption() {
561 let bundle = SecretBundle::generate();
562 let encrypted = bundle.encrypt("pass").unwrap();
563 assert!(SecretBundle::decrypt(&encrypted[..10], "pass").is_err());
564 }
565
566 #[test]
567 fn generate_identity_is_deterministic() {
568 let bundle = SecretBundle::generate();
569 let id1 = bundle.generate_identity().unwrap();
570 let id2 = bundle.generate_identity().unwrap();
571 assert_eq!(
572 id1.document.id, id2.document.id,
573 "same bundle must always produce the same DID"
574 );
575 assert_eq!(
576 id1.signing_private_key_hex, id2.signing_private_key_hex,
577 "same bundle must always produce the same signing key"
578 );
579 }
580
581 #[test]
582 fn signing_key_public_key_matches_document_assertion_method() {
583 let bundle = SecretBundle::generate();
584 let identity = bundle.generate_identity().unwrap();
585 let signing_key = bundle.signing_key().unwrap();
586 let sign_vm_id = &identity.document.assertion_method[0];
587 let sign_vm = identity
588 .document
589 .get_verification_method_by_id(sign_vm_id)
590 .unwrap();
591 assert_eq!(
592 signing_key.public_key_multibase, sign_vm.public_key_multibase,
593 "signing_key() must match the document's assertion method VM"
594 );
595 }
596
597 #[test]
598 fn add_key_stores_and_retrieves_exact_bytes() {
599 let mut bundle = SecretBundle::generate();
600 let key_bytes = [0xAB_u8; 32];
601 bundle.add_key("exact", key_bytes).unwrap();
602 assert_eq!(bundle.get_key("exact"), Some(&key_bytes));
603 }
604
605 #[test]
606 fn two_encryptions_of_same_bundle_produce_different_ciphertext() {
607 let bundle = SecretBundle::generate();
609 let c1 = bundle.encrypt("pw").unwrap();
610 let c2 = bundle.encrypt("pw").unwrap();
611 assert_ne!(c1, c2, "each encryption must use a fresh salt+nonce");
612 }
613}