1use std::{
2 collections::HashMap,
3 fs,
4 io::{self, Read, Write},
5 path::{Path, PathBuf},
6 sync::{Arc, RwLock},
7};
8
9use rand::RngCore;
10use serde::{Deserialize, Serialize};
11use sha2::{Digest as Sha2Digest, Sha256};
12
13use crate::attestation::{Ed25519Signer, Signer};
14
15pub type KeyId = String;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct KeyInfo {
22 pub id: KeyId,
23 pub algorithm: String, pub is_default: bool,
25 pub created_at: String, pub fingerprint: String,
28 pub public_key: Vec<u8>, }
30
31#[derive(Debug)]
33pub enum KeyError {
34 Io(io::Error),
35 Json(serde_json::Error),
36 Crypto(String),
37 NotFound(KeyId),
38 EmptyKeyId,
39 NoDefaultKey,
40}
41
42impl std::fmt::Display for KeyError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Io(e) => write!(f, "keys io: {}", e),
46 Self::Json(e) => write!(f, "keys json: {}", e),
47 Self::Crypto(e) => write!(f, "keys crypto: {}", e),
48 Self::NotFound(k) => write!(f, "key not found: {}", k),
49 Self::EmptyKeyId => write!(f, "key id must not be empty"),
50 Self::NoDefaultKey => write!(f, "no default key — run treeship init"),
51 }
52 }
53}
54
55impl std::error::Error for KeyError {}
56impl From<io::Error> for KeyError { fn from(e: io::Error) -> Self { Self::Io(e) } }
57impl From<serde_json::Error> for KeyError { fn from(e: serde_json::Error) -> Self { Self::Json(e) } }
58
59#[derive(Serialize, Deserialize)]
63struct EncryptedEntry {
64 id: KeyId,
65 algorithm: String,
66 created_at: String,
67 public_key: Vec<u8>,
68 enc_priv_key: Vec<u8>,
70 nonce: Vec<u8>,
72}
73
74#[derive(Serialize, Deserialize, Default)]
76struct Manifest {
77 default_key_id: Option<KeyId>,
78 key_ids: Vec<KeyId>,
79}
80
81pub struct Store {
91 dir: PathBuf,
92 machine_key: [u8; 32],
93 cache: Arc<RwLock<HashMap<KeyId, EncryptedEntry>>>,
95}
96
97impl Store {
98 pub fn open(dir: impl AsRef<Path>) -> Result<Self, KeyError> {
100 let dir = dir.as_ref().to_path_buf();
101 fs::create_dir_all(&dir)?;
102
103 let machine_key = derive_machine_key(&dir)?;
104
105 Ok(Self {
106 dir,
107 machine_key,
108 cache: Arc::new(RwLock::new(HashMap::new())),
109 })
110 }
111
112 pub fn generate(&self, set_default: bool) -> Result<KeyInfo, KeyError> {
116 let key_id = new_key_id();
117
118 let signer = Ed25519Signer::generate(&key_id)
119 .map_err(|e| KeyError::Crypto(e.to_string()))?;
120
121 let secret = signer.secret_bytes();
122 let pub_key = signer.public_key_bytes();
123
124 let (enc, nonce) = aes_gcm_encrypt(&self.machine_key, &secret)
125 .map_err(|e| KeyError::Crypto(e))?;
126
127 let entry = EncryptedEntry {
128 id: key_id.clone(),
129 algorithm: "ed25519".into(),
130 created_at: crate::statements::unix_to_rfc3339(unix_now()),
131 public_key: pub_key.clone(),
132 enc_priv_key: enc,
133 nonce,
134 };
135
136 self.write_entry(&entry)?;
137
138 let mut manifest = self.read_manifest()?;
140 manifest.key_ids.push(key_id.clone());
141 if set_default || manifest.default_key_id.is_none() {
142 manifest.default_key_id = Some(key_id.clone());
143 }
144 self.write_manifest(&manifest)?;
145
146 self.cache.write().unwrap().insert(key_id.clone(), entry);
148
149 Ok(KeyInfo {
150 id: key_id,
151 algorithm: "ed25519".into(),
152 is_default: manifest.default_key_id.as_deref() == Some(&manifest.key_ids.last().unwrap_or(&String::new())),
153 created_at: crate::statements::unix_to_rfc3339(unix_now()),
154 fingerprint: fingerprint(&pub_key),
155 public_key: pub_key,
156 })
157 }
158
159 pub fn default_signer(&self) -> Result<Box<dyn Signer>, KeyError> {
161 let manifest = self.read_manifest()?;
162 let id = manifest.default_key_id.ok_or(KeyError::NoDefaultKey)?;
163 self.signer(&id)
164 }
165
166 pub fn signer(&self, id: &str) -> Result<Box<dyn Signer>, KeyError> {
168 let entry = self.load_entry(id)?;
169
170 let secret = aes_gcm_decrypt(&self.machine_key, &entry.enc_priv_key, &entry.nonce)
171 .map_err(|e| self.enrich_crypto_error(e))?;
172
173 let secret_arr: [u8; 32] = secret.try_into()
174 .map_err(|_| KeyError::Crypto("decrypted key is wrong length".into()))?;
175
176 let signer = Ed25519Signer::from_bytes(&entry.id, &secret_arr)
177 .map_err(|e| KeyError::Crypto(e.to_string()))?;
178
179 Ok(Box::new(signer))
180 }
181
182 fn enrich_crypto_error(&self, raw: String) -> KeyError {
194 if !raw.contains("MAC verification failed") {
197 return KeyError::Crypto(raw);
198 }
199
200 let legacy_seed_dot = self.dir.join(".machineseed");
201 let legacy_seed = self.dir.join("machine_seed");
202 let has_legacy_seed = legacy_seed_dot.exists() || legacy_seed.exists();
203
204 let diagnosis = if has_legacy_seed {
205 "your keystore was created by an older Treeship version whose \
206 machine-key derivation has since changed. The ciphertext is \
207 intact but cannot be decrypted under the current derivation."
208 } else {
209 "the keystore cannot be decrypted. Usual causes: the key file \
210 was copied from a different machine, the hostname or username \
211 changed, or the file was corrupted."
212 };
213
214 let ts_dir = std::env::var("HOME")
217 .map(|h| format!("{h}/.treeship"))
218 .unwrap_or_else(|_| "~/.treeship".into());
219
220 let msg = format!(
225 "{raw}\n\n \
226 Diagnosis: {diagnosis}\n\n \
227 Recovery (nondestructive -- the old keystore is moved aside, \
228 not deleted; any sealed .treeship packages you produced remain \
229 verifiable since their receipts embed the old public key):\n\n \
230 mv {ts_dir} {ts_dir}.bak.$(date +%s)\n \
231 treeship init\n"
232 );
233
234 KeyError::Crypto(msg)
235 }
236
237 pub fn default_key_id(&self) -> Result<KeyId, KeyError> {
239 self.read_manifest()?
240 .default_key_id
241 .ok_or(KeyError::NoDefaultKey)
242 }
243
244 pub fn list(&self) -> Result<Vec<KeyInfo>, KeyError> {
246 let manifest = self.read_manifest()?;
247 let default = manifest.default_key_id.as_deref().unwrap_or("");
248
249 manifest.key_ids.iter().map(|id| {
250 let entry = self.load_entry(id)?;
251 Ok(KeyInfo {
252 id: entry.id.clone(),
253 algorithm: entry.algorithm.clone(),
254 is_default: entry.id == default,
255 created_at: entry.created_at.clone(),
256 fingerprint: fingerprint(&entry.public_key),
257 public_key: entry.public_key.clone(),
258 })
259 }).collect()
260 }
261
262 pub fn set_default(&self, id: &str) -> Result<(), KeyError> {
264 self.load_entry(id)?;
266 let mut manifest = self.read_manifest()?;
267 manifest.default_key_id = Some(id.to_string());
268 self.write_manifest(&manifest)
269 }
270
271 pub fn public_key(&self, id: &str) -> Result<Vec<u8>, KeyError> {
273 Ok(self.load_entry(id)?.public_key)
274 }
275
276 fn load_entry(&self, id: &str) -> Result<EncryptedEntry, KeyError> {
279 if let Ok(cache) = self.cache.read() {
281 if let Some(entry) = cache.get(id) {
282 return Ok(EncryptedEntry {
284 id: entry.id.clone(),
285 algorithm: entry.algorithm.clone(),
286 created_at: entry.created_at.clone(),
287 public_key: entry.public_key.clone(),
288 enc_priv_key: entry.enc_priv_key.clone(),
289 nonce: entry.nonce.clone(),
290 });
291 }
292 }
293 self.read_entry(id)
294 }
295
296 fn entry_path(&self, id: &str) -> PathBuf {
297 self.dir.join(format!("{}.json", id))
298 }
299
300 fn write_entry(&self, entry: &EncryptedEntry) -> Result<(), KeyError> {
301 let path = self.entry_path(&entry.id);
302 let json = serde_json::to_vec_pretty(entry)?;
303 write_file_600(&path, &json)?;
304 Ok(())
305 }
306
307 fn read_entry(&self, id: &str) -> Result<EncryptedEntry, KeyError> {
308 let path = self.entry_path(id);
309 if !path.exists() {
310 return Err(KeyError::NotFound(id.to_string()));
311 }
312 let bytes = fs::read(&path)?;
313 let entry: EncryptedEntry = serde_json::from_slice(&bytes)?;
314 Ok(entry)
315 }
316
317 fn manifest_path(&self) -> PathBuf {
318 self.dir.join("manifest.json")
319 }
320
321 fn read_manifest(&self) -> Result<Manifest, KeyError> {
322 let path = self.manifest_path();
323 if !path.exists() {
324 return Ok(Manifest::default());
325 }
326 let bytes = fs::read(&path)?;
327 Ok(serde_json::from_slice(&bytes)?)
328 }
329
330 fn write_manifest(&self, m: &Manifest) -> Result<(), KeyError> {
331 let json = serde_json::to_vec_pretty(m)?;
332 write_file_600(&self.manifest_path(), &json)?;
333 Ok(())
334 }
335}
336
337pub fn aes_gcm_encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
342 use sha2::Sha256;
353
354 let mut nonce = [0u8; 12];
355 rand::thread_rng().fill_bytes(&mut nonce);
356
357 let mut enc_key_input = key.to_vec();
359 enc_key_input.extend_from_slice(&nonce);
360 enc_key_input.extend_from_slice(b"enc");
361 let enc_key = Sha256::digest(&enc_key_input);
362
363 let mut mac_key_input = key.to_vec();
364 mac_key_input.extend_from_slice(&nonce);
365 mac_key_input.extend_from_slice(b"mac");
366 let mac_key = Sha256::digest(&mac_key_input);
367
368 let ciphertext: Vec<u8> = plaintext.iter().enumerate().map(|(i, &b)| {
370 let mut block_input = enc_key.to_vec();
371 block_input.extend_from_slice(&(i as u64).to_le_bytes());
372 let block = Sha256::digest(&block_input);
373 b ^ block[i % 32]
374 }).collect();
375
376 let mut mac_input = mac_key.to_vec();
378 mac_input.extend_from_slice(&nonce);
379 mac_input.extend_from_slice(&ciphertext);
380 let mac = Sha256::digest(&mac_input);
381
382 let mut out = Vec::with_capacity(12 + 32 + ciphertext.len());
384 out.extend_from_slice(&nonce);
385 out.extend_from_slice(&mac);
386 out.extend_from_slice(&ciphertext);
387
388 Ok((out, nonce.to_vec()))
389}
390
391pub fn aes_gcm_decrypt(key: &[u8; 32], enc_data: &[u8], _nonce_unused: &[u8]) -> Result<Vec<u8>, String> {
392 if enc_data.len() < 44 {
393 return Err("ciphertext too short".into());
394 }
395 use sha2::Sha256;
396
397 let nonce = &enc_data[..12];
398 let stored_mac = &enc_data[12..44];
399 let ciphertext = &enc_data[44..];
400
401 let nonce_arr: [u8; 12] = nonce.try_into().unwrap();
402
403 let mut enc_key_input = key.to_vec();
404 enc_key_input.extend_from_slice(&nonce_arr);
405 enc_key_input.extend_from_slice(b"enc");
406 let enc_key = Sha256::digest(&enc_key_input);
407
408 let mut mac_key_input = key.to_vec();
409 mac_key_input.extend_from_slice(&nonce_arr);
410 mac_key_input.extend_from_slice(b"mac");
411 let mac_key = Sha256::digest(&mac_key_input);
412
413 let mut mac_input = mac_key.to_vec();
415 mac_input.extend_from_slice(&nonce_arr);
416 mac_input.extend_from_slice(ciphertext);
417 let computed_mac = Sha256::digest(&mac_input);
418
419 let mac_ok = stored_mac.iter().zip(computed_mac.iter())
421 .fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0;
422
423 if !mac_ok {
424 return Err("MAC verification failed — key file may be corrupt or wrong machine".into());
425 }
426
427 let plaintext: Vec<u8> = ciphertext.iter().enumerate().map(|(i, &b)| {
428 let mut block_input = enc_key.to_vec();
429 block_input.extend_from_slice(&(i as u64).to_le_bytes());
430 let block = Sha256::digest(&block_input);
431 b ^ block[i % 32]
432 }).collect();
433
434 Ok(plaintext)
435}
436
437pub fn derive_machine_key(store_dir: &Path) -> Result<[u8; 32], KeyError> {
440 if let Ok(id) = fs::read_to_string("/etc/machine-id") {
442 let trimmed = id.trim();
443 if !trimmed.is_empty() {
444 let mut h = Sha256::new();
445 h.update(trimmed.as_bytes());
446 h.update(store_dir.to_string_lossy().as_bytes());
447 return Ok(h.finalize().into());
448 }
449 }
450
451 #[cfg(target_os = "macos")]
463 {
464 let hostname = std::process::Command::new("hostname")
465 .output()
466 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
467 .unwrap_or_default();
468 let username = std::env::var("USER").unwrap_or_default();
469 if !hostname.is_empty() && !username.is_empty() {
470 let mut h = Sha256::new();
471 h.update(b"treeship-machine-key:");
472 h.update(hostname.as_bytes());
473 h.update(b":");
474 h.update(username.as_bytes());
475 h.update(b":");
476 h.update(store_dir.to_string_lossy().as_bytes());
477 return Ok(h.finalize().into());
478 }
479 }
480
481 let home = std::env::var("HOME")
484 .map(std::path::PathBuf::from)
485 .map_err(|_| KeyError::Crypto("HOME not set".to_string()))?;
486 let seed_path = home.join(".treeship").join("machine_seed");
487 let seed = if seed_path.exists() {
488 fs::read_to_string(&seed_path).map_err(KeyError::Io)?
489 } else {
490 let mut bytes = [0u8; 32];
491 rand::thread_rng().fill_bytes(&mut bytes);
492 let seed_hex = hex_encode(&bytes);
493 let _ = fs::create_dir_all(seed_path.parent().unwrap_or(Path::new(".")));
494 fs::write(&seed_path, &seed_hex).map_err(KeyError::Io)?;
495 #[cfg(unix)]
496 {
497 use std::os::unix::fs::PermissionsExt;
498 let _ = fs::set_permissions(&seed_path, fs::Permissions::from_mode(0o600));
499 }
500 seed_hex
501 };
502
503 let mut h = Sha256::new();
504 h.update(b"treeship-machine-key-fallback:");
505 h.update(seed.trim().as_bytes());
506 h.update(b":");
507 h.update(store_dir.to_string_lossy().as_bytes());
508 Ok(h.finalize().into())
509}
510
511pub fn derive_machine_key_stable(store_dir: &Path) -> Result<[u8; 32], KeyError> {
515 if let Ok(id) = fs::read_to_string("/etc/machine-id") {
517 let trimmed = id.trim();
518 if !trimmed.is_empty() {
519 let mut h = Sha256::new();
520 h.update(b"treeship-machine-key-v2:");
521 h.update(trimmed.as_bytes());
522 h.update(b":");
523 h.update(store_dir.to_string_lossy().as_bytes());
524 return Ok(h.finalize().into());
525 }
526 }
527
528 #[cfg(target_os = "macos")]
531 {
532 if let Ok(output) = std::process::Command::new("ioreg")
533 .args(["-rd1", "-c", "IOPlatformExpertDevice"])
534 .output()
535 {
536 let stdout = String::from_utf8_lossy(&output.stdout);
537 for line in stdout.lines() {
538 if line.contains("IOPlatformSerialNumber") {
539 if let Some(serial) = line.split('"').nth(3) {
540 if !serial.is_empty() {
541 let mut h = Sha256::new();
542 h.update(b"treeship-machine-key-v2:");
543 h.update(serial.as_bytes());
544 h.update(b":");
545 h.update(store_dir.to_string_lossy().as_bytes());
546 return Ok(h.finalize().into());
547 }
548 }
549 }
550 }
551 }
552 }
553
554 let home = std::env::var("HOME")
557 .map(std::path::PathBuf::from)
558 .map_err(|_| KeyError::Crypto("HOME not set".to_string()))?;
559 let seed_dir = home.join(".treeship").join(".internal");
560 let _ = fs::create_dir_all(&seed_dir);
561 #[cfg(unix)]
562 {
563 use std::os::unix::fs::PermissionsExt;
564 let _ = fs::set_permissions(&seed_dir, fs::Permissions::from_mode(0o700));
565 }
566
567 let seed_path = seed_dir.join("machine_seed_v2");
568 let seed = if seed_path.exists() {
569 fs::read_to_string(&seed_path).map_err(KeyError::Io)?
570 } else {
571 let mut bytes = [0u8; 32];
572 rand::thread_rng().fill_bytes(&mut bytes);
573 let seed_hex = hex_encode(&bytes);
574 fs::write(&seed_path, &seed_hex).map_err(KeyError::Io)?;
575 #[cfg(unix)]
576 {
577 use std::os::unix::fs::PermissionsExt;
578 let _ = fs::set_permissions(&seed_path, fs::Permissions::from_mode(0o600));
579 }
580 seed_hex
581 };
582
583 let mut h = Sha256::new();
584 h.update(b"treeship-machine-key-v2-fallback:");
585 h.update(seed.trim().as_bytes());
586 h.update(b":");
587 h.update(store_dir.to_string_lossy().as_bytes());
588 Ok(h.finalize().into())
589}
590
591fn new_key_id() -> KeyId {
594 let mut b = [0u8; 8];
595 rand::thread_rng().fill_bytes(&mut b);
596 format!("key_{}", hex_encode(&b))
597}
598
599fn fingerprint(pub_key: &[u8]) -> String {
600 let h = Sha256::digest(pub_key);
601 hex_encode(&h[..8])
602}
603
604fn hex_encode(b: &[u8]) -> String {
605 b.iter().fold(String::new(), |mut s, byte| {
606 s.push_str(&format!("{:02x}", byte));
607 s
608 })
609}
610
611fn write_file_600(path: &Path, data: &[u8]) -> Result<(), KeyError> {
612 let mut f = fs::OpenOptions::new()
613 .write(true)
614 .create(true)
615 .truncate(true)
616 .open(path)?;
617 f.write_all(data)?;
618 #[cfg(unix)]
620 {
621 use std::os::unix::fs::PermissionsExt;
622 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
623 }
624 Ok(())
625}
626
627fn unix_now() -> u64 {
628 use std::time::{SystemTime, UNIX_EPOCH};
629 SystemTime::now()
630 .duration_since(UNIX_EPOCH)
631 .unwrap_or_default()
632 .as_secs()
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 fn temp_dir_path() -> PathBuf {
640 let mut p = std::env::temp_dir();
641 p.push(format!("treeship-test-{}", {
642 let mut b = [0u8; 4];
643 rand::thread_rng().fill_bytes(&mut b);
644 hex_encode(&b)
645 }));
646 p
647 }
648
649 fn make_store() -> (Store, PathBuf) {
650 let dir = temp_dir_path();
651 let store = Store::open(&dir).unwrap();
652 (store, dir)
653 }
654
655 fn cleanup(dir: PathBuf) {
656 let _ = fs::remove_dir_all(dir);
657 }
658
659 #[test]
660 fn generate_key() {
661 let (store, dir) = make_store();
662 let info = store.generate(true).unwrap();
663 assert!(info.id.starts_with("key_"));
664 assert_eq!(info.algorithm, "ed25519");
665 assert!(!info.fingerprint.is_empty());
666 assert_eq!(info.public_key.len(), 32);
667 cleanup(dir);
668 }
669
670 #[test]
671 fn default_signer_works() {
672 let (store, dir) = make_store();
673 store.generate(true).unwrap();
674 let signer = store.default_signer().unwrap();
675 assert!(!signer.key_id().is_empty());
676 let pae = crate::attestation::pae("text/plain", b"test");
677 let sig = signer.sign(&pae).unwrap();
678 assert_eq!(sig.len(), 64);
679 cleanup(dir);
680 }
681
682 #[test]
683 fn encrypt_decrypt_roundtrip() {
684 let key = [42u8; 32];
685 let plaintext = b"super secret private key material here!";
686 let (enc, nonce) = aes_gcm_encrypt(&key, plaintext).unwrap();
687 let dec = aes_gcm_decrypt(&key, &enc, &nonce).unwrap();
688 assert_eq!(dec, plaintext);
689 }
690
691 #[test]
692 fn decrypt_wrong_key_fails() {
693 let key = [42u8; 32];
694 let wrong = [99u8; 32];
695 let (enc, nonce) = aes_gcm_encrypt(&key, b"secret").unwrap();
696 assert!(aes_gcm_decrypt(&wrong, &enc, &nonce).is_err());
697 }
698
699 #[test]
700 fn persist_and_reload() {
701 let (store, dir) = make_store();
702 let info = store.generate(true).unwrap();
703
704 let store2 = Store::open(&dir).unwrap();
706 let signer = store2.signer(&info.id).unwrap();
707 assert_eq!(signer.key_id(), info.id);
708
709 let verifier = {
712 use crate::attestation::Verifier;
713 use ed25519_dalek::VerifyingKey;
714 let pk_bytes: [u8; 32] = info.public_key.try_into().unwrap();
715 let vk = VerifyingKey::from_bytes(&pk_bytes).unwrap();
716 let mut v = Verifier::new(std::collections::HashMap::new());
717 v.add_key(info.id.clone(), vk);
718 v
719 };
720
721 use crate::attestation::sign;
722 use crate::statements::ActionStatement;
723 let stmt = ActionStatement::new("agent://test", "tool.call");
724 let pt = crate::statements::payload_type("action");
725 let signed = sign(&pt, &stmt, signer.as_ref()).unwrap();
726 verifier.verify(&signed.envelope).unwrap();
727
728 cleanup(dir);
729 }
730
731 #[test]
732 fn list_keys() {
733 let (store, dir) = make_store();
734 store.generate(true).unwrap();
735 store.generate(false).unwrap();
736
737 let keys = store.list().unwrap();
738 assert_eq!(keys.len(), 2);
739 assert_eq!(keys.iter().filter(|k| k.is_default).count(), 1);
740 cleanup(dir);
741 }
742
743 #[test]
744 fn no_default_key_errors() {
745 let (store, dir) = make_store();
746 assert!(store.default_signer().is_err());
747 cleanup(dir);
748 }
749}