1use crate::agent::AgentHandle;
16use crate::api::runtime::{
17 agent_sign_with_handle, export_key_openssh_pem, export_key_openssh_pub, rotate_key,
18};
19use crate::config::EnvironmentConfig;
20use crate::config::{current_algorithm, set_encryption_algorithm};
21use crate::crypto::EncryptionAlgorithm;
22use crate::crypto::encryption::{decrypt_bytes, encrypt_bytes};
23use crate::crypto::signer::extract_seed_from_key_bytes;
24use crate::crypto::signer::{decrypt_keypair, encrypt_keypair};
25use crate::error::AgentError;
26use crate::storage::keychain::{
27 IdentityDID, KeyAlias, KeyRole, KeyStorage, get_platform_keychain_with_config,
28};
29use log::{debug, error, info, warn};
30use parking_lot::RwLock;
31use std::ffi::{CStr, CString};
32use std::os::raw::{c_char, c_int, c_uchar};
33use std::panic;
34use std::path::PathBuf;
35use std::ptr;
36use std::slice;
37use std::sync::{Arc, LazyLock};
38
39pub const FFI_OK: c_int = 0;
43pub const FFI_ERR_INVALID_UTF8: c_int = -1;
45pub const FFI_ERR_AGENT_NOT_INITIALIZED: c_int = -2;
47pub const FFI_ERR_PANIC: c_int = -127;
49
50static FFI_AGENT: LazyLock<RwLock<Option<Arc<AgentHandle>>>> = LazyLock::new(|| RwLock::new(None));
57
58#[unsafe(no_mangle)]
70#[allow(clippy::disallowed_methods)] pub unsafe extern "C" fn ffi_init_agent(socket_path: *const c_char) -> c_int {
72 let result = panic::catch_unwind(|| {
73 let path_str = match unsafe { c_str_to_str_safe(socket_path) } {
74 Ok(s) if !s.is_empty() => s,
75 Ok(_) => {
76 let home = match dirs::home_dir() {
78 Some(h) => h,
79 None => {
80 error!("FFI ffi_init_agent: Could not determine home directory");
81 return 1;
82 }
83 };
84 let default_path = home.join(".auths").join("agent.sock");
85 let handle = Arc::new(AgentHandle::new(default_path));
86 let mut guard = FFI_AGENT.write();
87 *guard = Some(handle);
88 info!("FFI agent initialized with default socket path");
89 return 0;
90 }
91 Err(code) => return code,
92 };
93
94 let socket = PathBuf::from(path_str);
95 let handle = Arc::new(AgentHandle::new(socket));
96
97 let mut guard = FFI_AGENT.write();
98 *guard = Some(handle);
99 info!("FFI agent initialized with socket path: {}", path_str);
100 0
101 });
102 result.unwrap_or_else(|_| {
103 error!("FFI ffi_init_agent: panic occurred");
104 FFI_ERR_PANIC
105 })
106}
107
108#[unsafe(no_mangle)]
120pub unsafe extern "C" fn ffi_shutdown_agent() -> c_int {
121 let result = panic::catch_unwind(|| {
122 let mut guard = FFI_AGENT.write();
123 if let Some(handle) = guard.take() {
124 if let Err(e) = handle.shutdown() {
125 warn!("FFI ffi_shutdown_agent: Shutdown returned error: {}", e);
126 }
127 info!("FFI agent shut down");
128 } else {
129 debug!("FFI ffi_shutdown_agent: Agent was not initialized");
130 }
131 0
132 });
133 result.unwrap_or_else(|_| {
134 error!("FFI ffi_shutdown_agent: panic occurred");
135 FFI_ERR_PANIC
136 })
137}
138
139fn get_ffi_agent() -> Option<Arc<AgentHandle>> {
143 FFI_AGENT.read().clone()
144}
145
146pub unsafe fn c_str_to_str_safe<'a>(ptr: *const c_char) -> Result<&'a str, c_int> {
156 if ptr.is_null() {
157 Ok("")
158 } else {
159 unsafe {
161 CStr::from_ptr(ptr)
162 .to_str()
163 .map_err(|_| FFI_ERR_INVALID_UTF8)
164 }
165 }
166}
167
168#[deprecated(note = "Use c_str_to_str_safe for panic-safe FFI")]
180#[allow(clippy::expect_used)] pub unsafe fn c_str_to_str<'a>(ptr: *const c_char) -> &'a str {
182 if ptr.is_null() {
183 ""
184 } else {
185 unsafe {
187 CStr::from_ptr(ptr)
188 .to_str()
189 .expect("FFI string inputs must be valid UTF-8")
190 }
191 }
192}
193
194pub unsafe fn result_to_c_int<T, E: std::fmt::Display>(
202 result: Result<T, E>,
203 fn_name: &str,
204) -> c_int {
205 match result {
206 Ok(_) => 0,
207 Err(e) => {
208 error!("FFI call {} failed: {}", fn_name, e);
209 1 }
211 }
212}
213
214pub unsafe fn malloc_and_copy_bytes(data: &[u8], out_len: *mut usize) -> *mut u8 {
224 unsafe {
226 if out_len.is_null() {
227 error!("malloc_and_copy_bytes failed: out_len pointer is null.");
228 return ptr::null_mut();
229 }
230 *out_len = data.len();
232 let ptr = libc::malloc(data.len()) as *mut u8;
234 if ptr.is_null() {
235 error!(
236 "malloc_and_copy_bytes failed: malloc returned null for size {}",
237 data.len()
238 );
239 *out_len = 0; return ptr::null_mut();
241 }
242 ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
244 ptr
245 }
246}
247
248fn malloc_and_copy_string(s: &str) -> *mut c_char {
257 match CString::new(s) {
258 Ok(c_string) => c_string.into_raw(), Err(e) => {
260 error!(
261 "malloc_and_copy_string failed: CString creation error: {}",
262 e
263 );
264 ptr::null_mut()
265 }
266 }
267}
268
269#[unsafe(no_mangle)]
280pub unsafe extern "C" fn ffi_key_exists(alias: *const c_char) -> bool {
281 let result = panic::catch_unwind(|| {
282 let alias_str = match unsafe { c_str_to_str_safe(alias) } {
283 Ok(s) => s,
284 Err(_) => return false,
285 };
286 if alias_str.is_empty() {
287 return false;
288 }
289 let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
291 Ok(kc) => kc,
292 Err(e) => {
293 error!("FFI ffi_key_exists: Failed to get platform keychain: {}", e);
294 return false;
295 }
296 };
297 let alias = KeyAlias::new_unchecked(alias_str);
298 keychain.load_key(&alias).is_ok()
299 });
300 result.unwrap_or_else(|_| {
301 error!("FFI ffi_key_exists: panic occurred");
302 false
303 })
304}
305
306#[unsafe(no_mangle)]
324pub unsafe extern "C" fn ffi_import_key(
325 alias: *const c_char, key_ptr: *const c_uchar, key_len: usize,
328 controller_did: *const c_char, passphrase: *const c_char, ) -> c_int {
331 let result = panic::catch_unwind(|| {
332 let alias_str = match unsafe { c_str_to_str_safe(alias) } {
334 Ok(s) => s,
335 Err(code) => return code,
336 };
337 let did_str = match unsafe { c_str_to_str_safe(controller_did) } {
338 Ok(s) => s,
339 Err(code) => return code,
340 };
341 let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
342 Ok(s) => s,
343 Err(code) => return code,
344 };
345 let key_data = unsafe { slice::from_raw_parts(key_ptr, key_len) };
346
347 if alias_str.is_empty()
349 || did_str.is_empty()
350 || !did_str.starts_with("did:")
351 || pass_str.is_empty()
352 {
353 error!(
354 "FFI import failed: Invalid arguments (alias='{}', did='{}', passphrase empty={}).",
355 alias_str,
356 did_str,
357 pass_str.is_empty()
358 );
359 return 1;
360 }
361
362 if let Err(e) = extract_seed_from_key_bytes(key_data) {
364 error!(
365 "FFI import failed: Provided key data is not valid Ed25519 for alias '{}': {}",
366 alias_str, e
367 );
368 return 2;
369 }
370
371 let encrypt_result = encrypt_keypair(key_data, pass_str);
373 let encrypted_key = match encrypt_result {
374 Ok(enc) => enc,
375 Err(e) => {
376 error!(
377 "FFI import failed: Encryption error for alias '{}': {}",
378 alias_str, e
379 );
380 return 4; }
382 };
383
384 #[allow(clippy::disallowed_methods)]
385 let did_string = IdentityDID::new_unchecked(did_str.to_string());
387 let alias = KeyAlias::new_unchecked(alias_str);
388
389 let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
392 Ok(kc) => kc,
393 Err(e) => {
394 error!("FFI import failed: Failed to get platform keychain: {}", e);
395 return 5; }
397 };
398 let store_result =
399 keychain.store_key(&alias, &did_string, KeyRole::Primary, &encrypted_key);
400
401 #[allow(deprecated)]
402 unsafe {
403 result_to_c_int(store_result, "ffi_import_key")
404 }
405 });
406 result.unwrap_or_else(|_| {
407 error!("FFI ffi_import_key: panic occurred");
408 FFI_ERR_PANIC
409 })
410}
411
412#[unsafe(no_mangle)]
428pub unsafe extern "C" fn ffi_rotate_key(
429 alias: *const c_char,
430 new_passphrase: *const c_char,
431) -> c_int {
432 let result = panic::catch_unwind(|| {
433 let alias_str = match unsafe { c_str_to_str_safe(alias) } {
434 Ok(s) => s,
435 Err(code) => return code,
436 };
437 let pass_str = match unsafe { c_str_to_str_safe(new_passphrase) } {
438 Ok(s) => s,
439 Err(code) => return code,
440 };
441
442 let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
445 Ok(kc) => kc,
446 Err(e) => {
447 error!("FFI rotate_key: Failed to get platform keychain: {}", e);
448 return 5; }
450 };
451 let rotate_result = rotate_key(alias_str, pass_str, keychain.as_ref());
452
453 match rotate_result {
455 Ok(()) => 0,
456 Err(e) => {
457 error!("FFI rotate_key failed for alias '{}': {}", alias_str, e);
458 match e {
459 AgentError::InvalidInput(_) => 1,
460 AgentError::KeyNotFound => 2,
461 AgentError::CryptoError(_) | AgentError::KeyDeserializationError(_) => 3,
462 _ => 4, }
464 }
465 }
466 });
467 result.unwrap_or_else(|_| {
468 error!("FFI ffi_rotate_key: panic occurred");
469 FFI_ERR_PANIC
470 })
471}
472
473#[unsafe(no_mangle)]
485pub unsafe extern "C" fn ffi_export_encrypted_key(
486 alias: *const c_char,
487 out_len: *mut usize,
488) -> *mut u8 {
489 let result = panic::catch_unwind(|| {
490 let alias_str = match unsafe { c_str_to_str_safe(alias) } {
491 Ok(s) => s,
492 Err(_) => return ptr::null_mut(),
493 };
494 if alias_str.is_empty() || out_len.is_null() {
495 if !out_len.is_null() {
496 unsafe { *out_len = 0 };
497 }
498 return ptr::null_mut();
499 }
500 unsafe { *out_len = 0 };
501
502 let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
504 Ok(kc) => kc,
505 Err(e) => {
506 error!(
507 "FFI export encrypted key: Failed to get platform keychain: {}",
508 e
509 );
510 return ptr::null_mut();
511 }
512 };
513 let alias = KeyAlias::new_unchecked(alias_str);
514 match keychain.load_key(&alias) {
515 Ok((_identity_did, _role, encrypted_data)) => {
516 debug!(
517 "FFI export encrypted key successful for alias '{}'",
518 alias_str
519 );
520 unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) }
521 }
522 Err(e) => {
523 error!(
524 "FFI export encrypted key failed for alias '{}': {}",
525 alias_str, e
526 );
527 ptr::null_mut()
528 }
529 }
530 });
531 result.unwrap_or_else(|_| {
532 error!("FFI ffi_export_encrypted_key: panic occurred");
533 ptr::null_mut()
534 })
535}
536
537#[unsafe(no_mangle)]
549pub unsafe extern "C" fn ffi_export_private_key_with_passphrase(
550 alias: *const c_char,
551 passphrase: *const c_char,
552 out_len: *mut usize,
553) -> *mut u8 {
554 let result = panic::catch_unwind(|| {
555 let alias_str = match unsafe { c_str_to_str_safe(alias) } {
556 Ok(s) => s,
557 Err(_) => return ptr::null_mut(),
558 };
559 let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
560 Ok(s) => s,
561 Err(_) => return ptr::null_mut(),
562 };
563
564 if alias_str.is_empty() || out_len.is_null() {
565 if !out_len.is_null() {
566 unsafe { *out_len = 0 };
567 }
568 return ptr::null_mut();
569 }
570 unsafe { *out_len = 0 };
571
572 let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
574 Ok(kc) => kc,
575 Err(e) => {
576 error!(
577 "FFI export_private_key_with_passphrase: Failed to get platform keychain: {}",
578 e
579 );
580 return ptr::null_mut();
581 }
582 };
583 let alias = KeyAlias::new_unchecked(alias_str);
584 let export_result = || -> Result<Vec<u8>, AgentError> {
585 if keychain.is_hardware_backend() {
586 return Err(AgentError::BackendUnavailable {
587 backend: keychain.backend_name(),
588 reason: "hardware-backed keys (e.g. Secure Enclave) cannot be exported via this FFI path".to_string(),
589 });
590 }
591 let (_controller_did, _role, encrypted_bytes) = keychain.load_key(&alias)?;
592 let _decrypted_pkcs8 = decrypt_keypair(&encrypted_bytes, pass_str)?;
594 debug!(
595 "FFI export_private_key_with_passphrase: Passphrase verified for alias '{}'",
596 alias_str
597 );
598 Ok(encrypted_bytes)
599 }();
600
601 match export_result {
602 Ok(encrypted_data) => unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) },
603 Err(e) => {
604 if !matches!(e, AgentError::IncorrectPassphrase) {
605 error!(
606 "FFI export_private_key_with_passphrase failed for alias '{}': {}",
607 alias_str, e
608 );
609 } else {
610 debug!(
611 "FFI export_private_key_with_passphrase: Incorrect passphrase for alias '{}'",
612 alias_str
613 );
614 }
615 ptr::null_mut()
616 }
617 }
618 });
619 result.unwrap_or_else(|_| {
620 error!("FFI ffi_export_private_key_with_passphrase: panic occurred");
621 ptr::null_mut()
622 })
623}
624
625#[unsafe(no_mangle)]
636pub unsafe extern "C" fn ffi_export_private_key_openssh(
637 alias: *const c_char,
638 passphrase: *const c_char,
639) -> *mut c_char {
640 let result = panic::catch_unwind(|| {
641 let alias_str = match unsafe { c_str_to_str_safe(alias) } {
642 Ok(s) => s,
643 Err(_) => return ptr::null_mut(),
644 };
645 let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
646 Ok(s) => s,
647 Err(_) => return ptr::null_mut(),
648 };
649
650 if alias_str.is_empty() {
651 return ptr::null_mut();
652 }
653
654 let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
656 Ok(kc) => kc,
657 Err(e) => {
658 error!("FFI export PEM: Failed to get platform keychain: {}", e);
659 return ptr::null_mut();
660 }
661 };
662 match export_key_openssh_pem(alias_str, pass_str, keychain.as_ref()) {
663 Ok(pem_zeroizing) => malloc_and_copy_string(pem_zeroizing.as_str()),
664 Err(e) => {
665 error!("FFI export PEM failed for alias '{}': {}", alias_str, e);
666 ptr::null_mut()
667 }
668 }
669 });
670 result.unwrap_or_else(|_| {
671 error!("FFI ffi_export_private_key_openssh: panic occurred");
672 ptr::null_mut()
673 })
674}
675
676#[unsafe(no_mangle)]
687pub unsafe extern "C" fn ffi_export_public_key_openssh(
688 alias: *const c_char,
689 passphrase: *const c_char,
690) -> *mut c_char {
691 let result = panic::catch_unwind(|| {
692 let alias_str = match unsafe { c_str_to_str_safe(alias) } {
693 Ok(s) => s,
694 Err(_) => return ptr::null_mut(),
695 };
696 let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
697 Ok(s) => s,
698 Err(_) => return ptr::null_mut(),
699 };
700
701 if alias_str.is_empty() {
702 return ptr::null_mut();
703 }
704
705 let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
707 Ok(kc) => kc,
708 Err(e) => {
709 error!(
710 "FFI export OpenSSH pubkey: Failed to get platform keychain: {}",
711 e
712 );
713 return ptr::null_mut();
714 }
715 };
716 match export_key_openssh_pub(alias_str, pass_str, keychain.as_ref()) {
717 Ok(formatted_pubkey) => malloc_and_copy_string(&formatted_pubkey),
718 Err(e) => {
719 error!(
720 "FFI export OpenSSH pubkey failed for alias '{}': {}",
721 alias_str, e
722 );
723 ptr::null_mut()
724 }
725 }
726 });
727 result.unwrap_or_else(|_| {
728 error!("FFI ffi_export_public_key_openssh: panic occurred");
729 ptr::null_mut()
730 })
731}
732
733#[unsafe(no_mangle)]
747pub unsafe extern "C" fn ffi_agent_sign(
748 pubkey_ptr: *const c_uchar,
749 pubkey_len: usize,
750 data_ptr: *const c_uchar,
751 data_len: usize,
752 out_len: *mut usize,
753) -> *mut u8 {
754 let result = panic::catch_unwind(|| {
755 if pubkey_ptr.is_null() || data_ptr.is_null() || out_len.is_null() {
756 if !out_len.is_null() {
757 unsafe { *out_len = 0 };
758 }
759 error!("FFI agent_sign failed: Null pointer argument.");
760 return ptr::null_mut();
761 }
762
763 let handle = match get_ffi_agent() {
765 Some(h) => h,
766 None => {
767 error!(
768 "FFI agent_sign failed: Agent not initialized. Call ffi_init_agent() first."
769 );
770 unsafe { *out_len = 0 };
771 return ptr::null_mut();
772 }
773 };
774
775 let pubkey_slice = unsafe { slice::from_raw_parts(pubkey_ptr, pubkey_len) };
776 let data_slice = unsafe { slice::from_raw_parts(data_ptr, data_len) };
777 unsafe { *out_len = 0 };
778
779 match agent_sign_with_handle(&handle, pubkey_slice, data_slice) {
780 Ok(signature_bytes) => unsafe { malloc_and_copy_bytes(&signature_bytes, out_len) },
781 Err(e) => {
782 error!("FFI agent_sign failed: {}", e);
783 if matches!(e, AgentError::KeyNotFound) {
784 warn!(
785 "FFI agent_sign: Key not found in agent for pubkey prefix {:x?}",
786 &pubkey_slice[..std::cmp::min(pubkey_slice.len(), 8)]
787 );
788 }
789 ptr::null_mut()
790 }
791 }
792 });
793 result.unwrap_or_else(|_| {
794 error!("FFI ffi_agent_sign: panic occurred");
795 ptr::null_mut()
796 })
797}
798
799#[unsafe(no_mangle)]
812pub unsafe extern "C" fn ffi_encrypt_data(
813 passphrase: *const c_char,
814 input_ptr: *const u8,
815 input_len: usize,
816 out_len: *mut usize,
817) -> *mut u8 {
818 let result = panic::catch_unwind(|| {
819 if input_ptr.is_null() || out_len.is_null() {
820 if !out_len.is_null() {
821 unsafe { *out_len = 0 };
822 }
823 error!("FFI encrypt_data failed: Null pointer argument.");
824 return ptr::null_mut();
825 }
826 let pass = match unsafe { c_str_to_str_safe(passphrase) } {
827 Ok(s) => s,
828 Err(_) => return ptr::null_mut(),
829 };
830 let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
831 unsafe { *out_len = 0 };
832 let algo = current_algorithm();
833
834 match encrypt_bytes(input, pass, algo) {
835 Ok(encrypted) => unsafe { malloc_and_copy_bytes(&encrypted, out_len) },
836 Err(e) => {
837 error!("FFI encrypt_data failed: {}", e);
838 ptr::null_mut()
839 }
840 }
841 });
842 result.unwrap_or_else(|_| {
843 error!("FFI ffi_encrypt_data: panic occurred");
844 ptr::null_mut()
845 })
846}
847
848#[unsafe(no_mangle)]
859pub unsafe extern "C" fn ffi_decrypt_data(
860 passphrase: *const c_char,
861 input_ptr: *const u8,
862 input_len: usize,
863 out_len: *mut usize,
864) -> *mut u8 {
865 let result = panic::catch_unwind(|| {
866 if input_ptr.is_null() || out_len.is_null() {
867 if !out_len.is_null() {
868 unsafe { *out_len = 0 };
869 }
870 error!("FFI decrypt_data failed: Null pointer argument.");
871 return ptr::null_mut();
872 }
873 let pass = match unsafe { c_str_to_str_safe(passphrase) } {
874 Ok(s) => s,
875 Err(_) => return ptr::null_mut(),
876 };
877 let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
878 unsafe { *out_len = 0 };
879
880 match decrypt_bytes(input, pass) {
881 Ok(decrypted) => unsafe { malloc_and_copy_bytes(&decrypted, out_len) },
882 Err(e) => {
883 if !matches!(e, AgentError::IncorrectPassphrase) {
884 error!("FFI decrypt_data failed: {}", e);
885 } else {
886 debug!("FFI decrypt_data: Incorrect passphrase provided.");
887 }
888 ptr::null_mut()
889 }
890 }
891 });
892 result.unwrap_or_else(|_| {
893 error!("FFI ffi_decrypt_data: panic occurred");
894 ptr::null_mut()
895 })
896}
897
898#[unsafe(no_mangle)]
907pub unsafe extern "C" fn ffi_free_str(ptr: *mut c_char) {
908 let _ = panic::catch_unwind(|| {
909 if !ptr.is_null() {
910 let _ = unsafe { CString::from_raw(ptr) };
913 }
914 });
915 }
917
918#[unsafe(no_mangle)]
928pub unsafe extern "C" fn ffi_free_bytes(ptr: *mut u8, _len: usize) {
929 let _ = panic::catch_unwind(|| {
930 if !ptr.is_null() {
931 unsafe { libc::free(ptr as *mut libc::c_void) };
932 }
933 });
934 }
936
937#[unsafe(no_mangle)]
944pub unsafe extern "C" fn ffi_set_encryption_algorithm(level: c_int) {
945 let _ = panic::catch_unwind(|| {
946 let algo = match level {
947 1 => EncryptionAlgorithm::AesGcm256,
948 2 => EncryptionAlgorithm::ChaCha20Poly1305,
949 _ => {
950 warn!(
951 "FFI: Unknown encryption level {}, defaulting to AES-GCM.",
952 level
953 );
954 EncryptionAlgorithm::AesGcm256
955 }
956 };
957 info!("FFI: Setting global encryption algorithm to {:?}", algo);
958 set_encryption_algorithm(algo);
959 });
960 }
962
963