use std::env;
use std::path::PathBuf;
use std::time::Duration;
use ssh_agent_lib::blocking::Client;
use ssh_agent_lib::proto::{
AddIdentity, AddIdentityConstrained, Credential, KeyConstraint, RemoveIdentity, SignRequest,
};
use ssh_key::{Algorithm, HashAlg, PrivateKey, PublicKey, Signature};
use zeroize::Zeroizing;
use crate::AnvilError;
#[cfg(unix)]
type Transport = std::os::unix::net::UnixStream;
#[cfg(windows)]
type Transport = std::fs::File;
fn open_transport(path: &std::path::Path) -> std::io::Result<Transport> {
#[cfg(unix)]
{
std::os::unix::net::UnixStream::connect(path)
}
#[cfg(windows)]
{
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
}
}
#[derive(Debug, Clone)]
pub struct Identity {
pub public_key: PublicKey,
pub comment: String,
pub fingerprint: String,
}
#[derive(Debug)]
pub struct Agent {
inner: Client<Transport>,
}
impl Agent {
pub fn from_env() -> Result<Self, AnvilError> {
let sock = env::var("SSH_AUTH_SOCK").map_err(|_e| {
AnvilError::invalid_config("SSH_AUTH_SOCK is not set").with_hint(
"No SSH agent is advertised in this shell. Start one with \
`gitway agent start -s` and eval the output, or enable the \
bundled systemd user unit (`systemctl --user enable --now \
gitway-agent.service`). NixOS + Home Manager users can set \
`services.gitway-agent.enable = true;` — the unit runs \
automatically and `SSH_AUTH_SOCK` is exported to every \
child of `systemd --user`.",
)
})?;
if sock.is_empty() {
return Err(
AnvilError::invalid_config("SSH_AUTH_SOCK is empty").with_hint(
"Something cleared `SSH_AUTH_SOCK` to the empty string. \
Unset it (`unset SSH_AUTH_SOCK`) and re-export it to a \
real socket path, or just restart the shell.",
),
);
}
Self::connect(&PathBuf::from(sock))
}
pub fn connect(path: &std::path::Path) -> Result<Self, AnvilError> {
let stream = open_transport(path)?;
Ok(Self {
inner: Client::new(stream),
})
}
pub fn list(&mut self) -> Result<Vec<Identity>, AnvilError> {
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<(), AnvilError> {
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| AnvilError::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<(), AnvilError> {
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<(), AnvilError> {
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<(), AnvilError> {
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<(), AnvilError> {
self.inner
.unlock(passphrase.as_str().to_owned())
.map_err(|e| io_err(format!("agent unlock failed: {e}")))
}
pub fn sign(&mut self, public_key: &PublicKey, data: &[u8]) -> Result<Signature, AnvilError> {
let flags: u32 = match public_key.algorithm() {
Algorithm::Rsa { .. } => 4, _ => 0,
};
self.inner
.sign(SignRequest {
pubkey: public_key.key_data().clone(),
data: data.to_vec(),
flags,
})
.map_err(|e| io_err(format!("agent sign failed: {e}")))
}
}
fn io_err(message: String) -> AnvilError {
AnvilError::from(std::io::Error::other(message))
}