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.secret_key.public().to_string())
.field("secret_key", &"<redacted>")
.field("ssh_key", &"<redacted>")
.finish()
}
}
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,
})?
}
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(),
})?
} 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,
})
}