use std::ffi::c_void;
use std::sync::{Arc, Mutex};
use tracing::{debug, error, warn};
use zeroize::Zeroize as _;
use crate::error::SynqroError;
use crate::keychain::KeychainProvider;
type JniEnvPtr = *mut c_void;
pub type JavaKeychainFn = unsafe extern "C" fn(
env: JniEnvPtr,
service: *const u8,
service_len: usize,
account: *const u8,
account_len: usize,
secret: *const u8,
secret_len: usize,
out_buf: *mut u8,
out_cap: usize,
out_len: *mut usize,
) -> i32;
struct JniCallbacks {
load: Option<JavaKeychainFn>,
store: Option<JavaKeychainFn>,
delete: Option<JavaKeychainFn>,
}
pub struct AndroidKeychain {
jni_env: Arc<Mutex<Option<JniEnvPtr>>>,
callbacks: Arc<Mutex<JniCallbacks>>,
}
unsafe impl Send for AndroidKeychain {}
unsafe impl Sync for AndroidKeychain {}
impl AndroidKeychain {
pub fn new() -> Result<Self, SynqroError> {
debug!("AndroidKeychain: constructed without JNI env (JNI setup required)");
Ok(Self {
jni_env: Arc::new(Mutex::new(None)),
callbacks: Arc::new(Mutex::new(JniCallbacks {
load: None,
store: None,
delete: None,
})),
})
}
pub fn with_jni_env(jni_env: *mut c_void) -> Result<Self, SynqroError> {
if jni_env.is_null() {
error!("AndroidKeychain::with_jni_env called with null JNIEnv pointer");
return Err(SynqroError::InvalidInput(
"jni_env pointer must not be null".into(),
));
}
debug!("AndroidKeychain: JNI env registered");
Ok(Self {
jni_env: Arc::new(Mutex::new(Some(jni_env))),
callbacks: Arc::new(Mutex::new(JniCallbacks {
load: None,
store: None,
delete: None,
})),
})
}
pub fn register_callbacks(
&self,
load_fn: JavaKeychainFn,
store_fn: JavaKeychainFn,
delete_fn: JavaKeychainFn,
) -> Result<(), SynqroError> {
let mut cbs = self.callbacks.lock().map_err(|_| {
SynqroError::Internal("Android Keychain callback mutex poisoned".into())
})?;
cbs.load = Some(load_fn);
cbs.store = Some(store_fn);
cbs.delete = Some(delete_fn);
debug!("AndroidKeychain: JNI callbacks registered");
Ok(())
}
fn jni_env_or_err(&self) -> Result<JniEnvPtr, SynqroError> {
let guard = self
.jni_env
.lock()
.map_err(|_| SynqroError::Internal("Android Keychain JNIEnv mutex poisoned".into()))?;
guard.ok_or_else(|| {
SynqroError::Keychain(
"Android Keystore: JNI environment not configured. \
Call AndroidKeychain::with_jni_env from the Java layer before use."
.to_owned(),
)
})
}
}
impl KeychainProvider for AndroidKeychain {
fn load_secret(&self, service: &str, account: &str) -> Result<Vec<u8>, SynqroError> {
let env = self.jni_env_or_err()?;
let cbs = self.callbacks.lock().map_err(|_| {
SynqroError::Internal("Android Keychain callback mutex poisoned".into())
})?;
let load_fn = cbs.load.ok_or_else(|| {
SynqroError::Keychain("Android Keystore: load JNI callback not registered".to_owned())
})?;
const MAX_SECRET_LEN: usize = 4096;
let mut out_buf = vec![0u8; MAX_SECRET_LEN];
let mut out_len: usize = 0;
let ret = unsafe {
load_fn(
env,
service.as_ptr(),
service.len(),
account.as_ptr(),
account.len(),
std::ptr::null(), 0,
out_buf.as_mut_ptr(),
MAX_SECRET_LEN,
&mut out_len,
)
};
if ret != 0 {
out_buf.zeroize();
return Err(SynqroError::Keychain(format!(
"Android Keystore: load callback returned error code {}",
ret
)));
}
let out_len = out_len.min(MAX_SECRET_LEN);
let result = out_buf[..out_len].to_vec();
out_buf.zeroize();
debug!(
service = service,
account = account,
"Android Keystore: secret loaded via JNI"
);
Ok(result)
}
fn store_secret(&self, service: &str, account: &str, secret: &[u8]) -> Result<(), SynqroError> {
let env = self.jni_env_or_err()?;
let cbs = self.callbacks.lock().map_err(|_| {
SynqroError::Internal("Android Keychain callback mutex poisoned".into())
})?;
let store_fn = cbs.store.ok_or_else(|| {
SynqroError::Keychain("Android Keystore: store JNI callback not registered".to_owned())
})?;
let ret = unsafe {
store_fn(
env,
service.as_ptr(),
service.len(),
account.as_ptr(),
account.len(),
secret.as_ptr(),
secret.len(),
std::ptr::null_mut(), 0,
std::ptr::null_mut(),
)
};
if ret != 0 {
return Err(SynqroError::Keychain(format!(
"Android Keystore: store callback returned error code {}",
ret
)));
}
debug!(
service = service,
account = account,
"Android Keystore: secret stored via JNI"
);
Ok(())
}
fn delete_secret(&self, service: &str, account: &str) -> Result<(), SynqroError> {
let env = self.jni_env_or_err()?;
let cbs = self.callbacks.lock().map_err(|_| {
SynqroError::Internal("Android Keychain callback mutex poisoned".into())
})?;
let delete_fn = cbs.delete.ok_or_else(|| {
SynqroError::Keychain("Android Keystore: delete JNI callback not registered".to_owned())
})?;
let ret = unsafe {
delete_fn(
env,
service.as_ptr(),
service.len(),
account.as_ptr(),
account.len(),
std::ptr::null(), 0,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
)
};
if ret != 0 {
return Err(SynqroError::Keychain(format!(
"Android Keystore: delete callback returned error code {}",
ret
)));
}
debug!(
service = service,
account = account,
"Android Keystore: secret deleted via JNI"
);
Ok(())
}
}