Skip to main content

auths_core/api/
ffi.rs

1//! FFI bindings to expose core functionality to other languages (Swift, Kotlin, C, etc.).
2//!
3//! Provides functions for key management (import, rotate, export), cryptographic
4//! operations, and agent-based signing.
5//!
6//! # Safety
7//! Functions returning pointers (`*mut c_char`, `*mut u8`) allocate memory
8//! using `libc::malloc`. The caller is responsible for freeing this memory
9//! using the corresponding `ffi_free_*` function (`ffi_free_str`, `ffi_free_bytes`).
10//! Input C string pointers (`*const c_char`) must be valid, null-terminated UTF-8 strings.
11//! Input byte pointers (`*const u8`/`*const c_uchar`) must be valid for the specified length.
12//! Output length pointers (`*mut usize`) must be valid pointers.
13//! Operations involving raw pointers or calling C functions are wrapped in `unsafe` blocks.
14
15use crate::agent::AgentHandle;
16use crate::api::runtime::{
17    agent_sign_with_handle, export_key_openssh_pem, export_key_openssh_pub, rotate_key,
18};
19use crate::config::EnvironmentConfig;
20use crate::config::{current_algorithm, set_encryption_algorithm};
21use crate::crypto::EncryptionAlgorithm;
22use crate::crypto::encryption::{decrypt_bytes, encrypt_bytes};
23use crate::crypto::signer::extract_seed_from_key_bytes;
24use crate::crypto::signer::{decrypt_keypair, encrypt_keypair};
25use crate::error::AgentError;
26use crate::storage::keychain::{
27    IdentityDID, KeyAlias, KeyRole, KeyStorage, get_platform_keychain_with_config,
28};
29use log::{debug, error, info, warn};
30use parking_lot::RwLock;
31use std::ffi::{CStr, CString};
32use std::os::raw::{c_char, c_int, c_uchar};
33use std::panic;
34use std::path::PathBuf;
35use std::ptr;
36use std::slice;
37use std::sync::{Arc, LazyLock};
38
39// --- FFI Error Codes ---
40
41/// Successful operation
42pub const FFI_OK: c_int = 0;
43/// Invalid UTF-8 in C string input
44pub const FFI_ERR_INVALID_UTF8: c_int = -1;
45/// Agent not initialized (call ffi_init_agent first)
46pub const FFI_ERR_AGENT_NOT_INITIALIZED: c_int = -2;
47/// Internal panic occurred
48pub const FFI_ERR_PANIC: c_int = -127;
49
50// --- FFI Agent Handle ---
51
52/// Global FFI agent handle.
53///
54/// This static holds the `AgentHandle` used by FFI functions. It must be initialized
55/// by calling `ffi_init_agent()` before using functions like `ffi_agent_sign()`.
56static FFI_AGENT: LazyLock<RwLock<Option<Arc<AgentHandle>>>> = LazyLock::new(|| RwLock::new(None));
57
58/// Initializes the FFI agent with the specified socket path.
59///
60/// Must be called before using `ffi_agent_sign()` or other agent-related FFI functions.
61///
62/// # Safety
63/// - `socket_path` must be null or point to a valid C string.
64///
65/// # Returns
66/// - 0 on success
67/// - 1 if the socket path is invalid
68/// - FFI_ERR_PANIC (-127) if a panic occurred
69#[unsafe(no_mangle)]
70#[allow(clippy::disallowed_methods)] // INVARIANT: FFI boundary — home-dir fallback for default socket path
71pub unsafe extern "C" fn ffi_init_agent(socket_path: *const c_char) -> c_int {
72    let result = panic::catch_unwind(|| {
73        let path_str = match unsafe { c_str_to_str_safe(socket_path) } {
74            Ok(s) if !s.is_empty() => s,
75            Ok(_) => {
76                // Empty path - use default
77                let home = match dirs::home_dir() {
78                    Some(h) => h,
79                    None => {
80                        error!("FFI ffi_init_agent: Could not determine home directory");
81                        return 1;
82                    }
83                };
84                let default_path = home.join(".auths").join("agent.sock");
85                let handle = Arc::new(AgentHandle::new(default_path));
86                let mut guard = FFI_AGENT.write();
87                *guard = Some(handle);
88                info!("FFI agent initialized with default socket path");
89                return 0;
90            }
91            Err(code) => return code,
92        };
93
94        let socket = PathBuf::from(path_str);
95        let handle = Arc::new(AgentHandle::new(socket));
96
97        let mut guard = FFI_AGENT.write();
98        *guard = Some(handle);
99        info!("FFI agent initialized with socket path: {}", path_str);
100        0
101    });
102    result.unwrap_or_else(|_| {
103        error!("FFI ffi_init_agent: panic occurred");
104        FFI_ERR_PANIC
105    })
106}
107
108/// Shuts down the FFI agent, clearing all keys from memory.
109///
110/// After calling this, `ffi_agent_sign()` will return an error until
111/// `ffi_init_agent()` is called again.
112///
113/// # Safety
114/// This function is safe to call at any time.
115///
116/// # Returns
117/// - 0 on success
118/// - FFI_ERR_PANIC (-127) if a panic occurred
119#[unsafe(no_mangle)]
120pub unsafe extern "C" fn ffi_shutdown_agent() -> c_int {
121    let result = panic::catch_unwind(|| {
122        let mut guard = FFI_AGENT.write();
123        if let Some(handle) = guard.take() {
124            if let Err(e) = handle.shutdown() {
125                warn!("FFI ffi_shutdown_agent: Shutdown returned error: {}", e);
126            }
127            info!("FFI agent shut down");
128        } else {
129            debug!("FFI ffi_shutdown_agent: Agent was not initialized");
130        }
131        0
132    });
133    result.unwrap_or_else(|_| {
134        error!("FFI ffi_shutdown_agent: panic occurred");
135        FFI_ERR_PANIC
136    })
137}
138
139/// Gets a clone of the FFI agent handle.
140///
141/// Returns `None` if the agent has not been initialized.
142fn get_ffi_agent() -> Option<Arc<AgentHandle>> {
143    FFI_AGENT.read().clone()
144}
145
146// --- Helper Functions ---
147
148/// Safely converts a C string pointer to a Rust `&str`.
149/// Returns `Ok("")` if the pointer is null.
150/// Returns `Err(FFI_ERR_INVALID_UTF8)` if the C string is not valid UTF-8.
151///
152/// # Safety
153/// The caller must ensure `ptr` is either null or points to a valid,
154/// null-terminated C string with a lifetime that encompasses this function call.
155pub unsafe fn c_str_to_str_safe<'a>(ptr: *const c_char) -> Result<&'a str, c_int> {
156    if ptr.is_null() {
157        Ok("")
158    } else {
159        // Safety: Assumes ptr is valid C string per function contract.
160        unsafe {
161            CStr::from_ptr(ptr)
162                .to_str()
163                .map_err(|_| FFI_ERR_INVALID_UTF8)
164        }
165    }
166}
167
168/// Converts a C string pointer to a Rust `&str`.
169/// Returns an empty string if the pointer is null.
170/// Panics if the C string is not valid UTF-8.
171///
172/// # Safety
173/// The caller must ensure `ptr` is either null or points to a valid,
174/// null-terminated C string with a lifetime that encompasses this function call.
175/// The function itself needs to be marked `unsafe` because `CStr::from_ptr` is unsafe.
176///
177/// # Deprecated
178/// Use `c_str_to_str_safe` instead for panic-safe FFI code.
179#[deprecated(note = "Use c_str_to_str_safe for panic-safe FFI")]
180#[allow(clippy::expect_used)] // deprecated function — use c_str_to_str_safe instead
181pub unsafe fn c_str_to_str<'a>(ptr: *const c_char) -> &'a str {
182    if ptr.is_null() {
183        ""
184    } else {
185        // Safety: Assumes ptr is valid C string per function contract.
186        unsafe {
187            CStr::from_ptr(ptr)
188                .to_str()
189                .expect("FFI string inputs must be valid UTF-8")
190        }
191    }
192}
193
194/// Converts a Rust `Result<T, E: Display>` to a C-style integer error code.
195/// Logs the error on failure. Returns 0 on Ok, 1 on Err (general error).
196/// Consider more specific error codes in the future.
197///
198/// # Safety
199/// This function is marked unsafe for FFI compatibility but does not perform
200/// any unsafe operations itself.
201pub unsafe fn result_to_c_int<T, E: std::fmt::Display>(
202    result: Result<T, E>,
203    fn_name: &str,
204) -> c_int {
205    match result {
206        Ok(_) => 0,
207        Err(e) => {
208            error!("FFI call {} failed: {}", fn_name, e);
209            1 // General error code
210        }
211    }
212}
213
214/// Helper to allocate memory via malloc, copy Rust slice data into it,
215/// set the out_len pointer, and return the raw pointer.
216/// Returns null pointer on allocation failure.
217///
218/// # Safety
219/// - `out_len` must be a valid pointer to `usize`.
220/// - The caller must ensure the returned pointer (if not null) is eventually freed
221///   using `ffi_free_bytes`.
222/// - Operations involve raw pointers and calling `libc::malloc`, requiring `unsafe` block.
223pub unsafe fn malloc_and_copy_bytes(data: &[u8], out_len: *mut usize) -> *mut u8 {
224    // Safety: Operations require unsafe block.
225    unsafe {
226        if out_len.is_null() {
227            error!("malloc_and_copy_bytes failed: out_len pointer is null.");
228            return ptr::null_mut();
229        }
230        // Dereferencing out_len is unsafe
231        *out_len = data.len();
232        // Calling C function is unsafe
233        let ptr = libc::malloc(data.len()) as *mut u8;
234        if ptr.is_null() {
235            error!(
236                "malloc_and_copy_bytes failed: malloc returned null for size {}",
237                data.len()
238            );
239            *out_len = 0; // Reset len on failure
240            return ptr::null_mut();
241        }
242        // Pointer copy is unsafe
243        ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
244        ptr
245    }
246}
247
248/// Helper to convert a Rust String (or Zeroizing<String>) into a C string,
249/// allocating memory via `CString::into_raw`.
250/// Returns null pointer on allocation failure or if the string contains null bytes.
251///
252/// # Safety
253/// - The caller must ensure the returned pointer (if not null) is eventually freed
254///   using `ffi_free_str`.
255/// - Calls `CString::into_raw` which transfers ownership.
256fn malloc_and_copy_string(s: &str) -> *mut c_char {
257    match CString::new(s) {
258        Ok(c_string) => c_string.into_raw(), // Transfers ownership, C caller must free
259        Err(e) => {
260            error!(
261                "malloc_and_copy_string failed: CString creation error: {}",
262                e
263            );
264            ptr::null_mut()
265        }
266    }
267}
268
269// --- FFI Functions ---
270
271/// Checks if a key with the given alias exists in the secure storage.
272///
273/// # Safety
274/// - `alias` must be null or point to a valid C string.
275///
276/// # Returns
277/// - `true` if the key exists
278/// - `false` if key doesn't exist, invalid input, or internal error
279#[unsafe(no_mangle)]
280pub unsafe extern "C" fn ffi_key_exists(alias: *const c_char) -> bool {
281    let result = panic::catch_unwind(|| {
282        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
283            Ok(s) => s,
284            Err(_) => return false,
285        };
286        if alias_str.is_empty() {
287            return false;
288        }
289        // TODO: Refactor FFI to accept configuration context
290        let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
291            Ok(kc) => kc,
292            Err(e) => {
293                error!("FFI ffi_key_exists: Failed to get platform keychain: {}", e);
294                return false;
295            }
296        };
297        let alias = KeyAlias::new_unchecked(alias_str);
298        keychain.load_key(&alias).is_ok()
299    });
300    result.unwrap_or_else(|_| {
301        error!("FFI ffi_key_exists: panic occurred");
302        false
303    })
304}
305
306/// Imports a private key (provided as raw PKCS#8 bytes), encrypts it with the
307/// given passphrase, and stores it in the secure storage under the specified
308/// local alias, associated with the given controller DID.
309///
310/// # Safety
311/// - `alias`, `controller_did`, `passphrase` must be valid C strings.
312/// - `key_ptr` must point to valid PKCS#8 key data of `key_len` bytes for the duration of the call.
313/// - `key_len` must be the correct length for the data pointed to by `key_ptr`.
314///
315/// # Returns
316/// - 0 on success
317/// - 1 if arguments are invalid
318/// - 2 if key data is not valid PKCS#8
319/// - 4 if encryption fails
320/// - 5 if keychain initialization fails
321/// - FFI_ERR_INVALID_UTF8 (-1) if C strings contain invalid UTF-8
322/// - FFI_ERR_PANIC (-127) if a panic occurred
323#[unsafe(no_mangle)]
324pub unsafe extern "C" fn ffi_import_key(
325    alias: *const c_char,    // Local keychain alias
326    key_ptr: *const c_uchar, // Pointer to PKCS#8 bytes
327    key_len: usize,
328    controller_did: *const c_char, // Controller DID to associate
329    passphrase: *const c_char,     // Passphrase to encrypt WITH
330) -> c_int {
331    let result = panic::catch_unwind(|| {
332        // Safety: Calls unsafe helper and slice::from_raw_parts.
333        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
334            Ok(s) => s,
335            Err(code) => return code,
336        };
337        let did_str = match unsafe { c_str_to_str_safe(controller_did) } {
338            Ok(s) => s,
339            Err(code) => return code,
340        };
341        let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
342            Ok(s) => s,
343            Err(code) => return code,
344        };
345        let key_data = unsafe { slice::from_raw_parts(key_ptr, key_len) };
346
347        // Argument validation
348        if alias_str.is_empty()
349            || did_str.is_empty()
350            || !did_str.starts_with("did:")
351            || pass_str.is_empty()
352        {
353            error!(
354                "FFI import failed: Invalid arguments (alias='{}', did='{}', passphrase empty={}).",
355                alias_str,
356                did_str,
357                pass_str.is_empty()
358            );
359            return 1;
360        }
361
362        // Key data validation via seed extraction
363        if let Err(e) = extract_seed_from_key_bytes(key_data) {
364            error!(
365                "FFI import failed: Provided key data is not valid Ed25519 for alias '{}': {}",
366                alias_str, e
367            );
368            return 2;
369        }
370
371        // Encrypt
372        let encrypt_result = encrypt_keypair(key_data, pass_str);
373        let encrypted_key = match encrypt_result {
374            Ok(enc) => enc,
375            Err(e) => {
376                error!(
377                    "FFI import failed: Encryption error for alias '{}': {}",
378                    alias_str, e
379                );
380                return 4; // Encryption error
381            }
382        };
383
384        #[allow(clippy::disallowed_methods)]
385        // INVARIANT: validated with starts_with("did:") guard above
386        let did_string = IdentityDID::new_unchecked(did_str.to_string());
387        let alias = KeyAlias::new_unchecked(alias_str);
388
389        // Store
390        // TODO: Refactor FFI to accept configuration context
391        let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
392            Ok(kc) => kc,
393            Err(e) => {
394                error!("FFI import failed: Failed to get platform keychain: {}", e);
395                return 5; // Keychain initialization error
396            }
397        };
398        let store_result =
399            keychain.store_key(&alias, &did_string, KeyRole::Primary, &encrypted_key);
400
401        #[allow(deprecated)]
402        unsafe {
403            result_to_c_int(store_result, "ffi_import_key")
404        }
405    });
406    result.unwrap_or_else(|_| {
407        error!("FFI ffi_import_key: panic occurred");
408        FFI_ERR_PANIC
409    })
410}
411
412/// Rotates the keypair for a given local alias.
413/// Generates a new key, encrypts it with the *new* passphrase, and replaces the
414/// existing key in secure storage, keeping the association with the original Controller DID.
415///
416/// # Safety
417/// - `alias`, `new_passphrase` must be valid C strings.
418///
419/// # Returns
420/// - 0 on success.
421/// - 1 if arguments are invalid.
422/// - 2 if the original key/alias is not found.
423/// - 3 if crypto operations fail.
424/// - 4 if secure storage or other errors occur.
425/// - FFI_ERR_INVALID_UTF8 (-1) if C strings contain invalid UTF-8
426/// - FFI_ERR_PANIC (-127) if a panic occurred
427#[unsafe(no_mangle)]
428pub unsafe extern "C" fn ffi_rotate_key(
429    alias: *const c_char,
430    new_passphrase: *const c_char,
431) -> c_int {
432    let result = panic::catch_unwind(|| {
433        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
434            Ok(s) => s,
435            Err(code) => return code,
436        };
437        let pass_str = match unsafe { c_str_to_str_safe(new_passphrase) } {
438            Ok(s) => s,
439            Err(code) => return code,
440        };
441
442        // Delegate to the runtime API function
443        // TODO: Refactor FFI to accept configuration context
444        let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
445            Ok(kc) => kc,
446            Err(e) => {
447                error!("FFI rotate_key: Failed to get platform keychain: {}", e);
448                return 5; // Keychain initialization error
449            }
450        };
451        let rotate_result = rotate_key(alias_str, pass_str, keychain.as_ref());
452
453        // Map AgentError to FFI return codes
454        match rotate_result {
455            Ok(()) => 0,
456            Err(e) => {
457                error!("FFI rotate_key failed for alias '{}': {}", alias_str, e);
458                match e {
459                    AgentError::InvalidInput(_) => 1,
460                    AgentError::KeyNotFound => 2,
461                    AgentError::CryptoError(_) | AgentError::KeyDeserializationError(_) => 3,
462                    _ => 4, // Storage, Mutex, etc.
463                }
464            }
465        }
466    });
467    result.unwrap_or_else(|_| {
468        error!("FFI ffi_rotate_key: panic occurred");
469        FFI_ERR_PANIC
470    })
471}
472
473/// Exports the raw *encrypted* private key bytes associated with the alias.
474/// This function does *not* require a passphrase.
475///
476/// # Safety
477/// - `alias` must be a valid C string.
478/// - `out_len` must be a valid pointer to `usize`.
479/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_bytes`.
480///
481/// # Returns
482/// - Non-null pointer to encrypted key bytes on success
483/// - NULL on error (invalid input, key not found, or panic)
484#[unsafe(no_mangle)]
485pub unsafe extern "C" fn ffi_export_encrypted_key(
486    alias: *const c_char,
487    out_len: *mut usize,
488) -> *mut u8 {
489    let result = panic::catch_unwind(|| {
490        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
491            Ok(s) => s,
492            Err(_) => return ptr::null_mut(),
493        };
494        if alias_str.is_empty() || out_len.is_null() {
495            if !out_len.is_null() {
496                unsafe { *out_len = 0 };
497            }
498            return ptr::null_mut();
499        }
500        unsafe { *out_len = 0 };
501
502        // TODO: Refactor FFI to accept configuration context
503        let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
504            Ok(kc) => kc,
505            Err(e) => {
506                error!(
507                    "FFI export encrypted key: Failed to get platform keychain: {}",
508                    e
509                );
510                return ptr::null_mut();
511            }
512        };
513        let alias = KeyAlias::new_unchecked(alias_str);
514        match keychain.load_key(&alias) {
515            Ok((_identity_did, _role, encrypted_data)) => {
516                debug!(
517                    "FFI export encrypted key successful for alias '{}'",
518                    alias_str
519                );
520                unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) }
521            }
522            Err(e) => {
523                error!(
524                    "FFI export encrypted key failed for alias '{}': {}",
525                    alias_str, e
526                );
527                ptr::null_mut()
528            }
529        }
530    });
531    result.unwrap_or_else(|_| {
532        error!("FFI ffi_export_encrypted_key: panic occurred");
533        ptr::null_mut()
534    })
535}
536
537/// Verifies a passphrase against the stored encrypted key for the given alias.
538/// If the passphrase is correct, returns a copy of the *encrypted* key data.
539///
540/// # Safety
541/// - `alias`, `passphrase` must be valid C strings.
542/// - `out_len` must be a valid pointer to `usize`.
543/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_bytes`.
544///
545/// # Returns
546/// - Non-null pointer to encrypted key bytes on success
547/// - NULL on error (invalid input, incorrect passphrase, or panic)
548#[unsafe(no_mangle)]
549pub unsafe extern "C" fn ffi_export_private_key_with_passphrase(
550    alias: *const c_char,
551    passphrase: *const c_char,
552    out_len: *mut usize,
553) -> *mut u8 {
554    let result = panic::catch_unwind(|| {
555        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
556            Ok(s) => s,
557            Err(_) => return ptr::null_mut(),
558        };
559        let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
560            Ok(s) => s,
561            Err(_) => return ptr::null_mut(),
562        };
563
564        if alias_str.is_empty() || out_len.is_null() {
565            if !out_len.is_null() {
566                unsafe { *out_len = 0 };
567            }
568            return ptr::null_mut();
569        }
570        unsafe { *out_len = 0 };
571
572        // TODO: Refactor FFI to accept configuration context
573        let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
574            Ok(kc) => kc,
575            Err(e) => {
576                error!(
577                    "FFI export_private_key_with_passphrase: Failed to get platform keychain: {}",
578                    e
579                );
580                return ptr::null_mut();
581            }
582        };
583        let alias = KeyAlias::new_unchecked(alias_str);
584        let export_result = || -> Result<Vec<u8>, AgentError> {
585            if keychain.is_hardware_backend() {
586                return Err(AgentError::BackendUnavailable {
587                    backend: keychain.backend_name(),
588                    reason: "hardware-backed keys (e.g. Secure Enclave) cannot be exported via this FFI path".to_string(),
589                });
590            }
591            let (_controller_did, _role, encrypted_bytes) = keychain.load_key(&alias)?;
592            // Attempt decryption only to verify passphrase
593            let _decrypted_pkcs8 = decrypt_keypair(&encrypted_bytes, pass_str)?;
594            debug!(
595                "FFI export_private_key_with_passphrase: Passphrase verified for alias '{}'",
596                alias_str
597            );
598            Ok(encrypted_bytes)
599        }();
600
601        match export_result {
602            Ok(encrypted_data) => unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) },
603            Err(e) => {
604                if !matches!(e, AgentError::IncorrectPassphrase) {
605                    error!(
606                        "FFI export_private_key_with_passphrase failed for alias '{}': {}",
607                        alias_str, e
608                    );
609                } else {
610                    debug!(
611                        "FFI export_private_key_with_passphrase: Incorrect passphrase for alias '{}'",
612                        alias_str
613                    );
614                }
615                ptr::null_mut()
616            }
617        }
618    });
619    result.unwrap_or_else(|_| {
620        error!("FFI ffi_export_private_key_with_passphrase: panic occurred");
621        ptr::null_mut()
622    })
623}
624
625/// Exports the decrypted private key in OpenSSH PEM format.
626/// Requires the correct passphrase to decrypt the key.
627///
628/// # Safety
629/// - `alias`, `passphrase` must be valid C strings.
630/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_str`.
631///
632/// # Returns
633/// - Non-null pointer to PEM string on success
634/// - NULL on error (invalid input, incorrect passphrase, or panic)
635#[unsafe(no_mangle)]
636pub unsafe extern "C" fn ffi_export_private_key_openssh(
637    alias: *const c_char,
638    passphrase: *const c_char,
639) -> *mut c_char {
640    let result = panic::catch_unwind(|| {
641        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
642            Ok(s) => s,
643            Err(_) => return ptr::null_mut(),
644        };
645        let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
646            Ok(s) => s,
647            Err(_) => return ptr::null_mut(),
648        };
649
650        if alias_str.is_empty() {
651            return ptr::null_mut();
652        }
653
654        // TODO: Refactor FFI to accept configuration context
655        let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
656            Ok(kc) => kc,
657            Err(e) => {
658                error!("FFI export PEM: Failed to get platform keychain: {}", e);
659                return ptr::null_mut();
660            }
661        };
662        match export_key_openssh_pem(alias_str, pass_str, keychain.as_ref()) {
663            Ok(pem_zeroizing) => malloc_and_copy_string(pem_zeroizing.as_str()),
664            Err(e) => {
665                error!("FFI export PEM failed for alias '{}': {}", alias_str, e);
666                ptr::null_mut()
667            }
668        }
669    });
670    result.unwrap_or_else(|_| {
671        error!("FFI ffi_export_private_key_openssh: panic occurred");
672        ptr::null_mut()
673    })
674}
675
676/// Exports the public key in OpenSSH `.pub` format.
677/// Requires the correct passphrase to decrypt the associated private key first.
678///
679/// # Safety
680/// - `alias`, `passphrase` must be valid C strings.
681/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_str`.
682///
683/// # Returns
684/// - Non-null pointer to public key string on success
685/// - NULL on error (invalid input, incorrect passphrase, or panic)
686#[unsafe(no_mangle)]
687pub unsafe extern "C" fn ffi_export_public_key_openssh(
688    alias: *const c_char,
689    passphrase: *const c_char,
690) -> *mut c_char {
691    let result = panic::catch_unwind(|| {
692        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
693            Ok(s) => s,
694            Err(_) => return ptr::null_mut(),
695        };
696        let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
697            Ok(s) => s,
698            Err(_) => return ptr::null_mut(),
699        };
700
701        if alias_str.is_empty() {
702            return ptr::null_mut();
703        }
704
705        // TODO: Refactor FFI to accept configuration context
706        let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
707            Ok(kc) => kc,
708            Err(e) => {
709                error!(
710                    "FFI export OpenSSH pubkey: Failed to get platform keychain: {}",
711                    e
712                );
713                return ptr::null_mut();
714            }
715        };
716        match export_key_openssh_pub(alias_str, pass_str, keychain.as_ref()) {
717            Ok(formatted_pubkey) => malloc_and_copy_string(&formatted_pubkey),
718            Err(e) => {
719                error!(
720                    "FFI export OpenSSH pubkey failed for alias '{}': {}",
721                    alias_str, e
722                );
723                ptr::null_mut()
724            }
725        }
726    });
727    result.unwrap_or_else(|_| {
728        error!("FFI ffi_export_public_key_openssh: panic occurred");
729        ptr::null_mut()
730    })
731}
732
733/// Signs a message using a key loaded into the FFI agent.
734///
735/// **Important:** `ffi_init_agent()` must be called before using this function.
736///
737/// # Safety
738/// - `pubkey_ptr` must point to valid public key bytes of `pubkey_len` bytes.
739/// - `data_ptr` must point to valid data bytes of `data_len` bytes.
740/// - `out_len` must be a valid pointer to `usize`.
741/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_bytes`.
742///
743/// # Returns
744/// - Non-null pointer to signature bytes on success
745/// - NULL on error (agent not initialized, invalid input, key not found, or panic)
746#[unsafe(no_mangle)]
747pub unsafe extern "C" fn ffi_agent_sign(
748    pubkey_ptr: *const c_uchar,
749    pubkey_len: usize,
750    data_ptr: *const c_uchar,
751    data_len: usize,
752    out_len: *mut usize,
753) -> *mut u8 {
754    let result = panic::catch_unwind(|| {
755        if pubkey_ptr.is_null() || data_ptr.is_null() || out_len.is_null() {
756            if !out_len.is_null() {
757                unsafe { *out_len = 0 };
758            }
759            error!("FFI agent_sign failed: Null pointer argument.");
760            return ptr::null_mut();
761        }
762
763        // Get the FFI agent handle
764        let handle = match get_ffi_agent() {
765            Some(h) => h,
766            None => {
767                error!(
768                    "FFI agent_sign failed: Agent not initialized. Call ffi_init_agent() first."
769                );
770                unsafe { *out_len = 0 };
771                return ptr::null_mut();
772            }
773        };
774
775        let pubkey_slice = unsafe { slice::from_raw_parts(pubkey_ptr, pubkey_len) };
776        let data_slice = unsafe { slice::from_raw_parts(data_ptr, data_len) };
777        unsafe { *out_len = 0 };
778
779        match agent_sign_with_handle(&handle, pubkey_slice, data_slice) {
780            Ok(signature_bytes) => unsafe { malloc_and_copy_bytes(&signature_bytes, out_len) },
781            Err(e) => {
782                error!("FFI agent_sign failed: {}", e);
783                if matches!(e, AgentError::KeyNotFound) {
784                    warn!(
785                        "FFI agent_sign: Key not found in agent for pubkey prefix {:x?}",
786                        &pubkey_slice[..std::cmp::min(pubkey_slice.len(), 8)]
787                    );
788                }
789                ptr::null_mut()
790            }
791        }
792    });
793    result.unwrap_or_else(|_| {
794        error!("FFI ffi_agent_sign: panic occurred");
795        ptr::null_mut()
796    })
797}
798
799// --- General Crypto & Config FFI Functions ---
800
801/// Encrypts data using the given passphrase.
802///
803/// # Safety
804/// - `passphrase` must be a valid null-terminated C string
805/// - `input_ptr` must point to valid memory of at least `input_len` bytes
806/// - `out_len` must be a valid pointer to write the output length
807///
808/// # Returns
809/// - Non-null pointer to encrypted bytes on success
810/// - NULL on error (invalid input or panic)
811#[unsafe(no_mangle)]
812pub unsafe extern "C" fn ffi_encrypt_data(
813    passphrase: *const c_char,
814    input_ptr: *const u8,
815    input_len: usize,
816    out_len: *mut usize,
817) -> *mut u8 {
818    let result = panic::catch_unwind(|| {
819        if input_ptr.is_null() || out_len.is_null() {
820            if !out_len.is_null() {
821                unsafe { *out_len = 0 };
822            }
823            error!("FFI encrypt_data failed: Null pointer argument.");
824            return ptr::null_mut();
825        }
826        let pass = match unsafe { c_str_to_str_safe(passphrase) } {
827            Ok(s) => s,
828            Err(_) => return ptr::null_mut(),
829        };
830        let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
831        unsafe { *out_len = 0 };
832        let algo = current_algorithm();
833
834        match encrypt_bytes(input, pass, algo) {
835            Ok(encrypted) => unsafe { malloc_and_copy_bytes(&encrypted, out_len) },
836            Err(e) => {
837                error!("FFI encrypt_data failed: {}", e);
838                ptr::null_mut()
839            }
840        }
841    });
842    result.unwrap_or_else(|_| {
843        error!("FFI ffi_encrypt_data: panic occurred");
844        ptr::null_mut()
845    })
846}
847
848/// Decrypts data using the given passphrase.
849///
850/// # Safety
851/// - `passphrase` must be a valid null-terminated C string
852/// - `input_ptr` must point to valid memory of at least `input_len` bytes
853/// - `out_len` must be a valid pointer to write the output length
854///
855/// # Returns
856/// - Non-null pointer to decrypted bytes on success
857/// - NULL on error (invalid input, incorrect passphrase, or panic)
858#[unsafe(no_mangle)]
859pub unsafe extern "C" fn ffi_decrypt_data(
860    passphrase: *const c_char,
861    input_ptr: *const u8,
862    input_len: usize,
863    out_len: *mut usize,
864) -> *mut u8 {
865    let result = panic::catch_unwind(|| {
866        if input_ptr.is_null() || out_len.is_null() {
867            if !out_len.is_null() {
868                unsafe { *out_len = 0 };
869            }
870            error!("FFI decrypt_data failed: Null pointer argument.");
871            return ptr::null_mut();
872        }
873        let pass = match unsafe { c_str_to_str_safe(passphrase) } {
874            Ok(s) => s,
875            Err(_) => return ptr::null_mut(),
876        };
877        let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
878        unsafe { *out_len = 0 };
879
880        match decrypt_bytes(input, pass) {
881            Ok(decrypted) => unsafe { malloc_and_copy_bytes(&decrypted, out_len) },
882            Err(e) => {
883                if !matches!(e, AgentError::IncorrectPassphrase) {
884                    error!("FFI decrypt_data failed: {}", e);
885                } else {
886                    debug!("FFI decrypt_data: Incorrect passphrase provided.");
887                }
888                ptr::null_mut()
889            }
890        }
891    });
892    result.unwrap_or_else(|_| {
893        error!("FFI ffi_decrypt_data: panic occurred");
894        ptr::null_mut()
895    })
896}
897
898/// Frees a C string (`char *`) previously returned by an FFI function
899/// in this library (which allocated it using `CString::into_raw`).
900/// Does nothing if `ptr` is null.
901///
902/// # Safety
903/// - `ptr` must be null or must have been previously allocated by a function
904///   in this library that returns `*mut c_char` (eg, `ffi_export_..._openssh`).
905/// - `ptr` must not be used after calling this function.
906#[unsafe(no_mangle)]
907pub unsafe extern "C" fn ffi_free_str(ptr: *mut c_char) {
908    let _ = panic::catch_unwind(|| {
909        if !ptr.is_null() {
910            // Safety: We are reclaiming ownership of the pointer originally transferred
911            // via CString::into_raw and letting the CString drop, which frees the memory.
912            let _ = unsafe { CString::from_raw(ptr) };
913        }
914    });
915    // Note: If panic occurs during free, we just swallow it to avoid UB from unwinding across FFI
916}
917
918/// Frees a byte buffer (`unsigned char *` / `uint8_t *`) previously returned
919/// by an FFI function in this library (which allocated it using `libc::malloc`).
920/// Does nothing if `ptr` is null. The `len` argument is ignored but kept for
921/// potential C-side compatibility if callers expect it.
922///
923/// # Safety
924/// - `ptr` must be null or must have been previously allocated by a function
925///   in this library that returns `*mut u8` (eg, `ffi_agent_sign`, `ffi_export_encrypted_key`).
926/// - `ptr` must not be used after calling this function.
927#[unsafe(no_mangle)]
928pub unsafe extern "C" fn ffi_free_bytes(ptr: *mut u8, _len: usize) {
929    let _ = panic::catch_unwind(|| {
930        if !ptr.is_null() {
931            unsafe { libc::free(ptr as *mut libc::c_void) };
932        }
933    });
934    // Note: If panic occurs during free, we just swallow it to avoid UB from unwinding across FFI
935}
936
937/// Sets the global encryption algorithm level used by `encrypt_keypair`.
938/// (1 = AES-GCM-256, 2 = ChaCha20Poly1305). Defaults to AES if level is unknown.
939///
940/// # Safety
941/// This function modifies global state. It should not be called concurrently
942/// from multiple threads.
943#[unsafe(no_mangle)]
944pub unsafe extern "C" fn ffi_set_encryption_algorithm(level: c_int) {
945    let _ = panic::catch_unwind(|| {
946        let algo = match level {
947            1 => EncryptionAlgorithm::AesGcm256,
948            2 => EncryptionAlgorithm::ChaCha20Poly1305,
949            _ => {
950                warn!(
951                    "FFI: Unknown encryption level {}, defaulting to AES-GCM.",
952                    level
953                );
954                EncryptionAlgorithm::AesGcm256
955            }
956        };
957        info!("FFI: Setting global encryption algorithm to {:?}", algo);
958        set_encryption_algorithm(algo);
959    });
960    // Note: If panic occurs, we just swallow it to avoid UB from unwinding across FFI
961}
962
963// --- Deprecated / Removed Functions ---
964
965// `ffi_init_identity` removed - requires more complex setup (metadata file) now.
966// `ffi_start_agent` removed - agent startup is separate from key loading now.
967// `ffi_get_public_key` removed - use `ffi_export_public_key_openssh`.
968// `ffi_sign_ssh_agent_request` removed - use `ffi_agent_sign`.
969// `ffi_sign_ssh_agent_request_with_passphrase` removed - use `ffi_agent_sign`.
970// Internal `sign_ssh_agent_request` removed.