use std::fmt;
use std::fs;
use std::str::FromStr;
use iroh::SecretKey;
use russh::keys::ssh_key::PrivateKey;
use russh::keys::ssh_key::private::Ed25519Keypair;
use tokio::task;
use crate::config::StateConfig;
use crate::error::{Result, StorageError};
fn ensure_key_dir(state: &StateConfig) -> Result<()> {
let path = state.root().join("keys");
if !path.exists() {
fs::create_dir_all(&path).map_err(|source| StorageError::DirectoryCreate {
path: path.clone(),
source,
})?;
}
Ok(())
}
pub struct NodeIdentity {
pub secret_key: SecretKey,
pub ssh_key: PrivateKey,
}
impl fmt::Debug for NodeIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NodeIdentity")
.field("node_id", &self.node_id())
.field("secret_key", &"<redacted>")
.field("ssh_key", &"<redacted>")
.finish()
}
}
impl NodeIdentity {
pub fn node_id(&self) -> String {
self.secret_key.public().to_string()
}
}
const SECRET_KEY_FILE: &str = "keys/node.secret";
pub async fn load_or_generate_identity(state: &StateConfig) -> Result<NodeIdentity> {
let state = state.clone();
task::spawn_blocking(move || load_or_generate_identity_blocking(&state))
.await
.map_err(|source| StorageError::BlockingTaskFailed {
operation: "loading or generating identity",
source,
})?
}
pub fn load_secret_key(state: &StateConfig) -> Result<SecretKey> {
let path = state.root().join(SECRET_KEY_FILE);
if !path.exists() {
return Err(StorageError::FileRead {
path,
source: std::io::Error::new(std::io::ErrorKind::NotFound, "node secret not found"),
}
.into());
}
let raw = fs::read_to_string(&path).map_err(|source| StorageError::FileRead {
path: path.clone(),
source,
})?;
let key = SecretKey::from_str(raw.trim()).map_err(|e| StorageError::NodeSecretInvalid {
path: path.clone(),
details: e.to_string(),
source: Box::new(e),
})?;
Ok(key)
}
fn load_or_generate_identity_blocking(state: &StateConfig) -> Result<NodeIdentity> {
ensure_key_dir(state)?;
let path = state.root().join(SECRET_KEY_FILE);
let secret_key = if path.exists() {
let raw = fs::read_to_string(&path).map_err(|source| StorageError::FileRead {
path: path.clone(),
source,
})?;
SecretKey::from_str(raw.trim()).map_err(|e| StorageError::NodeSecretInvalid {
path: path.clone(),
details: e.to_string(),
source: Box::new(e),
})?
} else {
let secret_key = SecretKey::generate(&mut rand::rng());
let hex = secret_key
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
fs::write(&path, hex).map_err(|source| StorageError::FileWrite {
path: path.clone(),
source,
})?;
secret_key
};
let seed = secret_key.to_bytes();
let keypair = Ed25519Keypair::from_seed(&seed);
let ssh_key = PrivateKey::from(keypair);
Ok(NodeIdentity {
secret_key,
ssh_key,
})
}
pub fn delete_secret_key(state: &StateConfig) -> Result<bool> {
let path = state.root().join(SECRET_KEY_FILE);
if path.exists() {
std::fs::remove_file(&path).map_err(|source| StorageError::FileWrite {
path: path.clone(),
source,
})?;
Ok(true)
} else {
Ok(false)
}
}
pub fn save_secret_key(state: &StateConfig, key: &SecretKey) -> Result<()> {
ensure_key_dir(state)?;
let path = state.root().join(SECRET_KEY_FILE);
let hex = key
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
fs::write(&path, hex)
.map_err(|source| StorageError::FileWrite { path, source })
.map_err(Into::into)
}