use anyhow::{Context, Result};
use ssh_key::rand_core::OsRng;
use ssh_key::{Algorithm, LineEnding, PrivateKey};
use std::ffi::OsString;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct SshKeypair {
pub private: String,
pub public: String,
}
impl SshKeypair {
pub fn new() -> Result<SshKeypair> {
let kp = PrivateKey::random(&mut OsRng, Algorithm::Ed25519)?;
let privkey = kp.to_openssh(LineEnding::LF)?.to_string();
let pubkey = kp.public_key().to_openssh()?;
Ok(SshKeypair {
private: privkey,
public: pubkey,
})
}
pub fn write_to(&self, private_key_path: &Path) -> Result<()> {
let mut privkey = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(private_key_path)
.with_context(|| {
format!(
"opening privkey path {} for writing",
private_key_path.display()
)
})?;
privkey
.write_all(self.private.as_bytes())
.context("Failed to write SSH privkey")?;
let pubkey_path = pub_path(private_key_path);
let mut pubkey = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o644)
.open(&pubkey_path)
.with_context(|| {
format!("opening pubkey path {} for writing", pubkey_path.display())
})?;
pubkey
.write_all(self.public.as_bytes())
.context("failed to write SSH pubkey")?;
Ok(())
}
}
fn pub_path(private_key_path: &Path) -> PathBuf {
let mut s: OsString = private_key_path.as_os_str().into();
s.push(".pub");
PathBuf::from(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whitespace_is_stripped() -> anyhow::Result<()> {
let kp = SshKeypair::new()?;
assert!(kp.private != kp.public);
assert!(!kp.public.ends_with('\n'));
assert!(!kp.public.ends_with(' '));
assert!(kp.private.ends_with('\n'));
Ok(())
}
#[test]
fn pub_path_appends() {
assert_eq!(
pub_path(Path::new("/tmp/client_id_ed25519")),
Path::new("/tmp/client_id_ed25519.pub")
);
}
}