use std::io::Write;
use std::path::{Path, PathBuf};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use crate::{from_hex, to_hex};
pub struct Identity {
key: SigningKey,
}
pub fn dir(home: &Path) -> PathBuf {
home.join("mesh")
}
pub fn path(home: &Path) -> PathBuf {
dir(home).join("node.key")
}
pub fn public_path(home: &Path) -> PathBuf {
borg::msg::node_pub_path(home)
}
impl Identity {
pub fn load_or_create(home: &Path) -> Result<Identity, String> {
let p = path(home);
let me = match std::fs::read_to_string(&p) {
Ok(t) => Identity { key: SigningKey::from_bytes(&from_hex::<32>(&t).map_err(|e| format!("{}: {e}", p.display()))?) },
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let seed = iroh::SecretKey::generate().to_bytes();
write_private(&p, &to_hex(&seed))?;
Identity { key: SigningKey::from_bytes(&seed) }
}
Err(e) => return Err(format!("{}: {e}", p.display())),
};
me.publish(home)?;
Ok(me)
}
fn publish(&self, home: &Path) -> Result<(), String> {
let (p, key) = (public_path(home), self.public());
if std::fs::read_to_string(&p).is_ok_and(|t| t.trim() == key) {
return Ok(());
}
let tmp = p.with_extension(format!("pub.{}.tmp", std::process::id()));
std::fs::write(&tmp, format!("{key}\n")).map_err(|e| format!("{}: {e}", tmp.display()))?;
std::fs::rename(&tmp, &p).map_err(|e| format!("{}: {e}", p.display()))
}
pub fn public(&self) -> String {
to_hex(self.key.verifying_key().as_bytes())
}
pub fn sign(&self, msg: &[u8]) -> String {
to_hex(&self.key.sign(msg).to_bytes())
}
pub fn iroh_secret(&self) -> iroh::SecretKey {
iroh::SecretKey::from_bytes(&self.key.to_bytes())
}
}
pub fn check_key(key: &str) -> Result<VerifyingKey, String> {
VerifyingKey::from_bytes(&from_hex::<32>(key)?).map_err(|e| format!("bad key {key}: {e}"))
}
pub fn verify(key: &str, msg: &[u8], sig: &str) -> Result<(), String> {
let k = check_key(key)?;
let s = Signature::from_bytes(&from_hex::<64>(sig)?);
k.verify_strict(msg, &s).map_err(|_| "bad signature".to_string())
}
fn write_private(p: &Path, text: &str) -> Result<(), String> {
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
let d = p.parent().unwrap_or(Path::new("."));
std::fs::DirBuilder::new().recursive(true).mode(0o700).create(d).map_err(|e| format!("{}: {e}", d.display()))?;
let mut f = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(p).map_err(|e| format!("{}: {e}", p.display()))?;
f.write_all(text.as_bytes()).map_err(|e| format!("{}: {e}", p.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn created_once_private_and_stable() {
let home = std::env::temp_dir().join(format!("borg-mesh-id-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&home);
let a = Identity::load_or_create(&home).unwrap();
let b = Identity::load_or_create(&home).unwrap();
assert_eq!(a.public(), b.public(), "the key is created once");
assert_eq!(std::fs::read_to_string(public_path(&home)).unwrap().trim(), a.public(), "published for borg");
assert_eq!(std::fs::metadata(path(&home)).unwrap().permissions().mode() & 0o777, 0o600);
assert_eq!(to_hex(a.iroh_secret().public().as_bytes()), a.public(), "iroh id == membership key");
let sig = a.sign(b"hello");
assert!(verify(&a.public(), b"hello", &sig).is_ok());
assert!(verify(&a.public(), b"hellO", &sig).is_err());
let _ = std::fs::remove_dir_all(&home);
}
}