use crate::agent::AgentHandle;
use crate::api::runtime::{
agent_sign_with_handle, export_key_openssh_pem, export_key_openssh_pub, rotate_key,
};
use crate::config::EnvironmentConfig;
use crate::config::{current_algorithm, set_encryption_algorithm};
use crate::crypto::EncryptionAlgorithm;
use crate::crypto::encryption::{decrypt_bytes, encrypt_bytes};
use crate::crypto::signer::extract_seed_from_key_bytes;
use crate::crypto::signer::{decrypt_keypair, encrypt_keypair};
use crate::error::AgentError;
use crate::storage::keychain::{
IdentityDID, KeyAlias, KeyRole, KeyStorage, get_platform_keychain_with_config,
};
use log::{debug, error, info, warn};
use parking_lot::RwLock;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_uchar};
use std::panic;
use std::path::PathBuf;
use std::ptr;
use std::slice;
use std::sync::{Arc, LazyLock};
pub const FFI_OK: c_int = 0;
pub const FFI_ERR_INVALID_UTF8: c_int = -1;
pub const FFI_ERR_AGENT_NOT_INITIALIZED: c_int = -2;
pub const FFI_ERR_PANIC: c_int = -127;
static FFI_AGENT: LazyLock<RwLock<Option<Arc<AgentHandle>>>> = LazyLock::new(|| RwLock::new(None));
#[unsafe(no_mangle)]
#[allow(clippy::disallowed_methods)] pub unsafe extern "C" fn ffi_init_agent(socket_path: *const c_char) -> c_int {
let result = panic::catch_unwind(|| {
let path_str = match unsafe { c_str_to_str_safe(socket_path) } {
Ok(s) if !s.is_empty() => s,
Ok(_) => {
let home = match dirs::home_dir() {
Some(h) => h,
None => {
error!("FFI ffi_init_agent: Could not determine home directory");
return 1;
}
};
let default_path = home.join(".auths").join("agent.sock");
let handle = Arc::new(AgentHandle::new(default_path));
let mut guard = FFI_AGENT.write();
*guard = Some(handle);
info!("FFI agent initialized with default socket path");
return 0;
}
Err(code) => return code,
};
let socket = PathBuf::from(path_str);
let handle = Arc::new(AgentHandle::new(socket));
let mut guard = FFI_AGENT.write();
*guard = Some(handle);
info!("FFI agent initialized with socket path: {}", path_str);
0
});
result.unwrap_or_else(|_| {
error!("FFI ffi_init_agent: panic occurred");
FFI_ERR_PANIC
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_shutdown_agent() -> c_int {
let result = panic::catch_unwind(|| {
let mut guard = FFI_AGENT.write();
if let Some(handle) = guard.take() {
if let Err(e) = handle.shutdown() {
warn!("FFI ffi_shutdown_agent: Shutdown returned error: {}", e);
}
info!("FFI agent shut down");
} else {
debug!("FFI ffi_shutdown_agent: Agent was not initialized");
}
0
});
result.unwrap_or_else(|_| {
error!("FFI ffi_shutdown_agent: panic occurred");
FFI_ERR_PANIC
})
}
fn get_ffi_agent() -> Option<Arc<AgentHandle>> {
FFI_AGENT.read().clone()
}
pub unsafe fn c_str_to_str_safe<'a>(ptr: *const c_char) -> Result<&'a str, c_int> {
if ptr.is_null() {
Ok("")
} else {
unsafe {
CStr::from_ptr(ptr)
.to_str()
.map_err(|_| FFI_ERR_INVALID_UTF8)
}
}
}
#[deprecated(note = "Use c_str_to_str_safe for panic-safe FFI")]
#[allow(clippy::expect_used)] pub unsafe fn c_str_to_str<'a>(ptr: *const c_char) -> &'a str {
if ptr.is_null() {
""
} else {
unsafe {
CStr::from_ptr(ptr)
.to_str()
.expect("FFI string inputs must be valid UTF-8")
}
}
}
pub unsafe fn result_to_c_int<T, E: std::fmt::Display>(
result: Result<T, E>,
fn_name: &str,
) -> c_int {
match result {
Ok(_) => 0,
Err(e) => {
error!("FFI call {} failed: {}", fn_name, e);
1 }
}
}
pub unsafe fn malloc_and_copy_bytes(data: &[u8], out_len: *mut usize) -> *mut u8 {
unsafe {
if out_len.is_null() {
error!("malloc_and_copy_bytes failed: out_len pointer is null.");
return ptr::null_mut();
}
*out_len = data.len();
let ptr = libc::malloc(data.len()) as *mut u8;
if ptr.is_null() {
error!(
"malloc_and_copy_bytes failed: malloc returned null for size {}",
data.len()
);
*out_len = 0; return ptr::null_mut();
}
ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
ptr
}
}
fn malloc_and_copy_string(s: &str) -> *mut c_char {
match CString::new(s) {
Ok(c_string) => c_string.into_raw(), Err(e) => {
error!(
"malloc_and_copy_string failed: CString creation error: {}",
e
);
ptr::null_mut()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_key_exists(alias: *const c_char) -> bool {
let result = panic::catch_unwind(|| {
let alias_str = match unsafe { c_str_to_str_safe(alias) } {
Ok(s) => s,
Err(_) => return false,
};
if alias_str.is_empty() {
return false;
}
let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
Ok(kc) => kc,
Err(e) => {
error!("FFI ffi_key_exists: Failed to get platform keychain: {}", e);
return false;
}
};
let alias = KeyAlias::new_unchecked(alias_str);
keychain.load_key(&alias).is_ok()
});
result.unwrap_or_else(|_| {
error!("FFI ffi_key_exists: panic occurred");
false
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_import_key(
alias: *const c_char, key_ptr: *const c_uchar, key_len: usize,
controller_did: *const c_char, passphrase: *const c_char, ) -> c_int {
let result = panic::catch_unwind(|| {
let alias_str = match unsafe { c_str_to_str_safe(alias) } {
Ok(s) => s,
Err(code) => return code,
};
let did_str = match unsafe { c_str_to_str_safe(controller_did) } {
Ok(s) => s,
Err(code) => return code,
};
let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
Ok(s) => s,
Err(code) => return code,
};
let key_data = unsafe { slice::from_raw_parts(key_ptr, key_len) };
if alias_str.is_empty()
|| did_str.is_empty()
|| !did_str.starts_with("did:")
|| pass_str.is_empty()
{
error!(
"FFI import failed: Invalid arguments (alias='{}', did='{}', passphrase empty={}).",
alias_str,
did_str,
pass_str.is_empty()
);
return 1;
}
if let Err(e) = extract_seed_from_key_bytes(key_data) {
error!(
"FFI import failed: Provided key data is not valid Ed25519 for alias '{}': {}",
alias_str, e
);
return 2;
}
let encrypt_result = encrypt_keypair(key_data, pass_str);
let encrypted_key = match encrypt_result {
Ok(enc) => enc,
Err(e) => {
error!(
"FFI import failed: Encryption error for alias '{}': {}",
alias_str, e
);
return 4; }
};
#[allow(clippy::disallowed_methods)]
let did_string = IdentityDID::new_unchecked(did_str.to_string());
let alias = KeyAlias::new_unchecked(alias_str);
let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
Ok(kc) => kc,
Err(e) => {
error!("FFI import failed: Failed to get platform keychain: {}", e);
return 5; }
};
let store_result =
keychain.store_key(&alias, &did_string, KeyRole::Primary, &encrypted_key);
#[allow(deprecated)]
unsafe {
result_to_c_int(store_result, "ffi_import_key")
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_import_key: panic occurred");
FFI_ERR_PANIC
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_rotate_key(
alias: *const c_char,
new_passphrase: *const c_char,
) -> c_int {
let result = panic::catch_unwind(|| {
let alias_str = match unsafe { c_str_to_str_safe(alias) } {
Ok(s) => s,
Err(code) => return code,
};
let pass_str = match unsafe { c_str_to_str_safe(new_passphrase) } {
Ok(s) => s,
Err(code) => return code,
};
let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
Ok(kc) => kc,
Err(e) => {
error!("FFI rotate_key: Failed to get platform keychain: {}", e);
return 5; }
};
let rotate_result = rotate_key(alias_str, pass_str, keychain.as_ref());
match rotate_result {
Ok(()) => 0,
Err(e) => {
error!("FFI rotate_key failed for alias '{}': {}", alias_str, e);
match e {
AgentError::InvalidInput(_) => 1,
AgentError::KeyNotFound => 2,
AgentError::CryptoError(_) | AgentError::KeyDeserializationError(_) => 3,
_ => 4, }
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_rotate_key: panic occurred");
FFI_ERR_PANIC
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_export_encrypted_key(
alias: *const c_char,
out_len: *mut usize,
) -> *mut u8 {
let result = panic::catch_unwind(|| {
let alias_str = match unsafe { c_str_to_str_safe(alias) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
if alias_str.is_empty() || out_len.is_null() {
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
return ptr::null_mut();
}
unsafe { *out_len = 0 };
let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
Ok(kc) => kc,
Err(e) => {
error!(
"FFI export encrypted key: Failed to get platform keychain: {}",
e
);
return ptr::null_mut();
}
};
let alias = KeyAlias::new_unchecked(alias_str);
match keychain.load_key(&alias) {
Ok((_identity_did, _role, encrypted_data)) => {
debug!(
"FFI export encrypted key successful for alias '{}'",
alias_str
);
unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) }
}
Err(e) => {
error!(
"FFI export encrypted key failed for alias '{}': {}",
alias_str, e
);
ptr::null_mut()
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_export_encrypted_key: panic occurred");
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_export_private_key_with_passphrase(
alias: *const c_char,
passphrase: *const c_char,
out_len: *mut usize,
) -> *mut u8 {
let result = panic::catch_unwind(|| {
let alias_str = match unsafe { c_str_to_str_safe(alias) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
if alias_str.is_empty() || out_len.is_null() {
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
return ptr::null_mut();
}
unsafe { *out_len = 0 };
let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
Ok(kc) => kc,
Err(e) => {
error!(
"FFI export_private_key_with_passphrase: Failed to get platform keychain: {}",
e
);
return ptr::null_mut();
}
};
let alias = KeyAlias::new_unchecked(alias_str);
let export_result = || -> Result<Vec<u8>, AgentError> {
if keychain.is_hardware_backend() {
return Err(AgentError::BackendUnavailable {
backend: keychain.backend_name(),
reason: "hardware-backed keys (e.g. Secure Enclave) cannot be exported via this FFI path".to_string(),
});
}
let (_controller_did, _role, encrypted_bytes) = keychain.load_key(&alias)?;
let _decrypted_pkcs8 = decrypt_keypair(&encrypted_bytes, pass_str)?;
debug!(
"FFI export_private_key_with_passphrase: Passphrase verified for alias '{}'",
alias_str
);
Ok(encrypted_bytes)
}();
match export_result {
Ok(encrypted_data) => unsafe { malloc_and_copy_bytes(&encrypted_data, out_len) },
Err(e) => {
if !matches!(e, AgentError::IncorrectPassphrase) {
error!(
"FFI export_private_key_with_passphrase failed for alias '{}': {}",
alias_str, e
);
} else {
debug!(
"FFI export_private_key_with_passphrase: Incorrect passphrase for alias '{}'",
alias_str
);
}
ptr::null_mut()
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_export_private_key_with_passphrase: panic occurred");
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_export_private_key_openssh(
alias: *const c_char,
passphrase: *const c_char,
) -> *mut c_char {
let result = panic::catch_unwind(|| {
let alias_str = match unsafe { c_str_to_str_safe(alias) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
if alias_str.is_empty() {
return ptr::null_mut();
}
let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
Ok(kc) => kc,
Err(e) => {
error!("FFI export PEM: Failed to get platform keychain: {}", e);
return ptr::null_mut();
}
};
match export_key_openssh_pem(alias_str, pass_str, keychain.as_ref()) {
Ok(pem_zeroizing) => malloc_and_copy_string(pem_zeroizing.as_str()),
Err(e) => {
error!("FFI export PEM failed for alias '{}': {}", alias_str, e);
ptr::null_mut()
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_export_private_key_openssh: panic occurred");
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_export_public_key_openssh(
alias: *const c_char,
passphrase: *const c_char,
) -> *mut c_char {
let result = panic::catch_unwind(|| {
let alias_str = match unsafe { c_str_to_str_safe(alias) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let pass_str = match unsafe { c_str_to_str_safe(passphrase) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
if alias_str.is_empty() {
return ptr::null_mut();
}
let keychain = match get_platform_keychain_with_config(&EnvironmentConfig::from_env()) {
Ok(kc) => kc,
Err(e) => {
error!(
"FFI export OpenSSH pubkey: Failed to get platform keychain: {}",
e
);
return ptr::null_mut();
}
};
match export_key_openssh_pub(alias_str, pass_str, keychain.as_ref()) {
Ok(formatted_pubkey) => malloc_and_copy_string(&formatted_pubkey),
Err(e) => {
error!(
"FFI export OpenSSH pubkey failed for alias '{}': {}",
alias_str, e
);
ptr::null_mut()
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_export_public_key_openssh: panic occurred");
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_agent_sign(
pubkey_ptr: *const c_uchar,
pubkey_len: usize,
data_ptr: *const c_uchar,
data_len: usize,
out_len: *mut usize,
) -> *mut u8 {
let result = panic::catch_unwind(|| {
if pubkey_ptr.is_null() || data_ptr.is_null() || out_len.is_null() {
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
error!("FFI agent_sign failed: Null pointer argument.");
return ptr::null_mut();
}
let handle = match get_ffi_agent() {
Some(h) => h,
None => {
error!(
"FFI agent_sign failed: Agent not initialized. Call ffi_init_agent() first."
);
unsafe { *out_len = 0 };
return ptr::null_mut();
}
};
let pubkey_slice = unsafe { slice::from_raw_parts(pubkey_ptr, pubkey_len) };
let data_slice = unsafe { slice::from_raw_parts(data_ptr, data_len) };
unsafe { *out_len = 0 };
match agent_sign_with_handle(&handle, pubkey_slice, data_slice) {
Ok(signature_bytes) => unsafe { malloc_and_copy_bytes(&signature_bytes, out_len) },
Err(e) => {
error!("FFI agent_sign failed: {}", e);
if matches!(e, AgentError::KeyNotFound) {
warn!(
"FFI agent_sign: Key not found in agent for pubkey prefix {:x?}",
&pubkey_slice[..std::cmp::min(pubkey_slice.len(), 8)]
);
}
ptr::null_mut()
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_agent_sign: panic occurred");
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_encrypt_data(
passphrase: *const c_char,
input_ptr: *const u8,
input_len: usize,
out_len: *mut usize,
) -> *mut u8 {
let result = panic::catch_unwind(|| {
if input_ptr.is_null() || out_len.is_null() {
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
error!("FFI encrypt_data failed: Null pointer argument.");
return ptr::null_mut();
}
let pass = match unsafe { c_str_to_str_safe(passphrase) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
unsafe { *out_len = 0 };
let algo = current_algorithm();
match encrypt_bytes(input, pass, algo) {
Ok(encrypted) => unsafe { malloc_and_copy_bytes(&encrypted, out_len) },
Err(e) => {
error!("FFI encrypt_data failed: {}", e);
ptr::null_mut()
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_encrypt_data: panic occurred");
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_decrypt_data(
passphrase: *const c_char,
input_ptr: *const u8,
input_len: usize,
out_len: *mut usize,
) -> *mut u8 {
let result = panic::catch_unwind(|| {
if input_ptr.is_null() || out_len.is_null() {
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
error!("FFI decrypt_data failed: Null pointer argument.");
return ptr::null_mut();
}
let pass = match unsafe { c_str_to_str_safe(passphrase) } {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let input = unsafe { slice::from_raw_parts(input_ptr, input_len) };
unsafe { *out_len = 0 };
match decrypt_bytes(input, pass) {
Ok(decrypted) => unsafe { malloc_and_copy_bytes(&decrypted, out_len) },
Err(e) => {
if !matches!(e, AgentError::IncorrectPassphrase) {
error!("FFI decrypt_data failed: {}", e);
} else {
debug!("FFI decrypt_data: Incorrect passphrase provided.");
}
ptr::null_mut()
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_decrypt_data: panic occurred");
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_free_str(ptr: *mut c_char) {
let _ = panic::catch_unwind(|| {
if !ptr.is_null() {
let _ = unsafe { CString::from_raw(ptr) };
}
});
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_free_bytes(ptr: *mut u8, _len: usize) {
let _ = panic::catch_unwind(|| {
if !ptr.is_null() {
unsafe { libc::free(ptr as *mut libc::c_void) };
}
});
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_set_encryption_algorithm(level: c_int) {
let _ = panic::catch_unwind(|| {
let algo = match level {
1 => EncryptionAlgorithm::AesGcm256,
2 => EncryptionAlgorithm::ChaCha20Poly1305,
_ => {
warn!(
"FFI: Unknown encryption level {}, defaulting to AES-GCM.",
level
);
EncryptionAlgorithm::AesGcm256
}
};
info!("FFI: Setting global encryption algorithm to {:?}", algo);
set_encryption_algorithm(algo);
});
}