1use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
11
12use crate::{from_hex, to_hex};
13
14pub struct Identity {
15 key: SigningKey,
16}
17
18pub fn dir(home: &Path) -> PathBuf {
19 home.join("mesh")
20}
21
22pub fn path(home: &Path) -> PathBuf {
23 dir(home).join("node.key")
24}
25
26pub fn public_path(home: &Path) -> PathBuf {
30 borg::msg::node_pub_path(home)
31}
32
33impl Identity {
34 pub fn load_or_create(home: &Path) -> Result<Identity, String> {
37 let p = path(home);
38 let me = match std::fs::read_to_string(&p) {
39 Ok(t) => Identity { key: SigningKey::from_bytes(&from_hex::<32>(&t).map_err(|e| format!("{}: {e}", p.display()))?) },
40 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
41 let seed = iroh::SecretKey::generate().to_bytes();
42 write_private(&p, &to_hex(&seed))?;
43 Identity { key: SigningKey::from_bytes(&seed) }
44 }
45 Err(e) => return Err(format!("{}: {e}", p.display())),
46 };
47 me.publish(home)?;
48 Ok(me)
49 }
50
51 fn publish(&self, home: &Path) -> Result<(), String> {
54 let (p, key) = (public_path(home), self.public());
55 if std::fs::read_to_string(&p).is_ok_and(|t| t.trim() == key) {
56 return Ok(());
57 }
58 let tmp = p.with_extension(format!("pub.{}.tmp", std::process::id()));
59 std::fs::write(&tmp, format!("{key}\n")).map_err(|e| format!("{}: {e}", tmp.display()))?;
60 std::fs::rename(&tmp, &p).map_err(|e| format!("{}: {e}", p.display()))
61 }
62
63 pub fn public(&self) -> String {
65 to_hex(self.key.verifying_key().as_bytes())
66 }
67
68 pub fn sign(&self, msg: &[u8]) -> String {
69 to_hex(&self.key.sign(msg).to_bytes())
70 }
71
72 pub fn iroh_secret(&self) -> iroh::SecretKey {
73 iroh::SecretKey::from_bytes(&self.key.to_bytes())
74 }
75}
76
77pub fn check_key(key: &str) -> Result<VerifyingKey, String> {
79 VerifyingKey::from_bytes(&from_hex::<32>(key)?).map_err(|e| format!("bad key {key}: {e}"))
80}
81
82pub fn verify(key: &str, msg: &[u8], sig: &str) -> Result<(), String> {
84 let k = check_key(key)?;
85 let s = Signature::from_bytes(&from_hex::<64>(sig)?);
86 k.verify_strict(msg, &s).map_err(|_| "bad signature".to_string())
87}
88
89fn write_private(p: &Path, text: &str) -> Result<(), String> {
91 use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
92 let d = p.parent().unwrap_or(Path::new("."));
93 std::fs::DirBuilder::new().recursive(true).mode(0o700).create(d).map_err(|e| format!("{}: {e}", d.display()))?;
94 let mut f = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(p).map_err(|e| format!("{}: {e}", p.display()))?;
95 f.write_all(text.as_bytes()).map_err(|e| format!("{}: {e}", p.display()))
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::os::unix::fs::PermissionsExt;
102
103 #[test]
104 fn created_once_private_and_stable() {
105 let home = std::env::temp_dir().join(format!("borg-mesh-id-{}", std::process::id()));
106 let _ = std::fs::remove_dir_all(&home);
107 let a = Identity::load_or_create(&home).unwrap();
108 let b = Identity::load_or_create(&home).unwrap();
109 assert_eq!(a.public(), b.public(), "the key is created once");
110 assert_eq!(std::fs::read_to_string(public_path(&home)).unwrap().trim(), a.public(), "published for borg");
111 assert_eq!(std::fs::metadata(path(&home)).unwrap().permissions().mode() & 0o777, 0o600);
112 assert_eq!(to_hex(a.iroh_secret().public().as_bytes()), a.public(), "iroh id == membership key");
113 let sig = a.sign(b"hello");
114 assert!(verify(&a.public(), b"hello", &sig).is_ok());
115 assert!(verify(&a.public(), b"hellO", &sig).is_err());
116 let _ = std::fs::remove_dir_all(&home);
117 }
118}