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>,
}
pub fn get_ssh_dir() -> PathBuf {
if let Some(home) = dirs::home_dir() {
home.join(".ssh")
} else {
PathBuf::from(".ssh")
}
}
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,
})
}
#[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();
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);
}
}
}
}
}
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") {
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,
})
}
#[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));
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();
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));
}
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(),
})
}
#[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)?;
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())?;
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 "));
}
}