use base64::Engine as _;
use base64::engine::general_purpose::STANDARD_NO_PAD;
use ed25519_dalek::{SigningKey, VerifyingKey};
use eyre::Report;
use eyre::eyre;
use getrandom;
use hex;
use rand::rngs::OsRng;
use serde_yaml;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use tracing::info;
use volli_core::config_dir;
pub fn default_secret_dir() -> PathBuf {
let mut base = config_dir();
base.push("profiles");
base.push("coordinator");
base
}
pub fn bootstrap_keypair(dir: Option<&Path>) -> Result<(), Report> {
let dir = dir.map(PathBuf::from).unwrap_or_else(default_secret_dir);
fs::create_dir_all(&dir)?;
let sk_path = dir.join("coord_sk");
let pk_path = dir.join("coord_pk");
if sk_path.exists() || pk_path.exists() {
return Err(eyre!("keypair already exists"));
}
let signing = SigningKey::generate(&mut OsRng);
let verifying: VerifyingKey = signing.verifying_key();
fs::write(&sk_path, hex::encode(signing.to_bytes()))?;
fs::write(&pk_path, hex::encode(verifying.to_bytes()))?;
let mut csk = [0u8; 32];
getrandom::getrandom(&mut csk)?;
fs::write(dir.join("csk"), hex::encode(csk))?;
fs::write(dir.join("csk_ver"), "1")?;
info!("Generated coordinator keypair at {}", dir.display());
Ok(())
}
pub fn load_signing_key(dir: Option<&Path>) -> Result<SigningKey, Report> {
let dir = dir.map(PathBuf::from).unwrap_or_else(default_secret_dir);
let data = fs::read(dir.join("coord_sk"))?;
let bytes = hex::decode(data)?;
let arr: [u8; 32] = bytes.as_slice().try_into().map_err(|_| eyre!("bad sk"))?;
Ok(SigningKey::from_bytes(&arr))
}
pub fn load_verifying_key(dir: Option<&Path>) -> Result<VerifyingKey, Report> {
let dir = dir.map(PathBuf::from).unwrap_or_else(default_secret_dir);
let data = fs::read(dir.join("coord_pk"))?;
let bytes = hex::decode(data)?;
let arr: [u8; 32] = bytes.as_slice().try_into().map_err(|_| eyre!("bad pk"))?;
Ok(VerifyingKey::from_bytes(&arr)?)
}
pub fn save_csk(profile: &str, csk: &[u8; 32], ver: u32) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("csk"), hex::encode(csk))?;
fs::write(dir.join("csk_ver"), ver.to_string())?;
Ok(())
}
pub fn load_csk(profile: &str) -> Result<Option<([u8; 32], u32)>, Report> {
let dir = secret_dir(Some(profile));
let key_path = dir.join("csk");
let ver_path = dir.join("csk_ver");
if key_path.exists() && ver_path.exists() {
let data = fs::read_to_string(key_path)?;
let bytes = hex::decode(data)?;
let arr: [u8; 32] = bytes.as_slice().try_into().map_err(|_| eyre!("bad csk"))?;
let ver: u32 = fs::read_to_string(ver_path)?.trim().parse()?;
Ok(Some((arr, ver)))
} else {
Ok(None)
}
}
pub fn secret_dir(profile: Option<&str>) -> PathBuf {
let mut dir = default_secret_dir();
if let Some(p) = profile {
dir.push(p);
}
dir
}
pub fn save_profile_host(profile: &str, host: &str) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("host"), host)?;
Ok(())
}
pub fn load_profile_host(profile: &str) -> Result<Option<String>, Report> {
let path = secret_dir(Some(profile)).join("host");
match fs::read_to_string(path) {
Ok(s) => Ok(Some(s.trim().to_string())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct JoinHostEntry {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coord_id: Option<String>,
pub host: String,
pub tcp_port: Option<u16>,
pub quic_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cert: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(default)]
pub last_ok: Option<u64>,
#[serde(default)]
pub last_fail: Option<u64>,
}
pub fn save_join_hosts(profile: &str, hosts: &[JoinHostEntry]) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("join_hosts.yaml"), serde_yaml::to_string(hosts)?)?;
Ok(())
}
pub fn load_join_hosts(profile: &str) -> Result<Vec<JoinHostEntry>, Report> {
let path = secret_dir(Some(profile)).join("join_hosts.yaml");
match fs::read_to_string(path) {
Ok(s) => Ok(serde_yaml::from_str(&s)?),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e.into()),
}
}
pub fn add_join_host(profile: &str, host: JoinHostEntry) -> Result<(), Report> {
let mut hosts = load_join_hosts(profile)?;
if let Some(existing) = hosts.iter_mut().find(|h| h.host == host.host) {
if host.coord_id.is_some() {
existing.coord_id = host.coord_id;
}
if host.tcp_port.is_some() {
existing.tcp_port = host.tcp_port;
}
if host.quic_port.is_some() {
existing.quic_port = host.quic_port;
}
if host.token.is_some() {
existing.token = host.token;
}
if host.cert.is_some() {
existing.cert = host.cert;
}
if host.fingerprint.is_some() {
existing.fingerprint = host.fingerprint;
}
if host.last_ok.is_some() {
existing.last_ok = host.last_ok;
}
if host.last_fail.is_some() {
existing.last_fail = host.last_fail;
}
} else {
hosts.push(host);
}
save_join_hosts(profile, &hosts)
}
pub fn remove_join_host(profile: &str, host: &str) -> Result<(), Report> {
let mut hosts = load_join_hosts(profile)?;
hosts.retain(|h| h.host != host);
save_join_hosts(profile, &hosts)
}
pub fn remove_join_host_index(profile: &str, idx: usize) -> Result<(), Report> {
let mut hosts = load_join_hosts(profile)?;
if idx < hosts.len() {
hosts.remove(idx);
save_join_hosts(profile, &hosts)?;
}
Ok(())
}
pub fn add_join_host_from_token(profile: &str, token: &str) -> Result<(), Report> {
let bs = volli_core::BootstrapSecret::decode(token)?;
let fp = hex::encode(Sha256::digest(&bs.cert));
let entry = JoinHostEntry {
coord_id: None,
host: bs.host,
tcp_port: Some(bs.tcp_port),
quic_port: Some(bs.quic_port),
token: Some(volli_core::token::encode_token(&bs.token)?),
cert: Some(STANDARD_NO_PAD.encode(bs.cert)),
fingerprint: Some(fp),
last_ok: None,
last_fail: None,
};
add_join_host(profile, entry)
}
pub fn save_bind_host(profile: &str, host: &str) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("bind_host"), host)?;
Ok(())
}
pub fn load_bind_host(profile: &str) -> Result<Option<String>, Report> {
let path = secret_dir(Some(profile)).join("bind_host");
match fs::read_to_string(path) {
Ok(s) => Ok(Some(s.trim().to_string())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn save_tcp_port(profile: &str, port: u16) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("tcp_port"), port.to_string())?;
Ok(())
}
pub fn load_tcp_port(profile: &str) -> Result<Option<u16>, Report> {
let path = secret_dir(Some(profile)).join("tcp_port");
match fs::read_to_string(path) {
Ok(s) => Ok(Some(s.trim().parse()?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn save_quic_port(profile: &str, port: u16) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("quic_port"), port.to_string())?;
Ok(())
}
pub fn load_quic_port(profile: &str) -> Result<Option<u16>, Report> {
let path = secret_dir(Some(profile)).join("quic_port");
match fs::read_to_string(path) {
Ok(s) => Ok(Some(s.trim().parse()?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn save_agent_whitelist(profile: &str, addrs: &[String]) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(
dir.join("agent_whitelist.yaml"),
serde_yaml::to_string(addrs)?,
)?;
Ok(())
}
pub fn load_agent_whitelist(profile: &str) -> Result<Option<Vec<String>>, Report> {
let path = secret_dir(Some(profile)).join("agent_whitelist.yaml");
match fs::read_to_string(path) {
Ok(s) => Ok(Some(serde_yaml::from_str(&s)?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn save_coord_whitelist(profile: &str, addrs: &[String]) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(
dir.join("coord_whitelist.yaml"),
serde_yaml::to_string(addrs)?,
)?;
Ok(())
}
pub fn load_coord_whitelist(profile: &str) -> Result<Option<Vec<String>>, Report> {
let path = secret_dir(Some(profile)).join("coord_whitelist.yaml");
match fs::read_to_string(path) {
Ok(s) => Ok(Some(serde_yaml::from_str(&s)?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn list_profiles() -> Result<Vec<String>, Report> {
let base = default_secret_dir();
let mut profiles = Vec::new();
if base.exists() {
for entry in fs::read_dir(base)? {
let entry = entry?;
if entry.path().is_dir() {
if let Some(name) = entry.file_name().to_str() {
profiles.push(name.to_string());
}
}
}
}
profiles.sort();
Ok(profiles)
}
pub fn delete_profile(profile: &str) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
if dir.exists() {
fs::remove_dir_all(dir)?;
}
Ok(())
}
pub fn profile_exists(profile: &str) -> bool {
secret_dir(Some(profile)).exists()
}
pub fn rename_profile(old: &str, new: &str) -> Result<(), Report> {
let src = secret_dir(Some(old));
let dst = secret_dir(Some(new));
if !src.exists() {
return Err(eyre!("profile not found"));
}
if dst.exists() {
return Err(eyre!("profile exists"));
}
fs::create_dir_all(dst.parent().unwrap())?;
fs::rename(src, dst)?;
Ok(())
}
pub fn save_bootstrap(profile: &str) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("bootstrap"), b"1")?;
Ok(())
}
pub fn load_bootstrap(profile: &str) -> bool {
secret_dir(Some(profile)).join("bootstrap").exists()
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct CoordProfileExport {
pub name: String,
pub host: Option<String>,
pub bind_host: Option<String>,
pub join_hosts: Vec<JoinHostEntry>,
pub agent_whitelist: Option<Vec<String>>,
pub coord_whitelist: Option<Vec<String>>,
pub coord_sk: Option<String>,
pub coord_pk: Option<String>,
pub tls_cert: Option<String>,
pub tls_key: Option<String>,
}
pub fn export_profile(profile: &str) -> Result<String, Report> {
let dir = secret_dir(Some(profile));
if !dir.exists() {
return Err(eyre!("profile not found"));
}
let host = load_profile_host(profile).ok().flatten();
let bind_host = load_bind_host(profile).ok().flatten();
let join_hosts = load_join_hosts(profile).unwrap_or_default();
let agent_whitelist = load_agent_whitelist(profile).ok().flatten();
let coord_whitelist = load_coord_whitelist(profile).ok().flatten();
let coord_sk = std::fs::read_to_string(dir.join("coord_sk")).ok();
let coord_pk = std::fs::read_to_string(dir.join("coord_pk")).ok();
let tls_cert = std::fs::read(dir.join("tls_cert.der"))
.ok()
.map(|b| STANDARD_NO_PAD.encode(b));
let tls_key = std::fs::read(dir.join("tls_key.der"))
.ok()
.map(|b| STANDARD_NO_PAD.encode(b));
let exp = CoordProfileExport {
name: profile.to_string(),
host,
bind_host,
join_hosts,
agent_whitelist,
coord_whitelist,
coord_sk,
coord_pk,
tls_cert,
tls_key,
};
Ok(serde_yaml::to_string(&exp)?)
}
pub fn import_profile(yaml: &str, name: Option<&str>, force: bool) -> Result<String, Report> {
let mut exp: CoordProfileExport = serde_yaml::from_str(yaml)?;
if let Some(n) = name {
exp.name = n.to_string();
}
let dir = secret_dir(Some(&exp.name));
if dir.exists() && !force {
return Err(eyre!("profile exists"));
}
std::fs::create_dir_all(&dir)?;
if let Some(ref v) = exp.host {
save_profile_host(&exp.name, v)?;
}
if let Some(ref v) = exp.bind_host {
save_bind_host(&exp.name, v)?;
}
if !exp.join_hosts.is_empty() {
save_join_hosts(&exp.name, &exp.join_hosts)?;
}
if let Some(ref v) = exp.agent_whitelist {
save_agent_whitelist(&exp.name, v)?;
}
if let Some(ref v) = exp.coord_whitelist {
save_coord_whitelist(&exp.name, v)?;
}
if let Some(ref v) = exp.coord_sk {
std::fs::write(dir.join("coord_sk"), v)?;
}
if let Some(ref v) = exp.coord_pk {
std::fs::write(dir.join("coord_pk"), v)?;
}
if let Some(ref v) = exp.tls_cert {
std::fs::write(
dir.join("tls_cert.der"),
STANDARD_NO_PAD.decode(v.as_bytes())?,
)?;
}
if let Some(ref v) = exp.tls_key {
std::fs::write(
dir.join("tls_key.der"),
STANDARD_NO_PAD.decode(v.as_bytes())?,
)?;
}
Ok(exp.name)
}