pwr-server 0.4.0

pwr daemon: runs on the NAS, handles project storage and retrieval over TLS
Documentation
//! TLS certificate generation for pwr-server.
//!
//! Generates self-signed ECDSA P-256 certificates for TLS 1.3.
//! The certificate fingerprint is printed so the user can pin it
//! in the client config for MITM protection.

use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair};
use std::fs;
use std::path::Path;

/// Generate a self-signed TLS certificate and private key.
///
/// Returns (cert_pem, key_pem, fingerprint_sha256).
/// The certificate is valid for 365 days from issuance.
pub fn generate_certificate(
    common_name: &str,
) -> Result<(String, String, String), String> {
    let mut params = CertificateParams::new(vec![common_name.to_string()])
        .map_err(|e| format!("cert params: {}", e))?;

    // Set distinguished name
    let mut dn = DistinguishedName::new();
    dn.push(DnType::CommonName, common_name);
    params.distinguished_name = dn;

    // Generate ECDSA P-256 key pair
    let key_pair = KeyPair::generate()
        .map_err(|e| format!("key gen: {}", e))?;

    let cert = params
        .self_signed(&key_pair)
        .map_err(|e| format!("self-sign: {}", e))?;

    let cert_pem = cert.pem();
    let key_pem = key_pair.serialize_pem();

    // Compute SHA-256 fingerprint for certificate pinning
    let fingerprint = pwr_core::crypto::sha256_hex(cert_pem.as_bytes());

    Ok((cert_pem, key_pem, fingerprint))
}

/// Write certificate and key to files with appropriate permissions.
pub fn save_certificate(
    cert_path: &Path,
    key_path: &Path,
    cert_pem: &str,
    key_pem: &str,
) -> Result<(), String> {
    // Create parent directories
    if let Some(parent) = cert_path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("mkdir {}: {}", parent.display(), e))?;
    }
    if let Some(parent) = key_path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("mkdir {}: {}", parent.display(), e))?;
    }

    // Write certificate (world-readable)
    fs::write(cert_path, cert_pem)
        .map_err(|e| format!("write cert: {}", e))?;

    // Write private key (owner-only)
    fs::write(key_path, key_pem)
        .map_err(|e| format!("write key: {}", e))?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(key_path)
            .map_err(|e| format!("stat key: {}", e))?
            .permissions();
        perms.set_mode(0o600);
        fs::set_permissions(key_path, perms)
            .map_err(|e| format!("chmod key: {}", e))?;
    }

    Ok(())
}

/// Generate a fresh server configuration with TLS certificates and PSK.
///
/// `config_dir` is the base directory for pwr-server files (e.g.
/// `/etc/pwr` for system installs or `~/.config/pwr` for user installs).
/// Cert and key files are written under this directory; the config file
/// is written at `config_dir/server.toml`.
///
/// `storage_dir` is where project archives will be stored (e.g.
/// `/srv/pwr/projects` or `~/.local/share/pwr/projects`).
pub fn init_server(
    config_dir: &Path,
    storage_dir: &Path,
    hostname: &str,
) -> Result<(), String> {
    use crate::config::{save_config, ServerConfig};

    let (cert_pem, key_pem, fingerprint) = generate_certificate(hostname)?;

    let cert_path = config_dir.join("server.crt");
    let key_path = config_dir.join("server.key");
    let config_path = config_dir.join("server.toml");

    save_certificate(&cert_path, &key_path, &cert_pem, &key_pem)?;

    // Generate PSK
    let psk = pwr_core::crypto::generate_psk();
    let psk_hex = pwr_core::crypto::psk_to_hex(&psk);

    let mut config = ServerConfig::default();
    config.auth_token = psk_hex.clone();
    config.tls_cert_path = cert_path.clone();
    config.tls_key_path = key_path.clone();
    config.storage_base_path = storage_dir.to_path_buf();

    save_config(&config, &config_path)?;

    println!("Server initialized successfully.");
    println!("  Config:      {}", config_path.display());
    println!("  Certificate: {}", cert_path.display());
    println!("  Private key: {}", key_path.display());
    println!("  Storage:     {}", storage_dir.display());
    println!("  PSK:         {}", psk_hex);
    println!("  Fingerprint: {}", fingerprint);
    println!();
    println!("Copy the PSK to your client config:");
    println!("  pwr init --server-host {} --psk {}", hostname, psk_hex);
    println!();
    println!("To start the server:");
    println!("  pwr-server --config {} start", config_path.display());
    println!();
    println!("To install as a systemd user service (no root needed):");
    println!("  mkdir -p ~/.config/systemd/user");
    println!("  cp pwr-server.user.service ~/.config/systemd/user/pwr-server.service");
    println!("  systemctl --user daemon-reload");
    println!("  systemctl --user enable --now pwr-server");
    println!();
    println!("To have the service survive logout:");
    println!("  sudo loginctl enable-linger $USER");

    Ok(())
}