use std::fs;
use std::path::{Path, PathBuf};
use russh::keys::ssh_key::PublicKey;
use crate::config::StateConfig;
use crate::error::{Result, StorageError};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TrustEventKind {
ServerKeyLearned,
ClientKeyAuthorized,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustEvent {
pub kind: TrustEventKind,
pub path: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustRecord {
pub exists: bool,
pub path: PathBuf,
pub public_key_openssh: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustSummary {
pub known_servers: Vec<TrustRecord>,
pub authorized_clients: Vec<TrustRecord>,
}
fn ensure_trust_dirs(state: &StateConfig) -> Result<()> {
let trust_dir = state.root().join("trust");
let servers_dir = trust_dir.join("servers");
let clients_dir = trust_dir.join("clients");
for dir in [&trust_dir, &servers_dir, &clients_dir] {
if !dir.exists() {
fs::create_dir_all(dir).map_err(|source| StorageError::DirectoryCreate {
path: dir.clone(),
source,
})?;
}
}
Ok(())
}
fn server_key_path(state: &StateConfig, node_id: &str) -> PathBuf {
let safe_id = node_id.replace(['/', '\\', '.', ':'], "_");
state
.root()
.join("trust")
.join("servers")
.join(format!("{}.pub", safe_id))
}
fn client_key_path(state: &StateConfig, node_id: &str) -> PathBuf {
let safe_id = node_id.replace(['/', '\\', '.', ':'], "_");
state
.root()
.join("trust")
.join("clients")
.join(format!("{}.pub", safe_id))
}
fn write_public_key(path: &Path, key: &PublicKey) -> Result<()> {
let content = key
.to_openssh()
.map_err(|source| StorageError::PublicKeyFormat { source })?;
crate::storage::utils::atomic_write_secure(path, content.as_bytes())
}
pub fn load_known_server(state: &StateConfig, node_id: &str) -> Result<Option<PublicKey>> {
let path = server_key_path(state, node_id);
if path.exists() {
PublicKey::read_openssh_file(&path)
.map(Some)
.map_err(|source| {
StorageError::PublicKeyRead {
path: path.clone(),
source,
}
.into()
})
} else {
let legacy_path = state.root().join("trust/known_server.pub");
if legacy_path.exists() {
PublicKey::read_openssh_file(&legacy_path)
.map(Some)
.map_err(|source| {
StorageError::PublicKeyRead {
path: legacy_path.clone(),
source,
}
.into()
})
} else {
Ok(None)
}
}
}
pub fn load_authorized_client(state: &StateConfig, node_id: &str) -> Result<Option<PublicKey>> {
let path = client_key_path(state, node_id);
if path.exists() {
PublicKey::read_openssh_file(&path)
.map(Some)
.map_err(|source| {
StorageError::PublicKeyRead {
path: path.clone(),
source,
}
.into()
})
} else {
let legacy_path = state.root().join("trust/authorized_client.pub");
if legacy_path.exists() {
PublicKey::read_openssh_file(&legacy_path)
.map(Some)
.map_err(|source| {
StorageError::PublicKeyRead {
path: legacy_path.clone(),
source,
}
.into()
})
} else {
Ok(None)
}
}
}
pub fn load_all_authorized_clients(state: &StateConfig) -> Result<Vec<(String, PublicKey)>> {
let mut keys = Vec::new();
let clients_dir = state.root().join("trust").join("clients");
if clients_dir.exists() {
let entries = fs::read_dir(&clients_dir).map_err(|source| StorageError::DirectoryRead {
path: clients_dir.clone(),
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| StorageError::DirectoryEntryRead {
path: clients_dir.clone(),
source,
})?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "pub") {
if let Some(stem) = path.file_stem() {
let node_id = stem.to_string_lossy().to_string();
if let Ok(key) = PublicKey::read_openssh_file(&path) {
keys.push((node_id, key));
}
}
}
}
}
let legacy_path = state.root().join("trust/authorized_client.pub");
if legacy_path.exists() {
if let Ok(key) = PublicKey::read_openssh_file(&legacy_path) {
keys.push(("legacy".to_string(), key));
}
}
Ok(keys)
}
pub fn write_known_server(
state: &StateConfig,
node_id: &str,
key: &PublicKey,
) -> Result<TrustEvent> {
ensure_trust_dirs(state)?;
let path = server_key_path(state, node_id);
write_public_key(&path, key)?;
Ok(TrustEvent {
kind: TrustEventKind::ServerKeyLearned,
path,
})
}
pub fn write_authorized_client(
state: &StateConfig,
node_id: &str,
key: &PublicKey,
) -> Result<TrustEvent> {
ensure_trust_dirs(state)?;
let path = client_key_path(state, node_id);
write_public_key(&path, key)?;
Ok(TrustEvent {
kind: TrustEventKind::ClientKeyAuthorized,
path,
})
}
pub fn reset_known_server(state: &StateConfig, node_id: &str) -> Result<bool> {
remove_if_exists(&server_key_path(state, node_id))
}
pub fn reset_authorized_client(state: &StateConfig, node_id: &str) -> Result<bool> {
remove_if_exists(&client_key_path(state, node_id))
}
fn remove_if_exists(path: &Path) -> Result<bool> {
match fs::remove_file(path) {
Ok(()) => Ok(true),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(StorageError::FileDelete {
path: path.to_path_buf(),
source,
}
.into()),
}
}
pub fn inspect_trust(state: &StateConfig) -> Result<TrustSummary> {
let servers_dir = state.root().join("trust").join("servers");
let clients_dir = state.root().join("trust").join("clients");
let mut known_servers = read_trust_dir(&servers_dir)?;
let mut authorized_clients = read_trust_dir(&clients_dir)?;
let legacy_server = state.root().join("trust/known_server.pub");
if legacy_server.exists() {
known_servers.push(inspect_public_key_record(legacy_server)?);
}
let legacy_client = state.root().join("trust/authorized_client.pub");
if legacy_client.exists() {
authorized_clients.push(inspect_public_key_record(legacy_client)?);
}
Ok(TrustSummary {
known_servers,
authorized_clients,
})
}
fn read_trust_dir(dir: &Path) -> Result<Vec<TrustRecord>> {
if !dir.exists() {
return Ok(Vec::new());
}
let mut records = Vec::new();
let entries = fs::read_dir(dir).map_err(|source| StorageError::DirectoryRead {
path: dir.to_path_buf(),
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| StorageError::DirectoryEntryRead {
path: dir.to_path_buf(),
source,
})?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "pub") {
records.push(inspect_public_key_record(path)?);
}
}
Ok(records)
}
fn inspect_public_key_record(path: PathBuf) -> Result<TrustRecord> {
if !path.exists() {
return Ok(TrustRecord {
exists: false,
path,
public_key_openssh: None,
});
}
let key =
PublicKey::read_openssh_file(&path).map_err(|source| StorageError::PublicKeyRead {
path: path.clone(),
source,
})?;
Ok(TrustRecord {
exists: true,
path,
public_key_openssh: Some(
key.to_openssh()
.map_err(|source| StorageError::PublicKeyFormat { source })?,
),
})
}