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 #[serde(default)]
72 extra: HashMap<String, String>,
73}
74
75pub struct SecretBundle {
109 pub iroh_secret_key: [u8; 32],
111 pub ipns_secret_key: [u8; 32],
113 pub did_signing_key: [u8; 32],
115 pub did_encryption_key: [u8; 32],
117
118 extra_keys: HashMap<String, [u8; 32]>,
121}
122
123impl Drop for SecretBundle {
124 fn drop(&mut self) {
125 self.iroh_secret_key.zeroize();
126 self.ipns_secret_key.zeroize();
127 self.did_signing_key.zeroize();
128 self.did_encryption_key.zeroize();
129 for v in self.extra_keys.values_mut() {
130 v.zeroize();
131 }
132 }
133}
134
135impl Clone for SecretBundle {
136 fn clone(&self) -> Self {
137 Self {
138 iroh_secret_key: self.iroh_secret_key,
139 ipns_secret_key: self.ipns_secret_key,
140 did_signing_key: self.did_signing_key,
141 did_encryption_key: self.did_encryption_key,
142 extra_keys: self.extra_keys.clone(),
143 }
144 }
145}
146
147impl SecretBundle {
148 pub fn generate() -> Self {
150 let mut rng = rand::rngs::OsRng;
151 let mut b = Self {
152 iroh_secret_key: [0u8; 32],
153 ipns_secret_key: [0u8; 32],
154 did_signing_key: [0u8; 32],
155 did_encryption_key: [0u8; 32],
156 extra_keys: HashMap::new(),
157 };
158 rng.fill_bytes(&mut b.iroh_secret_key);
159 rng.fill_bytes(&mut b.ipns_secret_key);
160 rng.fill_bytes(&mut b.did_signing_key);
161 rng.fill_bytes(&mut b.did_encryption_key);
162 b
163 }
164
165 pub fn add_key(&mut self, name: &str, key: [u8; 32]) -> Result<()> {
172 validate_key_name(name)?;
173 self.extra_keys.insert(name.to_string(), key);
174 Ok(())
175 }
176
177 pub fn generate_key(&mut self, name: &str) -> Result<[u8; 32]> {
181 validate_key_name(name)?;
182 let mut key = [0u8; 32];
183 rand::rngs::OsRng.fill_bytes(&mut key);
184 self.extra_keys.insert(name.to_string(), key);
185 Ok(key)
186 }
187
188 pub fn get_key(&self, name: &str) -> Option<&[u8; 32]> {
190 self.extra_keys.get(name)
191 }
192
193 pub fn remove_key(&mut self, name: &str) -> Option<[u8; 32]> {
195 self.extra_keys.remove(name)
196 }
197
198 pub fn extra_key_names(&self) -> impl Iterator<Item = &str> {
200 self.extra_keys.keys().map(String::as_str)
201 }
202
203 fn to_json_bytes(&self) -> Result<Vec<u8>> {
206 let wire = BundleJson {
207 iroh: B64.encode(self.iroh_secret_key),
208 ipns: B64.encode(self.ipns_secret_key),
209 did_signing: B64.encode(self.did_signing_key),
210 did_encryption: B64.encode(self.did_encryption_key),
211 extra: self
212 .extra_keys
213 .iter()
214 .map(|(k, v)| (k.clone(), B64.encode(v)))
215 .collect(),
216 };
217 serde_json::to_vec(&wire).map_err(|e| Error::Secrets(e.to_string()))
218 }
219
220 fn from_json_bytes(mut data: Vec<u8>) -> Result<Self> {
221 let wire: BundleJson = serde_json::from_slice(&data)
222 .map_err(|e| Error::Secrets(format!("failed to parse bundle JSON: {e}")))?;
223
224 data.zeroize();
225
226 let decode = |s: &str, field: &str| -> Result<[u8; 32]> {
227 let bytes = B64
228 .decode(s)
229 .map_err(|e| Error::Secrets(format!("base64 decode error in '{field}': {e}")))?;
230 bytes
231 .as_slice()
232 .try_into()
233 .map_err(|_| Error::Secrets(format!("'{field}' must be exactly 32 bytes")))
234 };
235
236 let mut extra_keys = HashMap::with_capacity(wire.extra.len());
237 for (name, val) in &wire.extra {
238 extra_keys.insert(name.clone(), decode(val, name)?);
239 }
240
241 Ok(Self {
242 iroh_secret_key: decode(&wire.iroh, "iroh")?,
243 ipns_secret_key: decode(&wire.ipns, "ipns")?,
244 did_signing_key: decode(&wire.did_signing, "did_signing")?,
245 did_encryption_key: decode(&wire.did_encryption, "did_encryption")?,
246 extra_keys,
247 })
248 }
249
250 pub fn encrypt(&self, passphrase: &str) -> Result<Vec<u8>> {
256 let mut salt = [0u8; 16];
257 rand::rngs::OsRng.fill_bytes(&mut salt);
258
259 let mut key_bytes = [0u8; 32];
260 Argon2::default()
261 .hash_password_into(passphrase.as_bytes(), &salt, &mut key_bytes)
262 .map_err(|e| Error::Secrets(e.to_string()))?;
263
264 let mut nonce_bytes = [0u8; 12];
265 rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
266 let nonce = *chacha20poly1305::Nonce::from_slice(&nonce_bytes);
267
268 let cipher = ChaCha20Poly1305::new_from_slice(&key_bytes)
269 .map_err(|e| Error::Secrets(e.to_string()))?;
270
271 let mut plaintext = self.to_json_bytes()?;
272 let ciphertext = cipher
273 .encrypt(&nonce, plaintext.as_slice())
274 .map_err(|e| Error::Secrets(e.to_string()))?;
275
276 plaintext.zeroize();
277 key_bytes.zeroize();
278
279 let mut out = Vec::with_capacity(16 + 12 + ciphertext.len());
280 out.extend_from_slice(&salt);
281 out.extend_from_slice(&nonce_bytes);
282 out.extend_from_slice(&ciphertext);
283 Ok(out)
284 }
285
286 pub fn decrypt(data: &[u8], passphrase: &str) -> Result<Self> {
291 if data.len() < 28 {
292 return Err(Error::Secrets("secret bundle too short".to_string()));
293 }
294
295 let salt = &data[0..16];
296 let nonce_bytes: [u8; 12] = data[16..28]
297 .try_into()
298 .map_err(|_| Error::Secrets("malformed bundle nonce".to_string()))?;
299 let ciphertext = &data[28..];
300
301 let mut key_bytes = [0u8; 32];
302 Argon2::default()
303 .hash_password_into(passphrase.as_bytes(), salt, &mut key_bytes)
304 .map_err(|e| Error::Secrets(e.to_string()))?;
305
306 let nonce = *chacha20poly1305::Nonce::from_slice(&nonce_bytes);
307 let cipher = ChaCha20Poly1305::new_from_slice(&key_bytes)
308 .map_err(|e| Error::Secrets(e.to_string()))?;
309 let plaintext = cipher
310 .decrypt(&nonce, ciphertext)
311 .map_err(|_| Error::Secrets("decryption failed (wrong passphrase?)".to_string()))?;
312
313 key_bytes.zeroize();
314
315 Self::from_json_bytes(plaintext)
316 }
317
318 #[cfg(not(target_arch = "wasm32"))]
320 pub fn load(path: &std::path::Path, passphrase: &str) -> Result<Self> {
321 let data = std::fs::read(path)
322 .map_err(|e| Error::Secrets(format!("failed to read {}: {e}", path.display())))?;
323 Self::decrypt(&data, passphrase)
324 }
325
326 #[cfg(not(target_arch = "wasm32"))]
328 pub fn save(&self, path: &std::path::Path, passphrase: &str) -> Result<()> {
329 let encrypted = self.encrypt(passphrase)?;
330 super::write_secure(path, &encrypted)
331 }
332
333 pub fn generate_passphrase() -> String {
335 use rand::distributions::{Alphanumeric, DistString};
336 Alphanumeric.sample_string(&mut rand::rngs::OsRng, 43)
337 }
338
339 pub fn generate_identity(&self) -> Result<crate::GeneratedIdentity> {
348 use crate::{
349 identity::build_identity_from_keys, ipns_from_secret, Did, EncryptionKey, SigningKey,
350 };
351 let ipns = ipns_from_secret(self.ipns_secret_key)
352 .map_err(|e| Error::Secrets(format!("ipns derivation failed: {e}")))?;
353 let sign_did = Did::new_url(&ipns, Some("sign"))
354 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
355 let enc_did = Did::new_url(&ipns, Some("enc"))
356 .map_err(|e| Error::Secrets(format!("enc did: {e}")))?;
357 let signing_key = SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
358 .map_err(|e| Error::Secrets(format!("signing key: {e}")))?;
359 let encryption_key =
360 EncryptionKey::from_private_key_bytes(enc_did, self.did_encryption_key)
361 .map_err(|e| Error::Secrets(format!("encryption key: {e}")))?;
362 build_identity_from_keys(&ipns, &signing_key, &encryption_key)
363 .map_err(|e| Error::Secrets(format!("identity generation failed: {e}")))
364 }
365
366 pub fn build_document(&self, ext: crate::doc::MaExtension) -> Result<crate::Document> {
383 use crate::{ipns_from_secret, Did, SigningKey};
384 let identity = self.generate_identity()?;
385 let mut document = identity.document;
386 let ipns = ipns_from_secret(self.ipns_secret_key)
387 .map_err(|e| Error::Secrets(format!("ipns derivation: {e}")))?;
388 let sign_did = Did::new_url(&ipns, Some("sign"))
389 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
390 let signing_key = SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
391 .map_err(|e| Error::Secrets(format!("signing key: {e}")))?;
392 let vm = document
393 .get_verification_method_by_id(&document.assertion_method[0].clone())
394 .map_err(|e| Error::Secrets(format!("assertion vm: {e}")))?;
395 let vm = vm.clone();
396 document.set_ma_extension(ext);
397 document
398 .sign(&signing_key, &vm)
399 .map_err(|e| Error::Secrets(format!("sign: {e}")))?;
400 Ok(document)
401 }
402
403 pub fn signing_key(&self) -> Result<crate::SigningKey> {
409 use crate::{ipns_from_secret, Did, SigningKey};
410 let ipns = ipns_from_secret(self.ipns_secret_key)
411 .map_err(|e| Error::Secrets(format!("ipns derivation: {e}")))?;
412 let sign_did = Did::new_url(&ipns, Some("sign"))
413 .map_err(|e| Error::Secrets(format!("sign did: {e}")))?;
414 SigningKey::from_private_key_bytes(sign_did, self.did_signing_key)
415 .map_err(|e| Error::Secrets(format!("signing key: {e}")))
416 }
417}
418
419fn validate_key_name(name: &str) -> Result<()> {
422 if name.is_empty() {
423 return Err(Error::Secrets("key name must not be empty".to_string()));
424 }
425 if RESERVED.contains(&name) {
426 return Err(Error::Secrets(format!(
427 "key name '{name}' is reserved for a standard key"
428 )));
429 }
430 Ok(())
431}
432
433#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn roundtrip_standard_keys() {
441 let bundle = SecretBundle::generate();
442 let passphrase = "test-passphrase-1234";
443 let encrypted = bundle.encrypt(passphrase).unwrap();
444 let restored = SecretBundle::decrypt(&encrypted, passphrase).unwrap();
445 assert_eq!(bundle.iroh_secret_key, restored.iroh_secret_key);
446 assert_eq!(bundle.ipns_secret_key, restored.ipns_secret_key);
447 assert_eq!(bundle.did_signing_key, restored.did_signing_key);
448 assert_eq!(bundle.did_encryption_key, restored.did_encryption_key);
449 }
450
451 #[test]
452 fn roundtrip_with_extra_keys() {
453 let mut bundle = SecretBundle::generate();
454 bundle.generate_key("my_service").unwrap();
455 bundle.generate_key("another_key").unwrap();
456
457 let passphrase = "extra-keys-test";
458 let encrypted = bundle.encrypt(passphrase).unwrap();
459 let restored = SecretBundle::decrypt(&encrypted, passphrase).unwrap();
460
461 assert_eq!(bundle.get_key("my_service"), restored.get_key("my_service"));
462 assert_eq!(
463 bundle.get_key("another_key"),
464 restored.get_key("another_key")
465 );
466 }
467
468 #[test]
469 fn reserved_name_rejected() {
470 let mut bundle = SecretBundle::generate();
471 assert!(bundle.add_key("iroh", [0u8; 32]).is_err());
472 assert!(bundle.add_key("did_signing", [0u8; 32]).is_err());
473 }
474
475 #[test]
476 fn empty_name_rejected() {
477 let mut bundle = SecretBundle::generate();
478 assert!(bundle.add_key("", [0u8; 32]).is_err());
479 }
480
481 #[test]
482 fn wrong_passphrase_fails() {
483 let bundle = SecretBundle::generate();
484 let encrypted = bundle.encrypt("correct").unwrap();
485 assert!(SecretBundle::decrypt(&encrypted, "wrong").is_err());
486 }
487}