use std::env;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::time::Duration;
use ssh_agent_lib::blocking::Client;
use ssh_agent_lib::proto::{
AddIdentity, AddIdentityConstrained, Credential, KeyConstraint, RemoveIdentity,
};
use ssh_key::{HashAlg, PrivateKey, PublicKey};
use zeroize::Zeroizing;
use crate::GitwayError;
#[derive(Debug, Clone)]
pub struct Identity {
pub public_key: PublicKey,
pub comment: String,
pub fingerprint: String,
}
#[derive(Debug)]
pub struct Agent {
inner: Client<UnixStream>,
}
impl Agent {
pub fn from_env() -> Result<Self, GitwayError> {
let sock = env::var("SSH_AUTH_SOCK").map_err(|_e| {
GitwayError::invalid_config(
"SSH_AUTH_SOCK is not set — start an agent first \
(e.g. `eval $(ssh-agent -s)`) or pass --socket",
)
})?;
if sock.is_empty() {
return Err(GitwayError::invalid_config("SSH_AUTH_SOCK is empty"));
}
Self::connect(&PathBuf::from(sock))
}
pub fn connect(path: &std::path::Path) -> Result<Self, GitwayError> {
let stream = UnixStream::connect(path)?;
Ok(Self {
inner: Client::new(stream),
})
}
pub fn list(&mut self) -> Result<Vec<Identity>, GitwayError> {
let raw = self
.inner
.request_identities()
.map_err(|e| io_err(format!("agent list failed: {e}")))?;
let mut out = Vec::with_capacity(raw.len());
for id in raw {
let public_key = PublicKey::new(id.pubkey, id.comment.clone());
let fingerprint = public_key.fingerprint(HashAlg::Sha256).to_string();
out.push(Identity {
public_key,
comment: id.comment,
fingerprint,
});
}
Ok(out)
}
pub fn add(
&mut self,
key: &PrivateKey,
lifetime: Option<Duration>,
confirm: bool,
) -> Result<(), GitwayError> {
let identity = AddIdentity {
credential: Credential::Key {
privkey: key.key_data().clone(),
comment: key.comment().to_owned(),
},
};
if lifetime.is_none() && !confirm {
self.inner
.add_identity(identity)
.map_err(|e| io_err(format!("agent add failed: {e}")))?;
return Ok(());
}
let mut constraints: Vec<KeyConstraint> = Vec::with_capacity(2);
if let Some(d) = lifetime {
let secs = u32::try_from(d.as_secs())
.map_err(|_e| GitwayError::invalid_config("lifetime exceeds u32 seconds"))?;
constraints.push(KeyConstraint::Lifetime(secs));
}
if confirm {
constraints.push(KeyConstraint::Confirm);
}
self.inner
.add_identity_constrained(AddIdentityConstrained {
identity,
constraints,
})
.map_err(|e| io_err(format!("agent add (constrained) failed: {e}")))?;
Ok(())
}
pub fn remove(&mut self, public_key: &PublicKey) -> Result<(), GitwayError> {
self.inner
.remove_identity(RemoveIdentity {
pubkey: public_key.key_data().clone(),
})
.map_err(|e| io_err(format!("agent remove failed: {e}")))
}
pub fn remove_all(&mut self) -> Result<(), GitwayError> {
self.inner
.remove_all_identities()
.map_err(|e| io_err(format!("agent remove-all failed: {e}")))
}
pub fn lock(&mut self, passphrase: &Zeroizing<String>) -> Result<(), GitwayError> {
self.inner
.lock(passphrase.as_str().to_owned())
.map_err(|e| io_err(format!("agent lock failed: {e}")))
}
pub fn unlock(&mut self, passphrase: &Zeroizing<String>) -> Result<(), GitwayError> {
self.inner
.unlock(passphrase.as_str().to_owned())
.map_err(|e| io_err(format!("agent unlock failed: {e}")))
}
}
fn io_err(message: String) -> GitwayError {
GitwayError::from(std::io::Error::other(message))
}