hussh 0.2.2

Memory-safe SSH daemon for restricted TCP forwarding
use crate::errors::*;
use russh::keys::{Algorithm, HashAlg, PrivateKey, ssh_key::LineEnding};
use std::io::ErrorKind;
use std::path::Path;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use zeroize::Zeroizing;

pub fn keygen() -> Result<PrivateKey> {
    let privkey = PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519)?;
    let pubkey = privkey.public_key();

    let fp = pubkey.fingerprint(HashAlg::Sha256);
    info!("Generated ssh key: {fp}");

    Ok(privkey)
}

pub fn keygen_str() -> Result<Zeroizing<String>> {
    let privkey = keygen()?;
    let privkey = privkey.to_openssh(LineEnding::LF)?;
    Ok(privkey)
}

fn privkey_str(privkey: &PrivateKey) -> Result<Zeroizing<String>> {
    let privkey = privkey.to_openssh(LineEnding::LF)?;
    Ok(privkey)
}

pub async fn init_from_path(path: &Path) -> Result<PrivateKey> {
    debug!("Loading ssh private key from: {path:?}");
    match fs::read_to_string(path).await {
        Ok(buf) => {
            let key = PrivateKey::from_openssh(buf)?;
            let fp = key.public_key().fingerprint(HashAlg::Sha256);
            info!("Loaded ssh key: {fp}");
            Ok(key)
        }
        Err(err) if err.kind() == ErrorKind::NotFound => {
            debug!("No existing ssh key, generating one");
            let key = keygen()?;
            let mut opt = fs::OpenOptions::new();
            opt.create_new(true).write(true);

            #[cfg(unix)]
            {
                opt.mode(0o600);
            }

            let mut file = opt
                .open(&path)
                .await
                .with_context(|| format!("Failed to create ssh private key file: {path:?}"))?;
            file.write_all(privkey_str(&key)?.as_bytes())
                .await
                .context("Failed to write ssh private key file")?;
            Ok(key)
        }
        Err(err) => Err(err).with_context(|| format!("Failed to load ssh private key: {path:?}")),
    }
}

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

    #[test]
    fn test_keygen() {
        keygen().unwrap();
    }
}