1use crate::agent::AgentCore;
9use crate::agent::AgentHandle;
10#[cfg(unix)]
11use crate::agent::AgentSession;
12use crate::crypto::provider_bridge;
13use crate::crypto::signer::extract_seed_from_key_bytes;
14use crate::crypto::signer::{decrypt_keypair, encrypt_keypair};
15use crate::error::AgentError;
16use crate::signing::PassphraseProvider;
17use crate::storage::keychain::{KeyAlias, KeyStorage};
18use log::{debug, error, info, warn};
19#[cfg(target_os = "macos")]
20use pkcs8::PrivateKeyInfo;
21#[cfg(target_os = "macos")]
22use pkcs8::der::Decode;
23#[cfg(target_os = "macos")]
24use pkcs8::der::asn1::OctetString;
25use serde::Serialize;
26#[cfg(unix)]
27use ssh_agent_lib;
28#[cfg(unix)]
29use ssh_agent_lib::agent::listen;
30#[cfg(target_os = "macos")]
31use ssh_key::Fingerprint;
32use ssh_key::private::{Ed25519Keypair as SshEdKeypair, KeypairData};
33use ssh_key::{
34 self, LineEnding, PrivateKey as SshPrivateKey, PublicKey as SshPublicKey,
35 public::Ed25519PublicKey as SshEd25519PublicKey,
36};
37#[cfg(unix)]
38use std::io;
39#[cfg(unix)]
40use std::sync::Arc;
41#[cfg(unix)]
42use tokio::net::UnixListener;
43use zeroize::Zeroizing;
44
45#[cfg(target_os = "macos")]
46use anyhow::Context;
47#[cfg(target_os = "macos")]
48use std::io::Write;
49
50#[cfg(target_os = "macos")]
51use {
52 anyhow::Result as AnyhowResult,
53 std::fs::{self, Permissions},
54 std::os::unix::fs::PermissionsExt,
55 tempfile::Builder as TempFileBuilder,
56};
57
58#[derive(Serialize, Debug, Clone)]
62pub struct KeyLoadStatus {
63 pub alias: KeyAlias,
65 pub loaded: bool,
67 pub error: Option<String>,
69}
70
71#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
73pub enum RegistrationOutcome {
74 Added,
76 AlreadyExists,
78 AgentNotFound,
80 CommandFailed,
82 UnsupportedKeyType,
84 ConversionFailed,
86 IoError,
88 InternalError,
90}
91
92#[derive(Serialize, Debug, Clone)]
94pub struct KeyRegistrationStatus {
95 pub fingerprint: String,
97 pub status: RegistrationOutcome,
99 pub message: Option<String>,
101}
102
103pub fn clear_agent_keys_with_handle(handle: &AgentHandle) -> Result<(), AgentError> {
121 info!("Clearing all keys from agent handle.");
122 let mut agent_guard = handle.lock()?;
123 agent_guard.clear_keys();
124 debug!("Agent keys cleared.");
125 Ok(())
126}
127
128pub fn load_keys_into_agent_with_handle(
144 handle: &AgentHandle,
145 aliases: Vec<String>,
146 passphrase_provider: &dyn PassphraseProvider,
147 keychain: &(dyn KeyStorage + Send + Sync),
148) -> Result<Vec<KeyLoadStatus>, AgentError> {
149 info!(
150 "Attempting to load keys into agent handle for aliases: {:?}",
151 aliases
152 );
153 if aliases.is_empty() {
154 warn!("load_keys_into_agent_with_handle called with empty alias list. Clearing agent.");
155 clear_agent_keys_with_handle(handle)?;
156 return Ok(vec![]);
157 }
158
159 let mut load_statuses = Vec::new();
160 let mut temp_unlocked_core = AgentCore::default();
161
162 for alias in aliases {
163 debug!("Processing alias for agent load: {}", alias);
164 let key_alias = KeyAlias::new_unchecked(&alias);
165 let mut status = KeyLoadStatus {
166 alias: key_alias.clone(),
167 loaded: false,
168 error: None,
169 };
170
171 let load_result = || -> Result<Zeroizing<Vec<u8>>, AgentError> {
172 let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
173 let prompt = format!(
174 "Enter passphrase to unlock key '{}' for agent session:",
175 key_alias
176 );
177 let passphrase = passphrase_provider.get_passphrase(&prompt)?;
178 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, &passphrase)?;
179 let _ = extract_seed_from_key_bytes(&pkcs8_bytes).map_err(|e| {
180 AgentError::KeyDeserializationError(format!(
181 "Failed to parse key for alias '{}' after decryption: {}",
182 key_alias, e
183 ))
184 })?;
185 Ok(pkcs8_bytes)
186 }();
187
188 match load_result {
189 Ok(pkcs8_bytes) => {
190 info!("Successfully unlocked key for alias '{}'", key_alias);
191 match temp_unlocked_core.register_key(pkcs8_bytes) {
192 Ok(()) => status.loaded = true,
193 Err(e) => {
194 error!(
195 "Failed to register key '{}' in agent core state after successful unlock/parse: {}",
196 key_alias, e
197 );
198 status.error = Some(format!(
199 "Internal error: Failed to register key in agent core state: {}",
200 e
201 ));
202 }
203 }
204 }
205 Err(e) => {
206 error!(
207 "Failed to load/decrypt key for alias '{}': {}",
208 key_alias, e
209 );
210 match e {
211 AgentError::IncorrectPassphrase => {
212 status.error = Some("Incorrect passphrase".to_string())
213 }
214 AgentError::KeyNotFound => status.error = Some("Key not found".to_string()),
215 AgentError::UserInputCancelled => {
216 status.error = Some("Operation cancelled by user".to_string())
217 }
218 AgentError::KeyDeserializationError(_) => {
219 status.error = Some(format!("Failed to parse key after decryption: {}", e))
220 }
221 _ => status.error = Some(e.to_string()),
222 }
223 }
224 }
225 load_statuses.push(status);
226 }
227
228 let mut agent_guard = handle.lock()?;
230 info!(
231 "Replacing agent core with {} unlocked keys ({} aliases attempted).",
232 temp_unlocked_core.key_count(),
233 load_statuses.len()
234 );
235 *agent_guard = temp_unlocked_core;
236
237 Ok(load_statuses)
238}
239
240pub fn rotate_key(
263 alias: &str,
264 new_passphrase: &str,
265 keychain: &(dyn KeyStorage + Send + Sync),
266) -> Result<(), AgentError> {
267 info!(
268 "[API] Attempting secure storage key rotation for local alias: {}",
269 alias
270 );
271 if alias.trim().is_empty() {
272 return Err(AgentError::InvalidInput(
273 "Alias cannot be empty".to_string(),
274 ));
275 }
276 if new_passphrase.is_empty() {
277 return Err(AgentError::InvalidInput(
278 "New passphrase cannot be empty".to_string(),
279 ));
280 }
281
282 let key_alias = KeyAlias::new_unchecked(alias);
284 let existing_did = keychain.get_identity_for_alias(&key_alias)?;
285 info!(
286 "Found existing key for alias '{}', associated with Controller DID '{}'. Proceeding with rotation.",
287 alias, existing_did
288 );
289
290 let (seed, pubkey) = provider_bridge::generate_ed25519_keypair_sync()
292 .map_err(|e| AgentError::CryptoError(format!("Failed to generate new keypair: {}", e)))?;
293 let new_pkcs8_bytes = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey);
295 debug!("Generated new keypair via CryptoProvider.");
296
297 let encrypted_new_key = encrypt_keypair(&new_pkcs8_bytes, new_passphrase)?;
299 debug!("Encrypted new keypair with provided passphrase.");
300
301 keychain.store_key(&key_alias, &existing_did, &encrypted_new_key)?;
304 info!(
305 "Successfully overwrote secure storage for alias '{}' with new encrypted key.",
306 alias
307 );
308
309 warn!(
310 "Secure storage key rotated for alias '{}'. This did NOT update any Git identity representation. The running agent may still hold the old decrypted key. Consider reloading keys into the agent.",
311 alias
312 );
313 Ok(())
314}
315
316pub fn agent_sign_with_handle(
331 handle: &AgentHandle,
332 pubkey: &[u8],
333 data: &[u8],
334) -> Result<Vec<u8>, AgentError> {
335 debug!(
336 "Agent sign request for pubkey starting with: {:x?}...",
337 &pubkey[..core::cmp::min(pubkey.len(), 8)]
338 );
339
340 handle.sign(pubkey, data)
342}
343
344pub fn export_key_openssh_pem(
356 alias: &str,
357 passphrase: &str,
358 keychain: &(dyn KeyStorage + Send + Sync),
359) -> Result<Zeroizing<String>, AgentError> {
360 info!("Exporting PEM for local alias: {}", alias);
361 if alias.trim().is_empty() {
362 return Err(AgentError::InvalidInput(
363 "Alias cannot be empty".to_string(),
364 ));
365 }
366 let key_alias = KeyAlias::new_unchecked(alias);
368 let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
369
370 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
372
373 let secure_seed = crate::crypto::ssh::extract_seed_from_pkcs8(&pkcs8_bytes).map_err(|e| {
375 AgentError::KeyDeserializationError(format!(
376 "Failed to extract Ed25519 seed for alias '{}': {}",
377 alias, e
378 ))
379 })?;
380
381 let ssh_ed_keypair = SshEdKeypair::from_seed(secure_seed.as_bytes());
382 let keypair_data = KeypairData::Ed25519(ssh_ed_keypair);
383 let ssh_private_key = SshPrivateKey::new(keypair_data, "") .map_err(|e| {
386 AgentError::CryptoError(format!(
388 "Failed to create ssh_key::PrivateKey for alias '{}': {}",
389 alias, e
390 ))
391 })?;
392
393 let pem = ssh_private_key.to_openssh(LineEnding::LF).map_err(|e| {
395 AgentError::CryptoError(format!(
397 "Failed to encode OpenSSH PEM for alias '{}': {}",
398 alias, e
399 ))
400 })?;
401
402 debug!("Successfully generated PEM for alias '{}'", alias);
403 Ok(pem) }
405
406pub fn export_key_openssh_pub(
419 alias: &str,
420 passphrase: &str,
421 keychain: &(dyn KeyStorage + Send + Sync),
422) -> Result<String, AgentError> {
423 info!("Exporting OpenSSH public key for local alias: {}", alias);
424 if alias.trim().is_empty() {
425 return Err(AgentError::InvalidInput(
426 "Alias cannot be empty".to_string(),
427 ));
428 }
429 let key_alias = KeyAlias::new_unchecked(alias);
431 let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
432
433 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
435
436 let (seed, pubkey_bytes) =
438 crate::crypto::signer::load_seed_and_pubkey(&pkcs8_bytes).map_err(|e| {
439 AgentError::CryptoError(format!(
440 "Failed to extract key for alias '{}': {}",
441 alias, e
442 ))
443 })?;
444 let _ = seed; let ssh_ed25519_pubkey =
446 SshEd25519PublicKey::try_from(pubkey_bytes.as_slice()).map_err(|e| {
447 AgentError::CryptoError(format!(
448 "Failed to create Ed25519PublicKey from bytes: {}",
449 e
450 ))
451 })?;
452 let key_data = ssh_key::public::KeyData::Ed25519(ssh_ed25519_pubkey);
453
454 let ssh_pub_key = SshPublicKey::new(key_data, ""); let pubkey_base = ssh_pub_key.to_openssh().map_err(|e| {
459 AgentError::CryptoError(format!("Failed to format OpenSSH pubkey base: {}", e))
461 })?;
462
463 let formatted_pubkey = format!("{} {}", pubkey_base, alias);
465
466 debug!(
467 "Successfully generated OpenSSH public key string for alias '{}'",
468 alias
469 );
470 Ok(formatted_pubkey)
471}
472
473pub fn get_agent_key_count_with_handle(handle: &AgentHandle) -> Result<usize, AgentError> {
481 handle.key_count()
482}
483
484#[cfg(target_os = "macos")]
503pub fn register_keys_with_macos_agent_with_handle(
504 handle: &AgentHandle,
505 ssh_agent_socket: Option<&std::path::Path>,
506 ssh_agent: &dyn crate::ports::ssh_agent::SshAgentPort,
507) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
508 info!("Attempting to register keys from agent handle with system ssh-agent...");
509 if ssh_agent_socket.is_none() {
510 warn!("SSH_AUTH_SOCK not configured. System ssh-agent may not be running or configured.");
511 }
512
513 let keys_to_register: Vec<(Vec<u8>, Zeroizing<Vec<u8>>)> = {
514 let agent_guard = handle.lock()?;
515 agent_guard
516 .keys
517 .iter()
518 .map(|(pubkey, seed)| {
519 let pubkey_arr: [u8; 32] = pubkey.as_slice().try_into().unwrap_or([0u8; 32]);
520 let pkcs8 = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey_arr);
521 (pubkey.clone(), Zeroizing::new(pkcs8))
522 })
523 .collect()
524 };
525
526 register_keys_with_macos_agent_internal(keys_to_register, ssh_agent)
527}
528
529#[cfg(not(target_os = "macos"))]
531pub fn register_keys_with_macos_agent_with_handle(
532 _handle: &AgentHandle,
533 _ssh_agent_socket: Option<&std::path::Path>,
534 _ssh_agent: &dyn crate::ports::ssh_agent::SshAgentPort,
535) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
536 info!("Not on macOS, skipping system ssh-agent registration.");
537 Ok(vec![])
538}
539
540#[cfg(target_os = "macos")]
545#[allow(clippy::too_many_lines)]
546fn register_keys_with_macos_agent_internal(
547 keys_to_register: Vec<(Vec<u8>, Zeroizing<Vec<u8>>)>,
548 ssh_agent: &dyn crate::ports::ssh_agent::SshAgentPort,
549) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
550 use crate::ports::ssh_agent::SshAgentError;
551
552 if keys_to_register.is_empty() {
553 info!("No keys to register with system agent.");
554 return Ok(vec![]);
555 }
556 info!(
557 "Found {} keys to attempt registration with system agent.",
558 keys_to_register.len()
559 );
560
561 let mut results = Vec::with_capacity(keys_to_register.len());
562
563 for (pubkey_bytes, pkcs8_bytes_zeroizing) in keys_to_register.into_iter() {
564 let fingerprint_str = (|| -> Result<String, AgentError> {
565 let pk = SshEd25519PublicKey::try_from(pubkey_bytes.as_slice()).map_err(|e| {
566 AgentError::KeyDeserializationError(format!(
567 "Invalid pubkey bytes for fingerprint: {}",
568 e
569 ))
570 })?;
571 let ssh_pub_key: SshPublicKey =
572 SshPublicKey::new(ssh_key::public::KeyData::Ed25519(pk), "");
573 let fp: Fingerprint = ssh_pub_key.fingerprint(Default::default());
574 Ok(fp.to_string())
575 })()
576 .unwrap_or_else(|e| {
577 warn!(
578 "Could not calculate fingerprint for key being registered: {}",
579 e
580 );
581 "unknown_fingerprint".to_string()
582 });
583
584 let mut status = KeyRegistrationStatus {
585 fingerprint: fingerprint_str.clone(),
586 status: RegistrationOutcome::InternalError,
587 message: None,
588 };
589
590 let result: AnyhowResult<()> = (|| {
591 let pkcs8_bytes = pkcs8_bytes_zeroizing.as_ref();
592 let private_key_info = PrivateKeyInfo::from_der(pkcs8_bytes)
593 .context("Failed to parse PKCS#8 DER for ssh-add")?;
594 let seed_octet_string = OctetString::from_der(private_key_info.private_key)
595 .context("Failed to parse PKCS#8 seed OctetString for ssh-add")?;
596 let seed_bytes = seed_octet_string.as_bytes();
597 if seed_bytes.len() != 32 {
598 anyhow::bail!(
599 "Invalid seed length {} in PKCS#8 for ssh-add",
600 seed_bytes.len()
601 );
602 }
603 #[allow(clippy::expect_used)] let seed_array: [u8; 32] = seed_bytes.try_into().expect("Length checked");
605 let ssh_ed_keypair = SshEdKeypair::from_seed(&seed_array);
606 let keypair_data = KeypairData::Ed25519(ssh_ed_keypair);
607 let ssh_private_key = SshPrivateKey::new(keypair_data, "")
608 .context("Failed to create ssh_key::PrivateKey for ssh-add")?;
609 let pem_zeroizing = ssh_private_key
610 .to_openssh(LineEnding::LF)
611 .context("Failed to encode OpenSSH PEM for ssh-add")?;
612 let pem_string = pem_zeroizing.to_string();
613
614 let mut temp_file_guard = TempFileBuilder::new()
615 .prefix("auths-key-")
616 .suffix(".pem")
617 .rand_bytes(5)
618 .tempfile()
619 .context("Failed to create temporary file for ssh-add")?;
620 if let Err(e) =
621 fs::set_permissions(temp_file_guard.path(), Permissions::from_mode(0o600))
622 {
623 warn!(
624 "Failed to set 600 permissions on temp file {:?}: {}. Continuing...",
625 temp_file_guard.path(),
626 e
627 );
628 }
629 temp_file_guard
630 .write_all(pem_string.as_bytes())
631 .context("Failed to write PEM to temporary file")?;
632 temp_file_guard
633 .flush()
634 .context("Failed to flush temporary PEM file")?;
635 let temp_file_path = temp_file_guard.path().to_path_buf();
636
637 debug!(
638 "Attempting ssh-add for temporary key file: {:?}",
639 temp_file_path
640 );
641 ssh_agent
642 .register_key(&temp_file_path)
643 .map_err(|e| anyhow::anyhow!("{e}"))?;
644 debug!("ssh-add finished for {:?}", temp_file_path);
645 Ok(())
646 })();
647
648 match result {
649 Ok(()) => {
650 info!(
651 "ssh-add successful for {}: Identity added.",
652 fingerprint_str
653 );
654 status.status = RegistrationOutcome::Added;
655 status.message = Some("Identity added via ssh-agent port".to_string());
656 }
657 Err(e) => {
658 if let Some(agent_err) = e.downcast_ref::<SshAgentError>() {
660 match agent_err {
661 SshAgentError::NotAvailable(_) => {
662 status.status = RegistrationOutcome::AgentNotFound;
663 }
664 SshAgentError::CommandFailed(_) => {
665 status.status = RegistrationOutcome::CommandFailed;
666 }
667 SshAgentError::IoError(_) => {
668 status.status = RegistrationOutcome::IoError;
669 }
670 }
671 } else if e.downcast_ref::<std::io::Error>().is_some() {
672 status.status = RegistrationOutcome::IoError;
673 } else if e.downcast_ref::<ssh_key::Error>().is_some()
674 || e.downcast_ref::<pkcs8::Error>().is_some()
675 || e.downcast_ref::<pkcs8::der::Error>().is_some()
676 {
677 status.status = RegistrationOutcome::ConversionFailed;
678 } else {
679 status.status = RegistrationOutcome::InternalError;
680 }
681 error!(
682 "Error during registration process for {}: {:?}",
683 fingerprint_str, e
684 );
685 status.message = Some(format!("Registration error: {}", e));
686 }
687 }
688 results.push(status);
689 }
690
691 info!(
692 "Finished attempting system agent registration for {} keys.",
693 results.len()
694 );
695 Ok(results)
696}
697
698#[cfg(unix)]
713pub async fn start_agent_listener_with_handle(handle: Arc<AgentHandle>) -> Result<(), AgentError> {
714 let socket_path = handle.socket_path();
715 info!("Attempting to start agent listener at {:?}", socket_path);
716
717 if let Some(parent) = socket_path.parent()
719 && !parent.exists()
720 {
721 debug!("Creating parent directory for socket: {:?}", parent);
722 if let Err(e) = std::fs::create_dir_all(parent) {
723 error!("Failed to create parent directory {:?}: {}", parent, e);
724 return Err(AgentError::IO(e));
725 }
726 }
727
728 match std::fs::remove_file(socket_path) {
730 Ok(()) => info!("Removed existing socket file at {:?}", socket_path),
731 Err(e) if e.kind() == io::ErrorKind::NotFound => {
732 debug!(
733 "No existing socket file found at {:?}, proceeding.",
734 socket_path
735 );
736 }
737 Err(e) => {
738 warn!(
739 "Failed to remove existing socket file at {:?}: {}. Binding might fail.",
740 socket_path, e
741 );
742 }
743 }
744
745 let listener = UnixListener::bind(socket_path).map_err(|e| {
747 error!("Failed to bind listener socket at {:?}: {}", socket_path, e);
748 AgentError::IO(e)
749 })?;
750
751 let actual_path = socket_path
753 .canonicalize()
754 .unwrap_or_else(|_| socket_path.to_path_buf());
755 info!(
756 "🚀 Agent listener started successfully at {:?}",
757 actual_path
758 );
759 info!(" Set SSH_AUTH_SOCK={:?} to use this agent.", actual_path);
760
761 handle.set_running(true);
763
764 let session = AgentSession::new(handle.clone());
766
767 let result = listen(listener, session).await;
769
770 handle.set_running(false);
772
773 if let Err(e) = result {
774 error!("SSH Agent listener failed: {:?}", e);
775 return Err(AgentError::IO(io::Error::other(format!(
776 "SSH Agent listener failed: {}",
777 e
778 ))));
779 }
780
781 warn!("Agent listener loop exited unexpectedly without error.");
782 Ok(())
783}
784
785#[cfg(unix)]
800pub async fn start_agent_listener(socket_path_str: String) -> Result<(), AgentError> {
801 use std::path::PathBuf;
802 let handle = Arc::new(AgentHandle::new(PathBuf::from(&socket_path_str)));
803 start_agent_listener_with_handle(handle).await
804}