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