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, std::fs::{self, Permissions},
54 std::os::unix::fs::PermissionsExt,
55 std::process::{Command, Output},
56 tempfile::Builder as TempFileBuilder,
57};
58
59#[derive(Serialize, Debug, Clone)]
63pub struct KeyLoadStatus {
64 pub alias: KeyAlias,
66 pub loaded: bool,
68 pub error: Option<String>,
70}
71
72#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
74pub enum RegistrationOutcome {
75 Added,
77 AlreadyExists,
79 AgentNotFound,
81 CommandFailed,
83 UnsupportedKeyType,
85 ConversionFailed,
87 IoError,
89 InternalError,
91}
92
93#[derive(Serialize, Debug, Clone)]
95pub struct KeyRegistrationStatus {
96 pub fingerprint: String,
98 pub status: RegistrationOutcome,
100 pub message: Option<String>,
102}
103
104pub fn clear_agent_keys_with_handle(handle: &AgentHandle) -> Result<(), AgentError> {
122 info!("Clearing all keys from agent handle.");
123 let mut agent_guard = handle.lock()?;
124 agent_guard.clear_keys();
125 debug!("Agent keys cleared.");
126 Ok(())
127}
128
129pub fn load_keys_into_agent_with_handle(
145 handle: &AgentHandle,
146 aliases: Vec<String>,
147 passphrase_provider: &dyn PassphraseProvider,
148 keychain: &(dyn KeyStorage + Send + Sync),
149) -> Result<Vec<KeyLoadStatus>, AgentError> {
150 info!(
151 "Attempting to load keys into agent handle for aliases: {:?}",
152 aliases
153 );
154 if aliases.is_empty() {
155 warn!("load_keys_into_agent_with_handle called with empty alias list. Clearing agent.");
156 clear_agent_keys_with_handle(handle)?;
157 return Ok(vec![]);
158 }
159
160 let mut load_statuses = Vec::new();
161 let mut temp_unlocked_core = AgentCore::default();
162
163 for alias in aliases {
164 debug!("Processing alias for agent load: {}", alias);
165 let key_alias = KeyAlias::new_unchecked(&alias);
166 let mut status = KeyLoadStatus {
167 alias: key_alias.clone(),
168 loaded: false,
169 error: None,
170 };
171
172 let load_result = || -> Result<Zeroizing<Vec<u8>>, AgentError> {
173 let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
174 let prompt = format!(
175 "Enter passphrase to unlock key '{}' for agent session:",
176 key_alias
177 );
178 let passphrase = passphrase_provider.get_passphrase(&prompt)?;
179 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, &passphrase)?;
180 let _ = extract_seed_from_key_bytes(&pkcs8_bytes).map_err(|e| {
181 AgentError::KeyDeserializationError(format!(
182 "Failed to parse key for alias '{}' after decryption: {}",
183 key_alias, e
184 ))
185 })?;
186 Ok(pkcs8_bytes)
187 }();
188
189 match load_result {
190 Ok(pkcs8_bytes) => {
191 info!("Successfully unlocked key for alias '{}'", key_alias);
192 match temp_unlocked_core.register_key(pkcs8_bytes) {
193 Ok(()) => status.loaded = true,
194 Err(e) => {
195 error!(
196 "Failed to register key '{}' in agent core state after successful unlock/parse: {}",
197 key_alias, e
198 );
199 status.error = Some(format!(
200 "Internal error: Failed to register key in agent core state: {}",
201 e
202 ));
203 }
204 }
205 }
206 Err(e) => {
207 error!(
208 "Failed to load/decrypt key for alias '{}': {}",
209 key_alias, e
210 );
211 match e {
212 AgentError::IncorrectPassphrase => {
213 status.error = Some("Incorrect passphrase".to_string())
214 }
215 AgentError::KeyNotFound => status.error = Some("Key not found".to_string()),
216 AgentError::UserInputCancelled => {
217 status.error = Some("Operation cancelled by user".to_string())
218 }
219 AgentError::KeyDeserializationError(_) => {
220 status.error = Some(format!("Failed to parse key after decryption: {}", e))
221 }
222 _ => status.error = Some(e.to_string()),
223 }
224 }
225 }
226 load_statuses.push(status);
227 }
228
229 let mut agent_guard = handle.lock()?;
231 info!(
232 "Replacing agent core with {} unlocked keys ({} aliases attempted).",
233 temp_unlocked_core.key_count(),
234 load_statuses.len()
235 );
236 *agent_guard = temp_unlocked_core;
237
238 Ok(load_statuses)
239}
240
241pub fn rotate_key(
264 alias: &str,
265 new_passphrase: &str,
266 keychain: &(dyn KeyStorage + Send + Sync),
267) -> Result<(), AgentError> {
268 info!(
269 "[API] Attempting secure storage key rotation for local alias: {}",
270 alias
271 );
272 if alias.trim().is_empty() {
273 return Err(AgentError::InvalidInput(
274 "Alias cannot be empty".to_string(),
275 ));
276 }
277 if new_passphrase.is_empty() {
278 return Err(AgentError::InvalidInput(
279 "New passphrase cannot be empty".to_string(),
280 ));
281 }
282
283 let key_alias = KeyAlias::new_unchecked(alias);
285 let existing_did = keychain.get_identity_for_alias(&key_alias)?;
286 info!(
287 "Found existing key for alias '{}', associated with Controller DID '{}'. Proceeding with rotation.",
288 alias, existing_did
289 );
290
291 let (seed, pubkey) = provider_bridge::generate_ed25519_keypair_sync()
293 .map_err(|e| AgentError::CryptoError(format!("Failed to generate new keypair: {}", e)))?;
294 let new_pkcs8_bytes = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey);
296 debug!("Generated new keypair via CryptoProvider.");
297
298 let encrypted_new_key = encrypt_keypair(&new_pkcs8_bytes, new_passphrase)?;
300 debug!("Encrypted new keypair with provided passphrase.");
301
302 keychain.store_key(&key_alias, &existing_did, &encrypted_new_key)?;
305 info!(
306 "Successfully overwrote secure storage for alias '{}' with new encrypted key.",
307 alias
308 );
309
310 warn!(
311 "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.",
312 alias
313 );
314 Ok(())
315}
316
317pub fn agent_sign_with_handle(
332 handle: &AgentHandle,
333 pubkey: &[u8],
334 data: &[u8],
335) -> Result<Vec<u8>, AgentError> {
336 debug!(
337 "Agent sign request for pubkey starting with: {:x?}...",
338 &pubkey[..core::cmp::min(pubkey.len(), 8)]
339 );
340
341 handle.sign(pubkey, data)
343}
344
345pub fn export_key_openssh_pem(
357 alias: &str,
358 passphrase: &str,
359 keychain: &(dyn KeyStorage + Send + Sync),
360) -> Result<Zeroizing<String>, AgentError> {
361 info!("Exporting PEM for local alias: {}", alias);
362 if alias.trim().is_empty() {
363 return Err(AgentError::InvalidInput(
364 "Alias cannot be empty".to_string(),
365 ));
366 }
367 let key_alias = KeyAlias::new_unchecked(alias);
369 let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
370
371 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
373
374 let secure_seed = crate::crypto::ssh::extract_seed_from_pkcs8(&pkcs8_bytes).map_err(|e| {
376 AgentError::KeyDeserializationError(format!(
377 "Failed to extract Ed25519 seed for alias '{}': {}",
378 alias, e
379 ))
380 })?;
381
382 let ssh_ed_keypair = SshEdKeypair::from_seed(secure_seed.as_bytes());
383 let keypair_data = KeypairData::Ed25519(ssh_ed_keypair);
384 let ssh_private_key = SshPrivateKey::new(keypair_data, "") .map_err(|e| {
387 AgentError::CryptoError(format!(
389 "Failed to create ssh_key::PrivateKey for alias '{}': {}",
390 alias, e
391 ))
392 })?;
393
394 let pem = ssh_private_key.to_openssh(LineEnding::LF).map_err(|e| {
396 AgentError::CryptoError(format!(
398 "Failed to encode OpenSSH PEM for alias '{}': {}",
399 alias, e
400 ))
401 })?;
402
403 debug!("Successfully generated PEM for alias '{}'", alias);
404 Ok(pem) }
406
407pub fn export_key_openssh_pub(
420 alias: &str,
421 passphrase: &str,
422 keychain: &(dyn KeyStorage + Send + Sync),
423) -> Result<String, AgentError> {
424 info!("Exporting OpenSSH public key for local alias: {}", alias);
425 if alias.trim().is_empty() {
426 return Err(AgentError::InvalidInput(
427 "Alias cannot be empty".to_string(),
428 ));
429 }
430 let key_alias = KeyAlias::new_unchecked(alias);
432 let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
433
434 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
436
437 let (seed, pubkey_bytes) =
439 crate::crypto::signer::load_seed_and_pubkey(&pkcs8_bytes).map_err(|e| {
440 AgentError::CryptoError(format!(
441 "Failed to extract key for alias '{}': {}",
442 alias, e
443 ))
444 })?;
445 let _ = seed; let ssh_ed25519_pubkey =
447 SshEd25519PublicKey::try_from(pubkey_bytes.as_slice()).map_err(|e| {
448 AgentError::CryptoError(format!(
449 "Failed to create Ed25519PublicKey from bytes: {}",
450 e
451 ))
452 })?;
453 let key_data = ssh_key::public::KeyData::Ed25519(ssh_ed25519_pubkey);
454
455 let ssh_pub_key = SshPublicKey::new(key_data, ""); let pubkey_base = ssh_pub_key.to_openssh().map_err(|e| {
460 AgentError::CryptoError(format!("Failed to format OpenSSH pubkey base: {}", e))
462 })?;
463
464 let formatted_pubkey = format!("{} {}", pubkey_base, alias);
466
467 debug!(
468 "Successfully generated OpenSSH public key string for alias '{}'",
469 alias
470 );
471 Ok(formatted_pubkey)
472}
473
474pub fn get_agent_key_count_with_handle(handle: &AgentHandle) -> Result<usize, AgentError> {
482 handle.key_count()
483}
484
485#[cfg(target_os = "macos")]
498pub fn register_keys_with_macos_agent_with_handle(
499 handle: &AgentHandle,
500 ssh_agent_socket: Option<&std::path::Path>,
501) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
502 info!("Attempting to register keys from agent handle with macOS system ssh-agent...");
503 if ssh_agent_socket.is_none() {
504 warn!("SSH_AUTH_SOCK not configured. System ssh-agent may not be running or configured.");
505 }
506
507 let keys_to_register: Vec<(Vec<u8>, Zeroizing<Vec<u8>>)> = {
508 let agent_guard = handle.lock()?;
509 agent_guard
510 .keys
511 .iter()
512 .map(|(pubkey, seed)| {
513 let pubkey_arr: [u8; 32] = pubkey.as_slice().try_into().unwrap_or([0u8; 32]);
514 let pkcs8 = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey_arr);
515 (pubkey.clone(), Zeroizing::new(pkcs8))
516 })
517 .collect()
518 };
519
520 register_keys_with_macos_agent_internal(keys_to_register)
521}
522
523#[cfg(not(target_os = "macos"))]
525pub fn register_keys_with_macos_agent_with_handle(
526 _handle: &AgentHandle,
527 _ssh_agent_socket: Option<&std::path::Path>,
528) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
529 info!("Not on macOS, skipping system ssh-agent registration.");
530 Ok(vec![])
531}
532
533#[cfg(target_os = "macos")]
535#[allow(clippy::too_many_lines)]
536fn register_keys_with_macos_agent_internal(
537 keys_to_register: Vec<(Vec<u8>, Zeroizing<Vec<u8>>)>,
538) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
539 if keys_to_register.is_empty() {
540 info!("No keys to register with system agent.");
541 return Ok(vec![]);
542 }
543 info!(
544 "Found {} keys to attempt registration with system agent.",
545 keys_to_register.len()
546 );
547
548 let mut results = Vec::with_capacity(keys_to_register.len());
549
550 for (pubkey_bytes, pkcs8_bytes_zeroizing) in keys_to_register.into_iter() {
551 let fingerprint_str = (|| -> Result<String, AgentError> {
552 let pk = SshEd25519PublicKey::try_from(pubkey_bytes.as_slice()).map_err(|e| {
553 AgentError::KeyDeserializationError(format!(
554 "Invalid pubkey bytes for fingerprint: {}",
555 e
556 ))
557 })?;
558 let ssh_pub_key: SshPublicKey =
559 SshPublicKey::new(ssh_key::public::KeyData::Ed25519(pk), "");
560 let fp: Fingerprint = ssh_pub_key.fingerprint(Default::default());
561 Ok(fp.to_string())
562 })()
563 .unwrap_or_else(|e| {
564 warn!(
565 "Could not calculate fingerprint for key being registered: {}",
566 e
567 );
568 "unknown_fingerprint".to_string()
569 });
570
571 let mut status = KeyRegistrationStatus {
572 fingerprint: fingerprint_str.clone(),
573 status: RegistrationOutcome::InternalError,
574 message: None,
575 };
576
577 let result: AnyhowResult<Output> = (|| {
578 let pkcs8_bytes = pkcs8_bytes_zeroizing.as_ref();
579 let private_key_info = PrivateKeyInfo::from_der(pkcs8_bytes)
580 .context("Failed to parse PKCS#8 DER for ssh-add")?;
581 let seed_octet_string = OctetString::from_der(private_key_info.private_key)
582 .context("Failed to parse PKCS#8 seed OctetString for ssh-add")?;
583 let seed_bytes = seed_octet_string.as_bytes();
584 if seed_bytes.len() != 32 {
585 anyhow::bail!(
586 "Invalid seed length {} in PKCS#8 for ssh-add",
587 seed_bytes.len()
588 );
589 }
590 let seed_array: [u8; 32] = seed_bytes.try_into().expect("Length checked");
591 let ssh_ed_keypair = SshEdKeypair::from_seed(&seed_array);
592 let keypair_data = KeypairData::Ed25519(ssh_ed_keypair);
593 let ssh_private_key = SshPrivateKey::new(keypair_data, "")
594 .context("Failed to create ssh_key::PrivateKey for ssh-add")?;
595 let pem_zeroizing = ssh_private_key
596 .to_openssh(LineEnding::LF)
597 .context("Failed to encode OpenSSH PEM for ssh-add")?;
598 let pem_string = pem_zeroizing.to_string();
599
600 let mut temp_file_guard = TempFileBuilder::new()
601 .prefix("auths-key-")
602 .suffix(".pem")
603 .rand_bytes(5)
604 .tempfile()
605 .context("Failed to create temporary file for ssh-add")?;
606 if let Err(e) =
607 fs::set_permissions(temp_file_guard.path(), Permissions::from_mode(0o600))
608 {
609 warn!(
610 "Failed to set 600 permissions on temp file {:?}: {}. Continuing...",
611 temp_file_guard.path(),
612 e
613 );
614 }
615 temp_file_guard
616 .write_all(pem_string.as_bytes())
617 .context("Failed to write PEM to temporary file")?;
618 temp_file_guard
619 .flush()
620 .context("Failed to flush temporary PEM file")?;
621 let temp_file_path = temp_file_guard.path().to_path_buf();
622
623 debug!(
624 "Attempting ssh-add for temporary key file: {:?}",
625 temp_file_path
626 );
627 let output = Command::new("ssh-add").arg(&temp_file_path).output()?;
628 debug!("ssh-add command finished for {:?}", temp_file_path);
629 Ok(output)
630 })();
631
632 match result {
633 Ok(output) => {
634 let stdout = String::from_utf8_lossy(&output.stdout)
635 .trim()
636 .to_lowercase();
637 let stderr = String::from_utf8_lossy(&output.stderr)
638 .trim()
639 .to_lowercase();
640 let full_output = format!("ssh-add stdout: '{}', stderr: '{}'", stdout, stderr);
641 status.message = Some(full_output.clone());
642
643 if output.status.success() {
644 if stdout.contains("identity added") {
645 info!(
646 "ssh-add successful for {}: Identity added.",
647 fingerprint_str
648 );
649 status.status = RegistrationOutcome::Added;
650 } else if stderr.contains("already exists") || stdout.contains("already exists")
651 {
652 info!(
653 "ssh-add skipped for {}: Identity already exists.",
654 fingerprint_str
655 );
656 status.status = RegistrationOutcome::AlreadyExists;
657 } else {
658 warn!(
659 "ssh-add command succeeded for {} but output was unexpected: {}",
660 fingerprint_str, full_output
661 );
662 status.status = RegistrationOutcome::Added;
663 }
664 } else {
665 error!(
666 "ssh-add command failed for {}: {}",
667 fingerprint_str, full_output
668 );
669 if stderr.contains("could not open a connection")
670 || stderr.contains("connection refused")
671 || stderr.contains("communication with agent failed")
672 {
673 status.status = RegistrationOutcome::AgentNotFound;
674 } else {
675 status.status = RegistrationOutcome::CommandFailed;
676 }
677 }
678 }
679 Err(e) => {
680 error!(
681 "Error during registration process for {}: {:?}",
682 fingerprint_str, e
683 );
684 if e.downcast_ref::<std::io::Error>().is_some() {
685 status.status = RegistrationOutcome::IoError;
686 } else if e.downcast_ref::<ssh_key::Error>().is_some()
687 || e.downcast_ref::<pkcs8::Error>().is_some()
688 || e.downcast_ref::<pkcs8::der::Error>().is_some()
689 {
690 status.status = RegistrationOutcome::ConversionFailed;
691 } else {
692 status.status = RegistrationOutcome::InternalError;
693 }
694 status.message = Some(format!("Internal error during registration: {}", e));
695 }
696 }
697 results.push(status);
698 }
699
700 info!(
701 "Finished attempting macOS system agent registration for {} keys.",
702 results.len()
703 );
704 Ok(results)
705}
706
707#[cfg(unix)]
722pub async fn start_agent_listener_with_handle(handle: Arc<AgentHandle>) -> Result<(), AgentError> {
723 let socket_path = handle.socket_path();
724 info!("Attempting to start agent listener at {:?}", socket_path);
725
726 if let Some(parent) = socket_path.parent()
728 && !parent.exists()
729 {
730 debug!("Creating parent directory for socket: {:?}", parent);
731 if let Err(e) = std::fs::create_dir_all(parent) {
732 error!("Failed to create parent directory {:?}: {}", parent, e);
733 return Err(AgentError::IO(e));
734 }
735 }
736
737 match std::fs::remove_file(socket_path) {
739 Ok(()) => info!("Removed existing socket file at {:?}", socket_path),
740 Err(e) if e.kind() == io::ErrorKind::NotFound => {
741 debug!(
742 "No existing socket file found at {:?}, proceeding.",
743 socket_path
744 );
745 }
746 Err(e) => {
747 warn!(
748 "Failed to remove existing socket file at {:?}: {}. Binding might fail.",
749 socket_path, e
750 );
751 }
752 }
753
754 let listener = UnixListener::bind(socket_path).map_err(|e| {
756 error!("Failed to bind listener socket at {:?}: {}", socket_path, e);
757 AgentError::IO(e)
758 })?;
759
760 let actual_path = socket_path
762 .canonicalize()
763 .unwrap_or_else(|_| socket_path.to_path_buf());
764 info!(
765 "🚀 Agent listener started successfully at {:?}",
766 actual_path
767 );
768 info!(" Set SSH_AUTH_SOCK={:?} to use this agent.", actual_path);
769
770 handle.set_running(true);
772
773 let session = AgentSession::new(handle.clone());
775
776 let result = listen(listener, session).await;
778
779 handle.set_running(false);
781
782 if let Err(e) = result {
783 error!("SSH Agent listener failed: {:?}", e);
784 return Err(AgentError::IO(io::Error::other(format!(
785 "SSH Agent listener failed: {}",
786 e
787 ))));
788 }
789
790 warn!("Agent listener loop exited unexpectedly without error.");
791 Ok(())
792}
793
794#[cfg(unix)]
809pub async fn start_agent_listener(socket_path_str: String) -> Result<(), AgentError> {
810 use std::path::PathBuf;
811 let handle = Arc::new(AgentHandle::new(PathBuf::from(&socket_path_str)));
812 start_agent_listener_with_handle(handle).await
813}