use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
const BUILD_VERSION: &str = match option_env!("SIRR_BUILD_VERSION") {
Some(v) => v,
None => env!("CARGO_PKG_VERSION"),
};
#[derive(Parser)]
#[command(
name = "sirrd",
about = "Sirrd — ephemeral secret vault server daemon",
version = BUILD_VERSION
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Serve {
#[arg(long, env = "SIRR_PORT", default_value = "39999")]
port: u16,
#[arg(long, env = "SIRR_HOST", default_value = "0.0.0.0")]
host: String,
#[arg(long, env = "SIRR_LOG_LEVEL")]
log_level: Option<String>,
#[arg(long)]
init: bool,
},
Rotate,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let effective_log_level = if let Commands::Serve { ref log_level, .. } = cli.command {
let raw = log_level
.clone()
.or_else(|| std::env::var("SIRR_LOG_LEVEL").ok())
.unwrap_or_else(|| "warn".into());
if raw.eq_ignore_ascii_case("verbose") {
"debug".to_owned()
} else {
raw
}
} else {
std::env::var("SIRR_LOG_LEVEL").unwrap_or_else(|_| "warn".into())
};
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new(&effective_log_level))
.init();
match cli.command {
Commands::Serve {
port,
host,
log_level: _,
init,
} => cmd_serve(host, port, effective_log_level, init).await,
Commands::Rotate => cmd_rotate().await,
}
}
async fn cmd_serve(host: String, port: u16, log_level: String, init: bool) -> Result<()> {
let no_banner = std::env::var("SIRR_NO_BANNER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_security_banner = std::env::var("SIRR_NO_SECURITY_BANNER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let env_api_key = std::env::var("SIRR_MASTER_API_KEY").ok();
let (api_key, auto_generated_key) = match env_api_key {
Some(k) => (Some(k), None),
None => {
let key = {
let mut bytes = [0u8; 16];
rand::Rng::fill(&mut rand::thread_rng(), &mut bytes);
format!("sirr_key_{}", hex::encode(bytes))
};
(Some(key.clone()), Some(key))
}
};
let auto_init = init
|| std::env::var("SIRR_AUTOINIT")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
let cfg = sirr_server::ServerConfig {
host,
port,
api_key,
auto_generated_key,
license_key: std::env::var("SIRR_LICENSE_KEY").ok(),
data_dir: std::env::var("SIRR_DATA_DIR").ok().map(Into::into),
log_level,
no_banner,
no_security_banner,
auto_init,
version: BUILD_VERSION.to_string(),
..Default::default()
};
sirr_server::run(cfg).await
}
async fn cmd_rotate() -> Result<()> {
let data_dir_env = std::env::var("SIRR_DATA_DIR").ok().map(Into::into);
let data_dir = sirr_server::resolve_data_dir(data_dir_env.as_ref())?;
let key_path = data_dir.join("sirr.key");
let old_bytes =
std::fs::read(&key_path).context("read sirr.key — is the server initialized?")?;
let old_key = sirr_server::store::crypto::load_key(&old_bytes)
.ok_or_else(|| anyhow::anyhow!("sirr.key is corrupt (expected 32 bytes)"))?;
let db_path = data_dir.join("sirr.db");
let store = sirr_server::store::Store::open(&db_path, old_key).context("open store")?;
let current_version = store.max_key_version()?;
let new_version = current_version
.checked_add(1)
.context("key version overflow (max 255 rotations)")?;
let new_key = sirr_server::store::crypto::generate_key();
let count = store.rotate(&new_key, new_version)?;
std::fs::write(&key_path, new_key.as_bytes()).context("write new sirr.key")?;
println!("rotated {count} secret(s) to key version {new_version}");
println!("new encryption key written to {}", key_path.display());
Ok(())
}