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.