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