Skip to main content

auths_core/api/
runtime.rs

1//! Application-level runtime API for managing the identity agent and keys.
2//!
3//! Provides functions to interact with core components: secure key storage (`KeyStorage`),
4//! cryptographic operations, the in-memory agent (`AgentCore`), and the agent listener.
5//! Uses `AgentHandle` for lifecycle management of agent instances.
6//! Also includes functions for interacting with the platform's SSH agent (on macOS).
7
8use 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, PrefilledPassphraseProvider};
17use crate::storage::keychain::{KeyAlias, KeyRole, KeyStorage};
18use log::{debug, error, info, warn};
19#[cfg(target_os = "macos")]
20use p256::pkcs8::DecodePrivateKey;
21#[cfg(target_os = "macos")]
22use pkcs8::PrivateKeyInfo;
23#[cfg(target_os = "macos")]
24use pkcs8::der::Decode;
25#[cfg(target_os = "macos")]
26use pkcs8::der::asn1::OctetString;
27use serde::Serialize;
28#[cfg(unix)]
29use ssh_agent_lib;
30#[cfg(unix)]
31use ssh_agent_lib::agent::listen;
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 std::io::Write;
47
48#[cfg(target_os = "macos")]
49use {
50    std::fs::{self, Permissions},
51    std::os::unix::fs::PermissionsExt,
52    tempfile::Builder as TempFileBuilder,
53};
54
55#[cfg(target_os = "macos")]
56#[derive(Debug)]
57enum SshRegError {
58    Agent(crate::ports::ssh_agent::SshAgentError),
59    Io(std::io::Error),
60    Conversion(String),
61    BadSeedLength(usize),
62}
63
64#[cfg(target_os = "macos")]
65impl std::fmt::Display for SshRegError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Self::Agent(e) => write!(f, "ssh agent error: {e}"),
69            Self::Io(e) => write!(f, "I/O error: {e}"),
70            Self::Conversion(s) => write!(f, "key conversion failed: {s}"),
71            Self::BadSeedLength(n) => {
72                write!(f, "invalid PKCS#8 seed length: expected 32 bytes, got {n}")
73            }
74        }
75    }
76}
77
78/// Compute the SSH fingerprint for a public key of the given curve.
79#[cfg(target_os = "macos")]
80fn compute_ssh_fingerprint(pubkey_bytes: &[u8], curve: auths_crypto::CurveType) -> String {
81    let key_data = match curve {
82        auths_crypto::CurveType::Ed25519 => SshEd25519PublicKey::try_from(pubkey_bytes)
83            .map(ssh_key::public::KeyData::Ed25519)
84            .ok(),
85        auths_crypto::CurveType::P256 => {
86            ssh_key::public::EcdsaPublicKey::from_sec1_bytes(pubkey_bytes)
87                .ok()
88                .map(ssh_key::public::KeyData::Ecdsa)
89        }
90    };
91    match key_data {
92        Some(kd) => SshPublicKey::new(kd, "")
93            .fingerprint(Default::default())
94            .to_string(),
95        None => {
96            warn!("Could not build public key for fingerprint computation");
97            "unknown_fingerprint".to_string()
98        }
99    }
100}
101
102/// Parse a PKCS#8 key blob into an `ssh_key::private::KeypairData` variant matching the curve.
103#[cfg(target_os = "macos")]
104fn build_ssh_keypair_data(
105    pkcs8_bytes: &[u8],
106    curve: auths_crypto::CurveType,
107) -> Result<KeypairData, SshRegError> {
108    match curve {
109        auths_crypto::CurveType::Ed25519 => {
110            let private_key_info = PrivateKeyInfo::from_der(pkcs8_bytes)
111                .map_err(|e| SshRegError::Conversion(e.to_string()))?;
112            let seed_octet_string = OctetString::from_der(private_key_info.private_key)
113                .map_err(|e| SshRegError::Conversion(e.to_string()))?;
114            let seed_bytes = seed_octet_string.as_bytes();
115            if seed_bytes.len() != 32 {
116                return Err(SshRegError::BadSeedLength(seed_bytes.len()));
117            }
118            #[allow(clippy::expect_used)]
119            // INVARIANT: length validated by the 32-byte check above.
120            let seed_array: [u8; 32] = seed_bytes.try_into().expect("Length checked");
121            Ok(KeypairData::Ed25519(SshEdKeypair::from_seed(&seed_array)))
122        }
123        auths_crypto::CurveType::P256 => {
124            use p256::elliptic_curve::sec1::ToEncodedPoint;
125            use ssh_key::private::{EcdsaKeypair, EcdsaPrivateKey};
126
127            let secret = p256::SecretKey::from_pkcs8_der(pkcs8_bytes)
128                .map_err(|e| SshRegError::Conversion(format!("P-256 PKCS#8 parse failed: {e}")))?;
129            let public = secret.public_key();
130            Ok(KeypairData::Ecdsa(EcdsaKeypair::NistP256 {
131                public: public.to_encoded_point(false),
132                private: EcdsaPrivateKey::from(secret),
133            }))
134        }
135    }
136}
137
138// --- Public Structs ---
139
140/// Represents the result of trying to load a single key into the agent core.
141#[derive(Serialize, Debug, Clone)]
142pub struct KeyLoadStatus {
143    /// Key alias.
144    pub alias: KeyAlias,
145    /// Whether the key was successfully loaded.
146    pub loaded: bool,
147    /// Load error message, if any.
148    pub error: Option<String>,
149}
150
151/// Represents the outcome of attempting to register a key with the system SSH agent.
152#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
153pub enum RegistrationOutcome {
154    /// Key was successfully added to the agent.
155    Added,
156    /// Key already exists in the agent.
157    AlreadyExists,
158    /// The SSH agent process was not found.
159    AgentNotFound,
160    /// The agent command failed.
161    CommandFailed,
162    /// The key type is not supported by this agent.
163    UnsupportedKeyType,
164    /// Key format conversion failed.
165    ConversionFailed,
166    /// An I/O error occurred.
167    IoError,
168    /// An unexpected internal error occurred.
169    InternalError,
170}
171
172/// Represents the status of registering a single key with the system SSH agent.
173#[derive(Serialize, Debug, Clone)]
174pub struct KeyRegistrationStatus {
175    /// Key fingerprint.
176    pub fingerprint: String,
177    /// Registration outcome.
178    pub status: RegistrationOutcome,
179    /// Additional message, if any.
180    pub message: Option<String>,
181}
182
183// --- Public API Functions ---
184
185/// Clears all unlocked keys from the specified agent handle.
186///
187/// This effectively locks the agent until keys are reloaded.
188///
189/// # Arguments
190/// * `handle` - The agent handle to clear keys from
191///
192/// # Example
193/// ```rust,ignore
194/// use auths_core::AgentHandle;
195/// use auths_core::api::clear_agent_keys_with_handle;
196///
197/// let handle = AgentHandle::new(socket_path);
198/// clear_agent_keys_with_handle(&handle)?;
199/// ```
200pub fn clear_agent_keys_with_handle(handle: &AgentHandle) -> Result<(), AgentError> {
201    info!("Clearing all keys from agent handle.");
202    let mut agent_guard = handle.lock()?;
203    agent_guard.clear_keys();
204    debug!("Agent keys cleared.");
205    Ok(())
206}
207
208/// Loads specific keys (by alias) from secure storage into the specified agent handle.
209///
210/// Requires the correct passphrase for each key, obtained via the `passphrase_provider`.
211/// Replaces any keys currently loaded in the agent. Stores decrypted PKCS#8 bytes securely
212/// in memory using `zeroize`.
213///
214/// # Arguments
215/// * `handle` - The agent handle to load keys into
216/// * `aliases`: A list of key aliases to load from secure storage.
217/// * `passphrase_provider`: A component responsible for securely obtaining passphrases.
218/// * `keychain`: The key storage backend to load keys from.
219///
220/// # Returns
221/// A `Result` containing a list of `KeyLoadStatus` structs, indicating the outcome
222/// for each requested alias, or an `AgentError` if a fatal error occurs.
223pub fn load_keys_into_agent_with_handle(
224    handle: &AgentHandle,
225    aliases: Vec<String>,
226    passphrase_provider: &dyn PassphraseProvider,
227    keychain: &(dyn KeyStorage + Send + Sync),
228) -> Result<Vec<KeyLoadStatus>, AgentError> {
229    info!(
230        "Attempting to load keys into agent handle for aliases: {:?}",
231        aliases
232    );
233    if aliases.is_empty() {
234        warn!("load_keys_into_agent_with_handle called with empty alias list. Clearing agent.");
235        clear_agent_keys_with_handle(handle)?;
236        return Ok(vec![]);
237    }
238
239    let mut load_statuses = Vec::new();
240    let mut temp_unlocked_core = AgentCore::default();
241
242    for alias in aliases {
243        debug!("Processing alias for agent load: {}", alias);
244        let key_alias = KeyAlias::new_unchecked(&alias);
245        let mut status = KeyLoadStatus {
246            alias: key_alias.clone(),
247            loaded: false,
248            error: None,
249        };
250
251        let load_result = || -> Result<Zeroizing<Vec<u8>>, AgentError> {
252            if keychain.is_hardware_backend() {
253                return Err(AgentError::HardwareKeyNotExportable {
254                    operation: "agent key load".to_string(),
255                });
256            }
257            let (_controller_did, _role, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
258            let prompt = format!(
259                "Enter passphrase to unlock key '{}' for agent session:",
260                key_alias
261            );
262            let passphrase = passphrase_provider.get_passphrase(&prompt)?;
263            let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, &passphrase)?;
264            let _ = extract_seed_from_key_bytes(&pkcs8_bytes).map_err(|e| {
265                AgentError::KeyDeserializationError(format!(
266                    "Failed to parse key for alias '{}' after decryption: {}",
267                    key_alias, e
268                ))
269            })?;
270            Ok(pkcs8_bytes)
271        }();
272
273        match load_result {
274            Ok(pkcs8_bytes) => {
275                info!("Successfully unlocked key for alias '{}'", key_alias);
276                match temp_unlocked_core.register_key(pkcs8_bytes) {
277                    Ok(()) => status.loaded = true,
278                    Err(e) => {
279                        error!(
280                            "Failed to register key '{}' in agent core state after successful unlock/parse: {}",
281                            key_alias, e
282                        );
283                        status.error = Some(format!(
284                            "Internal error: Failed to register key in agent core state: {}",
285                            e
286                        ));
287                    }
288                }
289            }
290            Err(e) => {
291                error!(
292                    "Failed to load/decrypt key for alias '{}': {}",
293                    key_alias, e
294                );
295                match e {
296                    AgentError::IncorrectPassphrase => {
297                        status.error = Some("Incorrect passphrase".to_string())
298                    }
299                    AgentError::KeyNotFound => status.error = Some("Key not found".to_string()),
300                    AgentError::UserInputCancelled => {
301                        status.error = Some("Operation cancelled by user".to_string())
302                    }
303                    AgentError::KeyDeserializationError(_) => {
304                        status.error = Some(format!("Failed to parse key after decryption: {}", e))
305                    }
306                    _ => status.error = Some(e.to_string()),
307                }
308            }
309        }
310        load_statuses.push(status);
311    }
312
313    // Atomically update the agent state
314    let mut agent_guard = handle.lock()?;
315    info!(
316        "Replacing agent core with {} unlocked keys ({} aliases attempted).",
317        temp_unlocked_core.key_count(),
318        load_statuses.len()
319    );
320    *agent_guard = temp_unlocked_core;
321
322    Ok(load_statuses)
323}
324
325/// Rotates the keypair for a given alias *in the secure storage only*.
326///
327/// This generates a new Ed25519 keypair, encrypts it with the `new_passphrase`,
328/// and overwrites the existing entry for `alias` in the platform's keychain or
329/// secure storage. The key remains associated with the *same Controller DID*
330/// as the original key.
331///
332/// **Warning:** This function does *not* update any corresponding identity
333/// representation in a Git repository (e.g., changing the Controller DID stored
334/// in an identity commit or creating a KERI rotation event). Using this function
335/// alone may lead to inconsistencies if the identity representation relies on the
336/// public key associated with the Controller DID. It also does not automatically
337/// update the key loaded in the running agent; `load_keys_into_agent` or restarting
338/// the agent may be required.
339///
340/// # Arguments
341/// * `alias`: The alias of the key entry in secure storage to rotate.
342/// * `new_passphrase`: The passphrase to encrypt the *new* private key with.
343///
344/// # Returns
345/// `Ok(())` on success, or an `AgentError` if the alias is not found, key generation
346/// fails, encryption fails, or storage fails.
347pub fn rotate_key(
348    alias: &str,
349    new_passphrase: &str,
350    keychain: &(dyn KeyStorage + Send + Sync),
351) -> Result<(), AgentError> {
352    info!(
353        "[API] Attempting secure storage key rotation for local alias: {}",
354        alias
355    );
356    if alias.trim().is_empty() {
357        return Err(AgentError::InvalidInput(
358            "Alias cannot be empty".to_string(),
359        ));
360    }
361    if new_passphrase.is_empty() {
362        return Err(AgentError::InvalidInput(
363            "New passphrase cannot be empty".to_string(),
364        ));
365    }
366
367    // 1. Verify the alias exists and retrieve its associated Controller DID
368    let key_alias = KeyAlias::new_unchecked(alias);
369    let existing_did = keychain.get_identity_for_alias(&key_alias)?;
370    info!(
371        "Found existing key for alias '{}', associated with Controller DID '{}'. Proceeding with rotation.",
372        alias, existing_did
373    );
374
375    // 2. Generate new keypair via CryptoProvider
376    let (seed, pubkey) = provider_bridge::generate_ed25519_keypair_sync()
377        .map_err(|e| AgentError::CryptoError(format!("Failed to generate new keypair: {}", e)))?;
378    // Build PKCS#8 v2 DER for storage compatibility
379    let new_pkcs8_bytes = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey);
380    debug!("Generated new keypair via CryptoProvider.");
381
382    // 3. Encrypt the new keypair with the new passphrase
383    let encrypted_new_key = encrypt_keypair(&new_pkcs8_bytes, new_passphrase)?;
384    debug!("Encrypted new keypair with provided passphrase.");
385
386    // 4. Overwrite the existing entry in secure storage with the new encrypted key,
387    //    keeping the original Controller DID association.
388    keychain.store_key(
389        &key_alias,
390        &existing_did,
391        KeyRole::Primary,
392        &encrypted_new_key,
393    )?;
394    info!(
395        "Successfully overwrote secure storage for alias '{}' with new encrypted key.",
396        alias
397    );
398
399    warn!(
400        "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.",
401        alias
402    );
403    Ok(())
404}
405
406/// Signs a message using a key currently loaded in the specified agent handle.
407///
408/// This retrieves the decrypted key material from the agent handle based on the
409/// provided public key bytes and performs the signing operation. It does *not*
410/// require a passphrase as the key is assumed to be already unlocked.
411///
412/// # Arguments
413/// * `handle` - The agent handle containing the loaded keys
414/// * `pubkey`: The public key bytes of the key to use for signing.
415/// * `data`: The data bytes to sign.
416///
417/// # Returns
418/// The raw signature bytes, or an `AgentError` if the key is not found in the
419/// agent core or if the signing operation fails internally.
420pub fn agent_sign_with_handle(
421    handle: &AgentHandle,
422    pubkey: &[u8],
423    data: &[u8],
424) -> Result<Vec<u8>, AgentError> {
425    debug!(
426        "Agent sign request for pubkey starting with: {:x?}...",
427        &pubkey[..core::cmp::min(pubkey.len(), 8)]
428    );
429
430    // Use the handle's sign method which includes lock check
431    handle.sign(pubkey, data)
432}
433
434/// Exports the decrypted private key in OpenSSH PEM format.
435///
436/// Retrieves the encrypted key from secure storage, decrypts it using the
437/// provided passphrase, and formats it as a standard OpenSSH PEM private key string.
438///
439/// # Arguments
440/// * `alias`: The alias of the key in secure storage.
441/// * `passphrase`: The passphrase to decrypt the key.
442///
443/// # Returns
444/// A `Zeroizing<String>` containing the PEM data on success, or an `AgentError`.
445pub fn export_key_openssh_pem(
446    alias: &str,
447    passphrase: &str,
448    keychain: &(dyn KeyStorage + Send + Sync),
449) -> Result<Zeroizing<String>, AgentError> {
450    info!("Exporting PEM for local alias: {}", alias);
451    if alias.trim().is_empty() {
452        return Err(AgentError::InvalidInput(
453            "Alias cannot be empty".to_string(),
454        ));
455    }
456    if keychain.is_hardware_backend() {
457        return Err(AgentError::HardwareKeyNotExportable {
458            operation: "OpenSSH private key export".to_string(),
459        });
460    }
461    // 1. Load encrypted key data
462    let key_alias = KeyAlias::new_unchecked(alias);
463    let (_controller_did, _role, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
464
465    // 2. Decrypt key data
466    let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
467
468    // 3. Parse the key material (auto-detects curve)
469    let parsed = auths_crypto::parse_key_material(&pkcs8_bytes[..]).map_err(|e| {
470        AgentError::KeyDeserializationError(format!(
471            "Failed to parse key material for alias '{}': {}",
472            alias, e
473        ))
474    })?;
475
476    let keypair_data = build_openssh_keypair_data(&parsed).map_err(|e| {
477        AgentError::CryptoError(format!(
478            "Failed to build SSH keypair for alias '{}': {}",
479            alias, e
480        ))
481    })?;
482
483    let ssh_private_key = SshPrivateKey::new(keypair_data, "").map_err(|e| {
484        AgentError::CryptoError(format!(
485            "Failed to create ssh_key::PrivateKey for alias '{}': {}",
486            alias, e
487        ))
488    })?;
489
490    let pem = ssh_private_key.to_openssh(LineEnding::LF).map_err(|e| {
491        AgentError::CryptoError(format!(
492            "Failed to encode OpenSSH PEM for alias '{}': {}",
493            alias, e
494        ))
495    })?;
496
497    debug!("Successfully generated PEM for alias '{}'", alias);
498    Ok(pem)
499}
500
501/// Build `ssh_key::private::KeypairData` from a parsed key material, dispatching on curve.
502fn build_openssh_keypair_data(parsed: &auths_crypto::ParsedKey) -> Result<KeypairData, String> {
503    match parsed.seed.curve() {
504        auths_crypto::CurveType::Ed25519 => Ok(KeypairData::Ed25519(SshEdKeypair::from_seed(
505            parsed.seed.as_bytes(),
506        ))),
507        auths_crypto::CurveType::P256 => {
508            use p256::elliptic_curve::sec1::ToEncodedPoint;
509            use ssh_key::private::{EcdsaKeypair, EcdsaPrivateKey};
510
511            let secret = p256::SecretKey::from_slice(parsed.seed.as_bytes())
512                .map_err(|e| format!("P-256 secret key parse: {e}"))?;
513            let public = secret.public_key();
514            Ok(KeypairData::Ecdsa(EcdsaKeypair::NistP256 {
515                public: public.to_encoded_point(false),
516                private: EcdsaPrivateKey::from(secret),
517            }))
518        }
519    }
520}
521
522/// Exports the public key in OpenSSH `.pub` format.
523///
524/// Retrieves the encrypted key from secure storage, decrypts it using the
525/// provided passphrase, derives the public key, and formats it as a standard
526/// OpenSSH `.pub` line (including the alias as a comment).
527///
528/// # Arguments
529/// * `alias`: The alias of the key in secure storage.
530/// * `passphrase`: The passphrase to decrypt the key.
531///
532/// # Returns
533/// A `String` containing the public key line on success, or an `AgentError`.
534pub fn export_key_openssh_pub(
535    alias: &str,
536    passphrase: &str,
537    keychain: &(dyn KeyStorage + Send + Sync),
538) -> Result<String, AgentError> {
539    info!("Exporting OpenSSH public key for local alias: {}", alias);
540    if alias.trim().is_empty() {
541        return Err(AgentError::InvalidInput(
542            "Alias cannot be empty".to_string(),
543        ));
544    }
545    // 1. Obtain public key bytes (hardware-aware; SE returns pubkey without decryption)
546    let key_alias = KeyAlias::new_unchecked(alias);
547    let passphrase_provider = PrefilledPassphraseProvider::new(passphrase);
548    let (pubkey_bytes, curve) = crate::storage::keychain::extract_public_key_bytes(
549        keychain,
550        &key_alias,
551        &passphrase_provider,
552    )?;
553    let key_data = match curve {
554        auths_crypto::CurveType::Ed25519 => {
555            let pk = SshEd25519PublicKey::try_from(pubkey_bytes.as_slice()).map_err(|e| {
556                AgentError::CryptoError(format!(
557                    "Failed to create Ed25519PublicKey from bytes: {}",
558                    e
559                ))
560            })?;
561            ssh_key::public::KeyData::Ed25519(pk)
562        }
563        auths_crypto::CurveType::P256 => {
564            let pk = ssh_key::public::EcdsaPublicKey::from_sec1_bytes(pubkey_bytes.as_slice())
565                .map_err(|e| {
566                    AgentError::CryptoError(format!(
567                        "Failed to create EcdsaPublicKey from bytes: {}",
568                        e
569                    ))
570                })?;
571            ssh_key::public::KeyData::Ecdsa(pk)
572        }
573    };
574
575    // 5. Create the ssh-key PublicKey object (comment is optional here)
576    let ssh_pub_key = SshPublicKey::new(key_data, ""); // Use empty comment for base formatting
577
578    // 6. Format the base public key string (type and key material)
579    let pubkey_base = ssh_pub_key.to_openssh().map_err(|e| {
580        // Use CryptoError for formatting failure
581        AgentError::CryptoError(format!("Failed to format OpenSSH pubkey base: {}", e))
582    })?;
583
584    // 7. Manually append the alias as the comment part of the .pub line
585    let formatted_pubkey = format!("{} {}", pubkey_base, alias);
586
587    debug!(
588        "Successfully generated OpenSSH public key string for alias '{}'",
589        alias
590    );
591    Ok(formatted_pubkey)
592}
593
594/// Returns the number of keys currently loaded in the specified agent handle.
595///
596/// # Arguments
597/// * `handle` - The agent handle to query
598///
599/// # Returns
600/// The number of keys currently loaded.
601pub fn get_agent_key_count_with_handle(handle: &AgentHandle) -> Result<usize, AgentError> {
602    handle.key_count()
603}
604
605/// Attempts to register all keys currently loaded in the specified agent handle
606/// with the system's running SSH agent via the injected `SshAgentPort`.
607///
608/// This iterates through the unlocked keys in the agent core, converts each to
609/// OpenSSH PEM format, writes it to a temporary file, and delegates to the
610/// provided `ssh_agent` port for the actual registration.
611///
612/// Args:
613/// * `handle` - The agent handle containing the keys to register.
614/// * `ssh_agent_socket` - Optional path to the SSH agent socket (for diagnostics).
615/// * `ssh_agent` - Port implementation that registers keys with the system agent.
616///
617/// Usage:
618/// ```ignore
619/// use auths_core::api::runtime::register_keys_with_macos_agent_with_handle;
620///
621/// let statuses = register_keys_with_macos_agent_with_handle(&handle, None, &adapter)?;
622/// ```
623#[cfg(target_os = "macos")]
624#[allow(clippy::disallowed_methods)]
625// INVARIANT: macOS SSH agent registration — temp file creation and permissions are inherently I/O
626#[allow(clippy::disallowed_types)]
627pub fn register_keys_with_macos_agent_with_handle(
628    handle: &AgentHandle,
629    ssh_agent_socket: Option<&std::path::Path>,
630    ssh_agent: &dyn crate::ports::ssh_agent::SshAgentPort,
631) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
632    info!("Attempting to register keys from agent handle with system ssh-agent...");
633    if ssh_agent_socket.is_none() {
634        warn!("SSH_AUTH_SOCK not configured. System ssh-agent may not be running or configured.");
635    }
636
637    let keys_to_register: Vec<(Vec<u8>, auths_crypto::CurveType, Zeroizing<Vec<u8>>)> = {
638        let agent_guard = handle.lock()?;
639        agent_guard
640            .keys
641            .iter()
642            .filter_map(|(pubkey, stored)| {
643                let typed_seed = match stored.curve {
644                    auths_crypto::CurveType::Ed25519 => {
645                        auths_crypto::TypedSeed::Ed25519(*stored.seed.as_bytes())
646                    }
647                    auths_crypto::CurveType::P256 => {
648                        auths_crypto::TypedSeed::P256(*stored.seed.as_bytes())
649                    }
650                };
651                let signer = auths_crypto::TypedSignerKey::from_seed(typed_seed).ok()?;
652                let pkcs8 = signer.to_pkcs8().ok()?;
653                Some((
654                    pubkey.clone(),
655                    stored.curve,
656                    Zeroizing::new(pkcs8.as_ref().to_vec()),
657                ))
658            })
659            .collect()
660    };
661
662    register_keys_with_macos_agent_internal(keys_to_register, ssh_agent)
663}
664
665/// Stub function for non-macOS platforms.
666#[cfg(not(target_os = "macos"))]
667pub fn register_keys_with_macos_agent_with_handle(
668    _handle: &AgentHandle,
669    _ssh_agent_socket: Option<&std::path::Path>,
670    _ssh_agent: &dyn crate::ports::ssh_agent::SshAgentPort,
671) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
672    info!("Not on macOS, skipping system ssh-agent registration.");
673    Ok(vec![])
674}
675
676/// Internal helper that performs the actual system SSH agent registration.
677///
678/// Converts each PKCS#8 key to OpenSSH PEM, writes to a temp file, and
679/// delegates to the injected `SshAgentPort` for the actual `ssh-add` call.
680#[cfg(target_os = "macos")]
681#[allow(clippy::too_many_lines)]
682fn register_keys_with_macos_agent_internal(
683    keys_to_register: Vec<(Vec<u8>, auths_crypto::CurveType, Zeroizing<Vec<u8>>)>,
684    ssh_agent: &dyn crate::ports::ssh_agent::SshAgentPort,
685) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
686    use crate::ports::ssh_agent::SshAgentError;
687
688    if keys_to_register.is_empty() {
689        info!("No keys to register with system agent.");
690        return Ok(vec![]);
691    }
692    info!(
693        "Found {} keys to attempt registration with system agent.",
694        keys_to_register.len()
695    );
696
697    let mut results = Vec::with_capacity(keys_to_register.len());
698
699    for (pubkey_bytes, curve, pkcs8_bytes_zeroizing) in keys_to_register.into_iter() {
700        let fingerprint_str = compute_ssh_fingerprint(&pubkey_bytes, curve);
701
702        let mut status = KeyRegistrationStatus {
703            fingerprint: fingerprint_str.clone(),
704            status: RegistrationOutcome::InternalError,
705            message: None,
706        };
707
708        let result: Result<(), SshRegError> = (|| {
709            let pkcs8_bytes = pkcs8_bytes_zeroizing.as_ref();
710            let keypair_data = build_ssh_keypair_data(pkcs8_bytes, curve)?;
711            let ssh_private_key = SshPrivateKey::new(keypair_data, "")
712                .map_err(|e| SshRegError::Conversion(e.to_string()))?;
713            let pem_zeroizing = ssh_private_key
714                .to_openssh(LineEnding::LF)
715                .map_err(|e| SshRegError::Conversion(e.to_string()))?;
716            let pem_string = pem_zeroizing.to_string();
717
718            let mut temp_file_guard = TempFileBuilder::new()
719                .prefix("auths-key-")
720                .suffix(".pem")
721                .rand_bytes(5)
722                .tempfile()
723                .map_err(SshRegError::Io)?;
724            if let Err(e) =
725                fs::set_permissions(temp_file_guard.path(), Permissions::from_mode(0o600))
726            {
727                warn!(
728                    "Failed to set 600 permissions on temp file {:?}: {}. Continuing...",
729                    temp_file_guard.path(),
730                    e
731                );
732            }
733            temp_file_guard
734                .write_all(pem_string.as_bytes())
735                .map_err(SshRegError::Io)?;
736            temp_file_guard.flush().map_err(SshRegError::Io)?;
737            let temp_file_path = temp_file_guard.path().to_path_buf();
738
739            debug!(
740                "Attempting ssh-add for temporary key file: {:?}",
741                temp_file_path
742            );
743            ssh_agent
744                .register_key(&temp_file_path)
745                .map_err(SshRegError::Agent)?;
746            debug!("ssh-add finished for {:?}", temp_file_path);
747            Ok(())
748        })();
749
750        match result {
751            Ok(()) => {
752                info!(
753                    "ssh-add successful for {}: Identity added.",
754                    fingerprint_str
755                );
756                status.status = RegistrationOutcome::Added;
757                status.message = Some("Identity added via ssh-agent port".to_string());
758            }
759            Err(e) => {
760                match &e {
761                    SshRegError::Agent(SshAgentError::NotAvailable(_)) => {
762                        status.status = RegistrationOutcome::AgentNotFound;
763                    }
764                    SshRegError::Agent(SshAgentError::CommandFailed(_)) => {
765                        status.status = RegistrationOutcome::CommandFailed;
766                    }
767                    SshRegError::Agent(SshAgentError::IoError(_)) | SshRegError::Io(_) => {
768                        status.status = RegistrationOutcome::IoError;
769                    }
770                    SshRegError::Conversion(_) | SshRegError::BadSeedLength(_) => {
771                        status.status = RegistrationOutcome::ConversionFailed;
772                    }
773                }
774                error!(
775                    "Error during registration process for {}: {:?}",
776                    fingerprint_str, e
777                );
778                status.message = Some(format!("Registration error: {}", e));
779            }
780        }
781        results.push(status);
782    }
783
784    info!(
785        "Finished attempting system agent registration for {} keys.",
786        results.len()
787    );
788    Ok(results)
789}
790
791/// Starts the SSH agent listener using the provided `AgentHandle`.
792///
793/// Binds to the socket path from the handle, cleans up any old socket file if present,
794/// and enters an asynchronous loop (`ssh_agent_lib::listen`) to accept and handle
795/// incoming agent connections using `AgentSession`.
796///
797/// Requires a `tokio` runtime context. Runs indefinitely on success.
798///
799/// # Arguments
800/// * `handle`: The agent handle containing the socket path and agent core.
801///
802/// # Returns
803/// - `Ok(())` if the listener starts successfully (runs indefinitely).
804/// - `Err(AgentError)` if binding/setup fails or the listener loop exits with an error.
805#[cfg(unix)]
806#[allow(clippy::disallowed_methods)] // INVARIANT: Unix socket lifecycle — socket dir creation and cleanup is inherently I/O
807pub async fn start_agent_listener_with_handle(handle: Arc<AgentHandle>) -> Result<(), AgentError> {
808    let socket_path = handle.socket_path();
809    info!("Attempting to start agent listener at {:?}", socket_path);
810
811    // --- Ensure parent directory exists ---
812    if let Some(parent) = socket_path.parent()
813        && !parent.exists()
814    {
815        debug!("Creating parent directory for socket: {:?}", parent);
816        if let Err(e) = std::fs::create_dir_all(parent) {
817            error!("Failed to create parent directory {:?}: {}", parent, e);
818            return Err(AgentError::IO(e));
819        }
820    }
821
822    // --- Clean up existing socket file (if any) ---
823    match std::fs::remove_file(socket_path) {
824        Ok(()) => info!("Removed existing socket file at {:?}", socket_path),
825        Err(e) if e.kind() == io::ErrorKind::NotFound => {
826            debug!(
827                "No existing socket file found at {:?}, proceeding.",
828                socket_path
829            );
830        }
831        Err(e) => {
832            warn!(
833                "Failed to remove existing socket file at {:?}: {}. Binding might fail.",
834                socket_path, e
835            );
836        }
837    }
838
839    // --- Bind the listener ---
840    let listener = UnixListener::bind(socket_path).map_err(|e| {
841        error!("Failed to bind listener socket at {:?}: {}", socket_path, e);
842        AgentError::IO(e)
843    })?;
844
845    // --- Listener started successfully ---
846    let actual_path = socket_path
847        .canonicalize()
848        .unwrap_or_else(|_| socket_path.to_path_buf());
849    info!(
850        "🚀 Agent listener started successfully at {:?}",
851        actual_path
852    );
853    info!("   Set SSH_AUTH_SOCK={:?} to use this agent.", actual_path);
854
855    // Mark agent as running
856    handle.set_running(true);
857
858    // --- Create the agent session handler using the provided handle ---
859    let session = AgentSession::new(handle.clone());
860
861    // --- Start the main listener loop from ssh_agent_lib ---
862    let result = listen(listener, session).await;
863
864    // Mark agent as no longer running
865    handle.set_running(false);
866
867    if let Err(e) = result {
868        error!("SSH Agent listener failed: {:?}", e);
869        return Err(AgentError::IO(io::Error::other(format!(
870            "SSH Agent listener failed: {}",
871            e
872        ))));
873    }
874
875    warn!("Agent listener loop exited unexpectedly without error.");
876    Ok(())
877}
878
879/// Starts the SSH agent listener on the specified Unix domain socket path.
880///
881/// This is a convenience function that creates an `AgentHandle` internally.
882/// For more control over the agent lifecycle, use `start_agent_listener_with_handle`
883/// with your own `AgentHandle`.
884///
885/// Requires a `tokio` runtime context. Runs indefinitely on success.
886///
887/// # Arguments
888/// * `socket_path_str`: The filesystem path for the Unix domain socket.
889///
890/// # Returns
891/// - `Ok(())` if the listener starts successfully (runs indefinitely).
892/// - `Err(AgentError)` if binding/setup fails or the listener loop exits with an error.
893#[cfg(unix)]
894pub async fn start_agent_listener(socket_path_str: String) -> Result<(), AgentError> {
895    use std::path::PathBuf;
896    let handle = Arc::new(AgentHandle::new(PathBuf::from(&socket_path_str)));
897    start_agent_listener_with_handle(handle).await
898}