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