1use super::error::{AgentError, map_remote_error};
2use crate::auth::ipc::{self, AgentRequestPayload, AgentResponse, UnlockPolicy, VaultStatus};
3use crate::auth::secret::{SensitiveString, sensitive_string};
4use crate::auth::vault::VaultPaths;
5use crate::command_path;
6use crate::log_debug;
7use std::process::{Command, Stdio};
8use std::thread;
9use std::time::{Duration, Instant};
10
11const AGENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(2);
12const AGENT_STARTUP_POLL_INTERVAL: Duration = Duration::from_millis(10);
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct AgentEntryStatus {
17 pub status: VaultStatus,
19 pub exists: bool,
21}
22
23#[derive(Debug, Clone)]
25pub struct AgentClient {
26 paths: VaultPaths,
27}
28
29impl AgentClient {
30 pub fn new() -> Result<Self, AgentError> {
32 let paths = VaultPaths::resolve_default()?;
33 log_debug!("Initialized password vault client for '{}'", paths.base_dir().display());
34 Ok(Self { paths })
35 }
36
37 pub fn status(&self) -> Result<VaultStatus, AgentError> {
39 log_debug!("Requesting password vault status");
40 match self.request(AgentRequestPayload::Status, false) {
41 Ok(AgentResponse::Status { status }) => Ok(status),
42 Ok(response) => Err(AgentError::Protocol(format!("unexpected status response: {response:?}"))),
43 Err(AgentError::Io(_)) => Ok(VaultStatus::locked(self.paths.metadata_path().is_file())),
44 Err(err) => Err(err),
45 }
46 }
47
48 pub fn unlock(&self, master_password: &str, policy: UnlockPolicy) -> Result<VaultStatus, AgentError> {
50 log_debug!(
51 "Requesting password vault unlock with idle={}s absolute={}s",
52 policy.idle_timeout_seconds,
53 policy.session_timeout_seconds
54 );
55 match self.request(
56 AgentRequestPayload::Unlock {
57 master_password: sensitive_string(master_password),
58 policy,
59 },
60 true,
61 )? {
62 AgentResponse::Success { status, .. } | AgentResponse::Status { status } => Ok(status),
63 AgentResponse::Error { code, message, .. } => Err(map_remote_error(&code, message)),
64 response => Err(AgentError::Protocol(format!("unexpected unlock response: {response:?}"))),
65 }
66 }
67
68 pub fn entry_status(&self, name: &str) -> Result<AgentEntryStatus, AgentError> {
70 log_debug!("Requesting password vault entry status '{}'", name);
71 match self.request(AgentRequestPayload::EntryStatus { name: name.to_string() }, true)? {
72 AgentResponse::EntryStatus { status, exists, .. } => Ok(AgentEntryStatus { status, exists }),
73 AgentResponse::Error { code, message, .. } => Err(map_remote_error(&code, message)),
74 response => Err(AgentError::Protocol(format!("unexpected entry-status response: {response:?}"))),
75 }
76 }
77
78 pub fn authorize_askpass(&self, name: &str) -> Result<SensitiveString, AgentError> {
80 log_debug!("Requesting internal askpass authorization for '{}'", name);
81 match self.request(AgentRequestPayload::AuthorizeAskpass { name: name.to_string() }, true)? {
82 AgentResponse::AskpassAuthorized { token, .. } => Ok(token),
83 AgentResponse::Error { code, message, .. } => Err(map_remote_error(&code, message)),
84 response => Err(AgentError::Protocol(format!("unexpected askpass authorization response: {response:?}"))),
85 }
86 }
87
88 pub fn get_secret(&self, token: &str) -> Result<SensitiveString, AgentError> {
90 log_debug!("Requesting password vault secret using askpass token");
91 match self.request(
92 AgentRequestPayload::GetSecret {
93 token: sensitive_string(token),
94 },
95 true,
96 )? {
97 AgentResponse::Secret { secret, .. } => Ok(secret),
98 AgentResponse::Error { code, message, .. } => Err(map_remote_error(&code, message)),
99 response => Err(AgentError::Protocol(format!("unexpected get-secret response: {response:?}"))),
100 }
101 }
102
103 pub fn lock(&self) -> Result<VaultStatus, AgentError> {
105 log_debug!("Requesting password vault lock");
106 match self.request(AgentRequestPayload::Lock, false)? {
107 AgentResponse::Success { status, .. } | AgentResponse::Status { status } => Ok(status),
108 AgentResponse::Error { code, message, .. } => Err(map_remote_error(&code, message)),
109 response => Err(AgentError::Protocol(format!("unexpected lock response: {response:?}"))),
110 }
111 }
112
113 fn request(&self, payload: AgentRequestPayload, auto_start: bool) -> Result<AgentResponse, AgentError> {
114 let payload_name = payload.debug_name();
115 log_debug!("Sending password vault agent request '{}' (auto_start={})", payload_name, auto_start);
116 match ipc::send_request(&self.paths, &payload) {
117 Ok(response) => Ok(response),
118 Err(first_err) if auto_start => {
119 log_debug!(
120 "Password vault agent request '{}' failed initially ({}); attempting auto-start",
121 payload_name,
122 first_err
123 );
124 self.spawn_server()?;
125 ipc::send_request(&self.paths, &payload)
126 .map_err(|second_err| AgentError::Protocol(format!("failed to contact password vault agent after restart: {first_err}; {second_err}")))
127 }
128 Err(err) => Err(AgentError::Io(err)),
129 }
130 }
131
132 fn spawn_server(&self) -> Result<(), AgentError> {
133 log_debug!("Starting password vault agent process");
134 let cossh_path = command_path::cossh_path()?;
135 let mut command = Command::new(cossh_path);
136 command
137 .arg("agent")
138 .arg("--serve")
139 .stdin(Stdio::null())
140 .stdout(Stdio::null())
141 .stderr(Stdio::null())
142 .env_remove(crate::auth::transport::INTERNAL_ASKPASS_MODE_ENV)
143 .env_remove(crate::auth::transport::INTERNAL_ASKPASS_TOKEN_ENV)
144 .env_remove("SSH_ASKPASS")
145 .env_remove("SSH_ASKPASS_REQUIRE");
146 command.spawn()?;
147
148 let started_at = Instant::now();
150 while started_at.elapsed() < AGENT_STARTUP_TIMEOUT {
151 if ipc::send_request(&self.paths, &AgentRequestPayload::Status).is_ok() {
152 log_debug!("Password vault agent became ready in {:?}", started_at.elapsed());
153 return Ok(());
154 }
155 thread::sleep(AGENT_STARTUP_POLL_INTERVAL);
156 }
157
158 Err(AgentError::Protocol("password vault agent did not become ready in time".to_string()))
159 }
160}