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        let key_data = unsafe { slice::from_raw_parts(key_ptr, key_len) };
468
469        // Argument validation
470        if alias_str.is_empty()
471            || did_str.is_empty()
472            || !did_str.starts_with("did:")
473            || pass_str.is_empty()
474        {
475            error!(
476                "FFI import failed: Invalid arguments (alias='{}', did='{}', passphrase empty={}).",
477                alias_str,
478                did_str,
479                pass_str.is_empty()
480            );
481            return 1;
482        }
483
484        // Key data validation via seed extraction
485        if let Err(e) = extract_seed_from_key_bytes(key_data) {
486            error!(
487                "FFI import failed: Provided key data is not valid Ed25519 for alias '{}': {}",
488                alias_str, e
489            );
490            return 2;
491        }
492
493        // Encrypt
494        let encrypt_result = encrypt_keypair(key_data, pass_str);
495        let encrypted_key = match encrypt_result {
496            Ok(enc) => enc,
497            Err(e) => {
498                error!(
499                    "FFI import failed: Encryption error for alias '{}': {}",
500                    alias_str, e
501                );
502                return 4; // Encryption error
503            }
504        };
505
506        #[allow(clippy::disallowed_methods)]
507        // INVARIANT: validated with starts_with("did:") guard above
508        let did_string = IdentityDID::new_unchecked(did_str.to_string());
509        let alias = KeyAlias::new_unchecked(alias_str);
510
511        // Store
512        let keychain = match unsafe { keychain_from_context(ctx, "ffi_import_key") } {
513            Ok(kc) => kc,
514            Err(code) => return code,
515        };
516        let store_result =
517            keychain.store_key(&alias, &did_string, KeyRole::Primary, &encrypted_key);
518
519        unsafe { result_to_c_int(store_result, "ffi_import_key") }
520    });
521    result.unwrap_or_else(|_| {
522        error!("FFI ffi_import_key: panic occurred");
523        FFI_ERR_PANIC
524    })
525}
526
527/// Rotates the keypair for a given local alias.
528/// Generates a new key, encrypts it with the *new* passphrase, and replaces the
529/// existing key in secure storage, keeping the association with the original Controller DID.
530///
531/// # Safety
532/// - `ctx` must be null or a pointer returned by `ffi_context_new` that has not been freed.
533/// - `alias`, `new_passphrase` must be valid C strings.
534///
535/// # Returns
536/// - 0 on success.
537/// - 1 if arguments are invalid.
538/// - 2 if the original key/alias is not found.
539/// - 3 if crypto operations fail.
540/// - 4 if secure storage or other errors occur.
541/// - FFI_ERR_KEYCHAIN (5) if keychain initialization fails
542/// - FFI_ERR_INVALID_UTF8 (-1) if C strings contain invalid UTF-8
543/// - FFI_ERR_NULL_CONTEXT (-3) if `ctx` is null
544/// - FFI_ERR_PANIC (-127) if a panic occurred
545#[unsafe(no_mangle)]
546pub unsafe extern "C" fn ffi_rotate_key(
547    ctx: *const AuthsFfiContext,
548    alias: *const c_char,
549    new_passphrase: *const c_char,
550) -> c_int {
551    let result = panic::catch_unwind(|| {
552        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
553            Ok(s) => s,
554            Err(code) => return code,
555        };
556        let pass_str = match unsafe { c_str_to_str_safe(new_passphrase) } {
557            Ok(s) => s,
558            Err(code) => return code,
559        };
560
561        // Delegate to the runtime API function
562        let keychain = match unsafe { keychain_from_context(ctx, "ffi_rotate_key") } {
563            Ok(kc) => kc,
564            Err(code) => return code,
565        };
566        let rotate_result = rotate_key(alias_str, pass_str, keychain.as_ref());
567
568        // Map AgentError to FFI return codes
569        match rotate_result {
570            Ok(()) => 0,
571            Err(e) => {
572                error!("FFI rotate_key failed for alias '{}': {}", alias_str, e);
573                match e {
574                    AgentError::InvalidInput(_) => 1,
575                    AgentError::KeyNotFound => 2,
576                    AgentError::CryptoError(_) | AgentError::KeyDeserializationError(_) => 3,
577                    _ => 4, // Storage, Mutex, etc.
578                }
579            }
580        }
581    });
582    result.unwrap_or_else(|_| {
583        error!("FFI ffi_rotate_key: panic occurred");
584        FFI_ERR_PANIC
585    })
586}
587
588/// Exports the raw *encrypted* private key bytes associated with the alias.
589/// This function does *not* require a passphrase.
590///
591/// # Safety
592/// - `ctx` must be null or a pointer returned by `ffi_context_new` that has not been freed.
593/// - `alias` must be a valid C string.
594/// - `out_len` must be a valid pointer to `usize`.
595/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_bytes`.
596///
597/// # Returns
598/// - Non-null pointer to encrypted key bytes on success
599/// - NULL on error (null context, invalid input, key not found, or panic)
600#[unsafe(no_mangle)]
601pub unsafe extern "C" fn ffi_export_encrypted_key(
602    ctx: *const AuthsFfiContext,
603    alias: *const c_char,
604    out_len: *mut usize,
605) -> *mut u8 {
606    let result = panic::catch_unwind(|| {
607        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
608            Ok(s) => s,
609            Err(_) => return ptr::null_mut(),
610        };
611        if alias_str.is_empty() || out_len.is_null() {
612            if !out_len.is_null() {
613                unsafe { *out_len = 0 };
614            }
615            return ptr::null_mut();
616        }
617        unsafe { *out_len = 0 };
618
619        let keychain = match unsafe { keychain_from_context(ctx, "ffi_export_encrypted_key") } {
620            Ok(kc) => kc,
621            Err(_) => return ptr::null_mut(),
622        };
623        let alias = KeyAlias::new_unchecked(alias_str);
624        match keychain.load_key(&alias) {
625            Ok((_identity_did, _role, encrypted_data)) => {
626                debug!(
627                    "FFI export encrypted key successful for alias '{}'",
628                    alias_str
629                );
630                unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) }
631            }
632            Err(e) => {
633                error!(
634                    "FFI export encrypted key failed for alias '{}': {}",
635                    alias_str, e
636                );
637                ptr::null_mut()
638            }
639        }
640    });
641    result.unwrap_or_else(|_| {
642        error!("FFI ffi_export_encrypted_key: panic occurred");
643        ptr::null_mut()
644    })
645}
646
647/// Verifies a passphrase against the stored encrypted key for the given alias.
648/// If the passphrase is correct, returns a copy of the *encrypted* key data.
649///
650/// # Safety
651/// - `ctx` must be null or a pointer returned by `ffi_context_new` that has not been freed.
652/// - `alias`, `passphrase` must be valid C strings.
653/// - `out_len` must be a valid pointer to `usize`.
654/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_bytes`.
655///
656/// # Returns
657/// - Non-null pointer to encrypted key bytes on success
658/// - NULL on error (null context, invalid input, incorrect passphrase, or panic)
659#[unsafe(no_mangle)]
660pub unsafe extern "C" fn ffi_export_private_key_with_passphrase(
661    ctx: *const AuthsFfiContext,
662    alias: *const c_char,
663    passphrase: *const c_char,
664    out_len: *mut usize,
665) -> *mut u8 {
666    let result = panic::catch_unwind(|| {
667        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
668            Ok(s) => s,
669            Err(_) => return ptr::null_mut(),
670        };
671        let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
672            Ok(s) => s,
673            Err(_) => return ptr::null_mut(),
674        };
675
676        if alias_str.is_empty() || out_len.is_null() {
677            if !out_len.is_null() {
678                unsafe { *out_len = 0 };
679            }
680            return ptr::null_mut();
681        }
682        unsafe { *out_len = 0 };
683
684        let keychain =
685            match unsafe { keychain_from_context(ctx, "ffi_export_private_key_with_passphrase") } {
686                Ok(kc) => kc,
687                Err(_) => return ptr::null_mut(),
688            };
689        let alias = KeyAlias::new_unchecked(alias_str);
690        let export_result = || -> Result<Vec<u8>, AgentError> {
691            if keychain.is_hardware_backend() {
692                return Err(AgentError::BackendUnavailable {
693                    backend: keychain.backend_name(),
694                    reason: "hardware-backed keys (e.g. Secure Enclave) cannot be exported via this FFI path".to_string(),
695                });
696            }
697            let (_controller_did, _role, encrypted_bytes) = keychain.load_key(&alias)?;
698            // Attempt decryption only to verify passphrase
699            let _decrypted_pkcs8 = decrypt_keypair(&encrypted_bytes, pass_str)?;
700            debug!(
701                "FFI export_private_key_with_passphrase: Passphrase verified for alias '{}'",
702                alias_str
703            );
704            Ok(encrypted_bytes)
705        }();
706
707        match export_result {
708            Ok(encrypted_data) => unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) },
709            Err(e) => {
710                if !matches!(e, AgentError::IncorrectPassphrase) {
711                    error!(
712                        "FFI export_private_key_with_passphrase failed for alias '{}': {}",
713                        alias_str, e
714                    );
715                } else {
716                    debug!(
717                        "FFI export_private_key_with_passphrase: Incorrect passphrase for alias '{}'",
718                        alias_str
719                    );
720                }
721                ptr::null_mut()
722            }
723        }
724    });
725    result.unwrap_or_else(|_| {
726        error!("FFI ffi_export_private_key_with_passphrase: panic occurred");
727        ptr::null_mut()
728    })
729}
730
731/// Exports the decrypted private key in OpenSSH PEM format.
732/// Requires the correct passphrase to decrypt the key.
733///
734/// # Safety
735/// - `ctx` must be null or a pointer returned by `ffi_context_new` that has not been freed.
736/// - `alias`, `passphrase` must be valid C strings.
737/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_str`.
738///
739/// # Returns
740/// - Non-null pointer to PEM string on success
741/// - NULL on error (null context, invalid input, incorrect passphrase, or panic)
742#[unsafe(no_mangle)]
743pub unsafe extern "C" fn ffi_export_private_key_openssh(
744    ctx: *const AuthsFfiContext,
745    alias: *const c_char,
746    passphrase: *const c_char,
747) -> *mut c_char {
748    let result = panic::catch_unwind(|| {
749        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
750            Ok(s) => s,
751            Err(_) => return ptr::null_mut(),
752        };
753        let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
754            Ok(s) => s,
755            Err(_) => return ptr::null_mut(),
756        };
757
758        if alias_str.is_empty() {
759            return ptr::null_mut();
760        }
761
762        let keychain = match unsafe { keychain_from_context(ctx, "ffi_export_private_key_openssh") }
763        {
764            Ok(kc) => kc,
765            Err(_) => return ptr::null_mut(),
766        };
767        match export_key_openssh_pem(alias_str, pass_str, keychain.as_ref()) {
768            Ok(pem_zeroizing) => malloc_and_copy_string(pem_zeroizing.as_str()),
769            Err(e) => {
770                error!("FFI export PEM failed for alias '{}': {}", alias_str, e);
771                ptr::null_mut()
772            }
773        }
774    });
775    result.unwrap_or_else(|_| {
776        error!("FFI ffi_export_private_key_openssh: panic occurred");
777        ptr::null_mut()
778    })
779}
780
781/// Exports the public key in OpenSSH `.pub` format.
782/// Requires the correct passphrase to decrypt the associated private key first.
783///
784/// # Safety
785/// - `ctx` must be null or a pointer returned by `ffi_context_new` that has not been freed.
786/// - `alias`, `passphrase` must be valid C strings.
787/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_str`.
788///
789/// # Returns
790/// - Non-null pointer to public key string on success
791/// - NULL on error (null context, invalid input, incorrect passphrase, or panic)
792#[unsafe(no_mangle)]
793pub unsafe extern "C" fn ffi_export_public_key_openssh(
794    ctx: *const AuthsFfiContext,
795    alias: *const c_char,
796    passphrase: *const c_char,
797) -> *mut c_char {
798    let result = panic::catch_unwind(|| {
799        let alias_str = match unsafe { c_str_to_str_safe(alias) } {
800            Ok(s) => s,
801            Err(_) => return ptr::null_mut(),
802        };
803        let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
804            Ok(s) => s,
805            Err(_) => return ptr::null_mut(),
806        };
807
808        if alias_str.is_empty() {
809            return ptr::null_mut();
810        }
811
812        let keychain = match unsafe { keychain_from_context(ctx, "ffi_export_public_key_openssh") }
813        {
814            Ok(kc) => kc,
815            Err(_) => return ptr::null_mut(),
816        };
817        match export_key_openssh_pub(alias_str, pass_str, keychain.as_ref()) {
818            Ok(formatted_pubkey) => malloc_and_copy_string(&formatted_pubkey),
819            Err(e) => {
820                error!(
821                    "FFI export OpenSSH pubkey failed for alias '{}': {}",
822                    alias_str, e
823                );
824                ptr::null_mut()
825            }
826        }
827    });
828    result.unwrap_or_else(|_| {
829        error!("FFI ffi_export_public_key_openssh: panic occurred");
830        ptr::null_mut()
831    })
832}
833
834/// Signs a message using a key loaded into the FFI agent.
835///
836/// **Important:** `ffi_init_agent()` must be called before using this function.
837///
838/// # Safety
839/// - `pubkey_ptr` must point to valid public key bytes of `pubkey_len` bytes.
840/// - `data_ptr` must point to valid data bytes of `data_len` bytes.
841/// - `out_len` must be a valid pointer to `usize`.
842/// - The returned pointer (if not null) must be freed by the caller using `ffi_free_bytes`.
843///
844/// # Returns
845/// - Non-null pointer to signature bytes on success
846/// - NULL on error (agent not initialized, invalid input, key not found, or panic)
847#[unsafe(no_mangle)]
848pub unsafe extern "C" fn ffi_agent_sign(
849    pubkey_ptr: *const c_uchar,
850    pubkey_len: usize,
851    data_ptr: *const c_uchar,
852    data_len: usize,
853    out_len: *mut usize,
854) -> *mut u8 {
855    let result = panic::catch_unwind(|| {
856        if pubkey_ptr.is_null() || data_ptr.is_null() || out_len.is_null() {
857            if !out_len.is_null() {
858                unsafe { *out_len = 0 };
859            }
860            error!("FFI agent_sign failed: Null pointer argument.");
861            return ptr::null_mut();
862        }
863
864        // Get the FFI agent handle
865        let handle = match get_ffi_agent() {
866            Some(h) => h,
867            None => {
868                error!(
869                    "FFI agent_sign failed: Agent not initialized. Call ffi_init_agent() first."
870                );
871                unsafe { *out_len = 0 };
872                return ptr::null_mut();
873            }
874        };
875
876        let pubkey_slice = unsafe { slice::from_raw_parts(pubkey_ptr, pubkey_len) };
877        let data_slice = unsafe { slice::from_raw_parts(data_ptr, data_len) };
878        unsafe { *out_len = 0 };
879
880        match agent_sign_with_handle(&handle, pubkey_slice, data_slice) {
881            Ok(signature_bytes) => unsafe { malloc_and_copy_bytes(&signature_bytes, out_len) },
882            Err(e) => {
883                error!("FFI agent_sign failed: {}", e);
884                if matches!(e, AgentError::KeyNotFound) {
885                    warn!(
886                        "FFI agent_sign: Key not found in agent for pubkey prefix {:x?}",
887                        &pubkey_slice[..std::cmp::min(pubkey_slice.len(), 8)]
888                    );
889                }
890                ptr::null_mut()
891            }
892        }
893    });
894    result.unwrap_or_else(|_| {
895        error!("FFI ffi_agent_sign: panic occurred");
896        ptr::null_mut()
897    })
898}
899
900// --- General Crypto & Config FFI Functions ---
901
902/// Encrypts data using the given passphrase.
903///
904/// # Safety
905/// - `passphrase` must be a valid null-terminated C string
906/// - `input_ptr` must point to valid memory of at least `input_len` bytes
907/// - `out_len` must be a valid pointer to write the output length
908///
909/// # Returns
910/// - Non-null pointer to encrypted bytes on success
911/// - NULL on error (invalid input or panic)
912#[unsafe(no_mangle)]
913pub unsafe extern "C" fn ffi_encrypt_data(
914    passphrase: *const c_char,
915    input_ptr: *const u8,
916    input_len: usize,
917    out_len: *mut usize,
918) -> *mut u8 {
919    let result = panic::catch_unwind(|| {
920        if input_ptr.is_null() || out_len.is_null() {
921            if !out_len.is_null() {
922                unsafe { *out_len = 0 };
923            }
924            error!("FFI encrypt_data failed: Null pointer argument.");
925            return ptr::null_mut();
926        }
927        let pass = match unsafe { c_str_to_str_safe(passphrase) } {
928            Ok(s) => s,
929            Err(_) => return ptr::null_mut(),
930        };
931        let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
932        unsafe { *out_len = 0 };
933        let algo = current_algorithm();
934
935        match encrypt_bytes(input, pass, algo) {
936            Ok(encrypted) => unsafe { malloc_and_copy_bytes(&encrypted, out_len) },
937            Err(e) => {
938                error!("FFI encrypt_data failed: {}", e);
939                ptr::null_mut()
940            }
941        }
942    });
943    result.unwrap_or_else(|_| {
944        error!("FFI ffi_encrypt_data: panic occurred");
945        ptr::null_mut()
946    })
947}
948
949/// Decrypts data using the given passphrase.
950///
951/// # Safety
952/// - `passphrase` must be a valid null-terminated C string
953/// - `input_ptr` must point to valid memory of at least `input_len` bytes
954/// - `out_len` must be a valid pointer to write the output length
955///
956/// # Returns
957/// - Non-null pointer to decrypted bytes on success
958/// - NULL on error (invalid input, incorrect passphrase, or panic)
959#[unsafe(no_mangle)]
960pub unsafe extern "C" fn ffi_decrypt_data(
961    passphrase: *const c_char,
962    input_ptr: *const u8,
963    input_len: usize,
964    out_len: *mut usize,
965) -> *mut u8 {
966    let result = panic::catch_unwind(|| {
967        if input_ptr.is_null() || out_len.is_null() {
968            if !out_len.is_null() {
969                unsafe { *out_len = 0 };
970            }
971            error!("FFI decrypt_data failed: Null pointer argument.");
972            return ptr::null_mut();
973        }
974        let pass = match unsafe { c_str_to_str_safe(passphrase) } {
975            Ok(s) => s,
976            Err(_) => return ptr::null_mut(),
977        };
978        let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
979        unsafe { *out_len = 0 };
980
981        match decrypt_bytes(input, pass) {
982            Ok(decrypted) => unsafe { malloc_and_copy_bytes(&decrypted, out_len) },
983            Err(e) => {
984                if !matches!(e, AgentError::IncorrectPassphrase) {
985                    error!("FFI decrypt_data failed: {}", e);
986                } else {
987                    debug!("FFI decrypt_data: Incorrect passphrase provided.");
988                }
989                ptr::null_mut()
990            }
991        }
992    });
993    result.unwrap_or_else(|_| {
994        error!("FFI ffi_decrypt_data: panic occurred");
995        ptr::null_mut()
996    })
997}
998
999/// Frees a C string (`char *`) previously returned by an FFI function
1000/// in this library (which allocated it using `CString::into_raw`).
1001/// Does nothing if `ptr` is null.
1002///
1003/// # Safety
1004/// - `ptr` must be null or must have been previously allocated by a function
1005///   in this library that returns `*mut c_char` (eg, `ffi_export_..._openssh`).
1006/// - `ptr` must not be used after calling this function.
1007#[unsafe(no_mangle)]
1008pub unsafe extern "C" fn ffi_free_str(ptr: *mut c_char) {
1009    let _ = panic::catch_unwind(|| {
1010        if !ptr.is_null() {
1011            // Safety: We are reclaiming ownership of the pointer originally transferred
1012            // via CString::into_raw and letting the CString drop, which frees the memory.
1013            let _ = unsafe { CString::from_raw(ptr) };
1014        }
1015    });
1016    // Note: If panic occurs during free, we just swallow it to avoid UB from unwinding across FFI
1017}
1018
1019/// Frees a byte buffer (`unsigned char *` / `uint8_t *`) previously returned
1020/// by an FFI function in this library (which allocated it using `libc::malloc`).
1021/// Does nothing if `ptr` is null. The `len` argument is ignored but kept for
1022/// potential C-side compatibility if callers expect it.
1023///
1024/// # Safety
1025/// - `ptr` must be null or must have been previously allocated by a function
1026///   in this library that returns `*mut u8` (eg, `ffi_agent_sign`, `ffi_export_encrypted_key`).
1027/// - `ptr` must not be used after calling this function.
1028#[unsafe(no_mangle)]
1029pub unsafe extern "C" fn ffi_free_bytes(ptr: *mut u8, _len: usize) {
1030    let _ = panic::catch_unwind(|| {
1031        if !ptr.is_null() {
1032            unsafe { libc::free(ptr as *mut libc::c_void) };
1033        }
1034    });
1035    // Note: If panic occurs during free, we just swallow it to avoid UB from unwinding across FFI
1036}
1037
1038/// Sets the global encryption algorithm level used by `encrypt_keypair`.
1039/// (1 = AES-GCM-256, 2 = ChaCha20Poly1305). Defaults to AES if level is unknown.
1040///
1041/// # Safety
1042/// This function modifies global state. It should not be called concurrently
1043/// from multiple threads.
1044#[unsafe(no_mangle)]
1045pub unsafe extern "C" fn ffi_set_encryption_algorithm(level: c_int) {
1046    let _ = panic::catch_unwind(|| {
1047        let algo = match level {
1048            1 => EncryptionAlgorithm::AesGcm256,
1049            2 => EncryptionAlgorithm::ChaCha20Poly1305,
1050            _ => {
1051                warn!(
1052                    "FFI: Unknown encryption level {}, defaulting to AES-GCM.",
1053                    level
1054                );
1055                EncryptionAlgorithm::AesGcm256
1056            }
1057        };
1058        info!("FFI: Setting global encryption algorithm to {:?}", algo);
1059        set_encryption_algorithm(algo);
1060    });
1061    // Note: If panic occurs, we just swallow it to avoid UB from unwinding across FFI
1062}
1063
1064// --- Deprecated / Removed Functions ---
1065
1066// `ffi_init_identity` removed - requires more complex setup (metadata file) now.
1067// `ffi_start_agent` removed - agent startup is separate from key loading now.
1068// `ffi_get_public_key` removed - use `ffi_export_public_key_openssh`.
1069// `ffi_sign_ssh_agent_request` removed - use `ffi_agent_sign`.
1070// `ffi_sign_ssh_agent_request_with_passphrase` removed - use `ffi_agent_sign`.
1071// Internal `sign_ssh_agent_request` removed.