sb-vault 0.1.0

S&B Vault // Zero-Trust Desktop Suite & Secret Engine
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use ssh_key::{Algorithm, LineEnding, PrivateKey, HashAlg};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SshKeySummary {
    pub name: String,
    pub path: String,
    pub public_key: String,
    pub fingerprint: String,
    pub algorithm: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SshKeyStatus {
    pub has_default_key: bool,
    pub default_key: Option<SshKeySummary>,
    pub available_keys: Vec<SshKeySummary>,
}

/// Ermittelt den ~/.ssh Ordner
pub fn get_ssh_dir() -> PathBuf {
    if let Some(home) = dirs::home_dir() {
        home.join(".ssh")
    } else {
        PathBuf::from(".ssh")
    }
}

/// Liest einen einzelnen Public-Key ein und ermittelt Fingerprint & Algorithmus
pub fn read_key_summary<P: AsRef<Path>>(pub_path: P) -> Option<SshKeySummary> {
    let pub_path = pub_path.as_ref();
    if !pub_path.exists() {
        return None;
    }

    let content = std::fs::read_to_string(pub_path).ok()?;
    let parsed = ssh_key::PublicKey::from_openssh(&content).ok()?;
    let fingerprint = parsed.fingerprint(HashAlg::Sha256).to_string();
    let algorithm = parsed.algorithm().to_string();

    let stem = pub_path.file_stem()?.to_string_lossy().to_string();
    let priv_path = pub_path.with_extension("");

    Some(SshKeySummary {
        name: stem,
        path: priv_path.to_string_lossy().to_string(),
        public_key: content.trim().to_string(),
        fingerprint,
        algorithm,
    })
}

/// Listet alle im System verfügbaren SSH-Schlüssel auf (~/.ssh und ~/.sb-ssh)
#[tauri::command]
pub fn get_ssh_key_status() -> Result<SshKeyStatus, String> {
    let ssh_dir = get_ssh_dir();
    let default_pub = ssh_dir.join("id_ed25519.pub");
    let default_key = read_key_summary(&default_pub);
    let has_default_key = default_key.is_some();

    let mut available_keys = Vec::new();

    // Suche in ~/.ssh/ nach *.pub
    if ssh_dir.exists() {
        if let Ok(entries) = std::fs::read_dir(&ssh_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.extension().and_then(|e| e.to_str()) == Some("pub") {
                    if let Some(summary) = read_key_summary(&path) {
                        available_keys.push(summary);
                    }
                }
            }
        }
    }

    // Suche in ~/.sb-ssh/ nach *.pub (falls vorhanden)
    if let Some(home) = dirs::home_dir() {
        let sb_ssh_dir = home.join(".sb-ssh");
        if sb_ssh_dir.exists() {
            if let Ok(entries) = std::fs::read_dir(&sb_ssh_dir) {
                for entry in entries.flatten() {
                    let path = entry.path();
                    if path.extension().and_then(|e| e.to_str()) == Some("pub") {
                        // ca_key.pub und id_ephemeral-cert.pub ignorieren
                        let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
                        if !filename.contains("ca_key") && !filename.contains("cert") {
                            if let Some(summary) = read_key_summary(&path) {
                                if !available_keys.iter().any(|k| k.path == summary.path) {
                                    available_keys.push(summary);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(SshKeyStatus {
        has_default_key,
        default_key,
        available_keys,
    })
}

/// Erzeugt ein modernes Ed25519-Schlüsselpaar mit 1 Klick (Standard: ~/.ssh/id_ed25519)
#[tauri::command]
pub fn generate_ed25519_keypair(
    key_name: Option<String>,
    comment: Option<String>,
    state: tauri::State<'_, crate::vault::VaultState>,
) -> Result<SshKeySummary, String> {
    crate::vault::ensure_unlocked(&state)?;

    let ssh_dir = get_ssh_dir();
    std::fs::create_dir_all(&ssh_dir)
        .map_err(|e| format!("~/.ssh Verzeichnis konnte nicht erstellt werden: {}", e))?;

    let name = key_name.unwrap_or_else(|| "id_ed25519".to_string());
    let priv_path = ssh_dir.join(&name);
    let pub_path = ssh_dir.join(format!("{}.pub", name));

    // Kommentar bilden
    let comment_str = comment.unwrap_or_else(|| {
        let username = std::env::var("USERNAME")
            .or_else(|_| std::env::var("USER"))
            .unwrap_or_else(|_| "operator".to_string());
        format!("{}@skulls-and-bones", username)
    });

    let mut rng = rand::rngs::OsRng;
    let mut private_key = PrivateKey::random(&mut rng, Algorithm::Ed25519)
        .map_err(|e| format!("Kryptographie-Fehler beim Generieren des Ed25519-Keys: {}", e))?;

    private_key.set_comment(&comment_str);

    let priv_openssh = private_key.to_openssh(LineEnding::LF)
        .map_err(|e| format!("Serialisierung des privaten Keys fehlgeschlagen: {}", e))?;

    let pub_key = private_key.public_key();
    let pub_openssh = format!("{} {}\n", pub_key.to_openssh()
        .map_err(|e| format!("Serialisierung des öffentlichen Keys fehlgeschlagen: {}", e))?
        .trim(), comment_str);

    let fingerprint = pub_key.fingerprint(HashAlg::Sha256).to_string();

    // Privaten Schlüssel mit 0600 Rechten speichern
    std::fs::write(&priv_path, priv_openssh.as_bytes())
        .map_err(|e| format!("Konnte privaten SSH-Schlüssel nicht speichern: {}", e))?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = std::fs::set_permissions(&priv_path, std::fs::Permissions::from_mode(0o600));
    }

    // Öffentlichen Schlüssel speichern
    std::fs::write(&pub_path, pub_openssh.as_bytes())
        .map_err(|e| format!("Konnte öffentlichen SSH-Schlüssel nicht speichern: {}", e))?;

    Ok(SshKeySummary {
        name,
        path: priv_path.to_string_lossy().to_string(),
        public_key: pub_openssh.trim().to_string(),
        fingerprint,
        algorithm: "ssh-ed25519".to_string(),
    })
}

/// Überträgt den Public-Key via sb-ssh auf den Zielserver in ~/.ssh/authorized_keys
#[tauri::command]
pub async fn deploy_ssh_key_to_server(
    server_name: String,
    public_key: Option<String>,
    state: tauri::State<'_, crate::vault::VaultState>,
) -> Result<String, String> {
    crate::vault::ensure_unlocked(&state)?;

    // Public Key ermitteln
    let pub_key_to_install = if let Some(pk) = public_key {
        pk
    } else {
        let status = get_ssh_key_status()?;
        if let Some(def) = status.default_key {
            def.public_key
        } else {
            return Err("Kein öffentlicher Schlüssel zum Hinterlegen gefunden. Bitte zuerst Schlüsselpaar generieren.".to_string());
        }
    };

    let sb_ssh = which::which("sb-ssh")
        .or_else(|_| {
            dirs::home_dir().map(|h| h.join(".cargo").join("bin").join(if cfg!(windows) { "sb-ssh.exe" } else { "sb-ssh" }))
                .filter(|p| p.exists())
                .ok_or("Nicht gefunden")
        })
        .map_err(|_| "sb-ssh Binary wurde nicht gefunden".to_string())?;

    // Sicheres Anfügen an ~/.ssh/authorized_keys via remote exec
    let safe_key = pub_key_to_install.trim();
    let remote_cmd = format!(
        "mkdir -p ~/.ssh && chmod 700 ~/.ssh && grep -qxF '{safe}' ~/.ssh/authorized_keys 2>/dev/null || echo '{safe}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys",
        safe = safe_key
    );

    let output = tokio::process::Command::new(&sb_ssh)
        .args([&server_name, &remote_cmd])
        .output()
        .await
        .map_err(|e| format!("Fehler bei der Key-Übertragung via sb-ssh: {}", e))?;

    if !output.status.success() {
        let err = String::from_utf8_lossy(&output.stderr);
        return Err(format!("Schlüssel konnte nicht hinterlegt werden: {}", err));
    }

    Ok(format!("Öffentlicher Ed25519-Schlüssel wurde erfolgreich auf '{}' in ~/.ssh/authorized_keys hinterlegt.", server_name))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_generate_ed25519_key() {
        let mut rng = rand::rngs::OsRng;
        let mut priv_key = PrivateKey::random(&mut rng, Algorithm::Ed25519).expect("Generate failed");
        priv_key.set_comment("test@skulls");

        let pub_key = priv_key.public_key();
        let fp = pub_key.fingerprint(HashAlg::Sha256).to_string();
        assert!(fp.starts_with("SHA256:"));

        let openssh = pub_key.to_openssh().expect("Serialize pub failed");
        assert!(openssh.starts_with("ssh-ed25519 "));
    }
}