use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::models::*;
#[cfg(target_os = "macos")]
extern "C" {
fn secure_element_list_keys(
key_name: *const std::ffi::c_char,
public_key: *const std::ffi::c_char,
) -> *mut std::ffi::c_char;
fn secure_element_check_support() -> *mut std::ffi::c_char;
fn secure_element_generate_secure_key(
key_name: *const std::ffi::c_char,
auth_mode: *const std::ffi::c_char,
) -> *mut std::ffi::c_char;
fn secure_element_sign_with_key(
key_name: *const std::ffi::c_char,
data_base64: *const std::ffi::c_char,
) -> *mut std::ffi::c_char;
fn secure_element_delete_key(
key_name: *const std::ffi::c_char,
public_key: *const std::ffi::c_char,
) -> *mut std::ffi::c_char;
}
#[cfg(target_os = "macos")]
mod ffi_helpers {
use std::ffi::CStr;
pub struct MallocGuard(*mut std::ffi::c_char);
impl MallocGuard {
pub fn new(ptr: *mut std::ffi::c_char) -> Option<Self> {
if ptr.is_null() {
None
} else {
Some(Self(ptr))
}
}
pub fn as_ptr(&self) -> *const std::ffi::c_char {
self.0
}
}
impl Drop for MallocGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
libc::free(self.0 as *mut libc::c_void);
}
}
}
}
pub unsafe fn ffi_string_to_owned(ptr: *mut std::ffi::c_char) -> crate::Result<String> {
let guard = MallocGuard::new(ptr)
.ok_or_else(|| crate::Error::Io(std::io::Error::other("FFI call returned null")))?;
let s = CStr::from_ptr(guard.as_ptr())
.to_str()
.map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid UTF-8 in FFI result: {}", e),
))
})?
.to_string();
if s.is_empty() {
return Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"FFI call returned empty string",
)));
}
Ok(s)
}
pub fn parse_ffi_response<T: serde::de::DeserializeOwned>(json: &str) -> crate::Result<T> {
let value: serde_json::Value = serde_json::from_str(json).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to parse JSON: {}", e),
))
})?;
if let Some(error_msg) = value.get("error").and_then(|v| v.as_str()) {
return Err(crate::Error::Io(std::io::Error::other(error_msg)));
}
serde_json::from_str(json).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to deserialize response: {}", e),
))
})
}
pub fn optional_to_cstring(
s: Option<&String>,
) -> (*const std::ffi::c_char, Option<std::ffi::CString>) {
match s {
Some(s) => match std::ffi::CString::new(s.as_str()) {
Ok(cstr) => {
let ptr = cstr.as_ptr();
(ptr, Some(cstr))
}
Err(_) => (std::ptr::null(), None),
},
None => (std::ptr::null(), None),
}
}
}
#[cfg(target_os = "windows")]
use crate::windows;
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<SecureElement<R>> {
Ok(SecureElement(app.clone()))
}
pub struct SecureElement<R: Runtime>(AppHandle<R>);
impl<R: Runtime> SecureElement<R> {
#[cfg(target_os = "windows")]
fn get_app_id(&self) -> String {
self.0.config().identifier.clone()
}
}
impl<R: Runtime> SecureElement<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
Ok(PingResponse {
value: payload.value,
})
}
#[cfg(target_os = "windows")]
fn get_main_window_hwnd(&self) -> Option<isize> {
use raw_window_handle::HasWindowHandle;
use tauri::Manager;
let webview_windows = self.0.webview_windows();
let window = webview_windows.values().next()?;
let handle = window.window_handle().ok()?;
windows::hwnd_from_raw(handle.as_raw())
}
pub fn generate_secure_key(
&self,
payload: GenerateSecureKeyRequest,
) -> crate::Result<GenerateSecureKeyResponse> {
#[cfg(target_os = "macos")]
{
use std::ffi::CString;
let key_name_cstr = CString::new(payload.key_name.as_str()).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid key_name: {}", e),
))
})?;
let auth_mode_str = match payload.auth_mode {
crate::models::AuthenticationMode::None => "none",
crate::models::AuthenticationMode::PinOrBiometric => "pinOrBiometric",
crate::models::AuthenticationMode::BiometricOnly => "biometricOnly",
};
let auth_mode_cstr = CString::new(auth_mode_str).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid auth_mode: {}", e),
))
})?;
let result_ptr = unsafe {
secure_element_generate_secure_key(key_name_cstr.as_ptr(), auth_mode_cstr.as_ptr())
};
let json = unsafe { ffi_helpers::ffi_string_to_owned(result_ptr)? };
ffi_helpers::parse_ffi_response(&json)
}
#[cfg(target_os = "windows")]
{
use base64::Engine;
let app_id = self.get_app_id();
let key = windows::create_key(&app_id, &payload.key_name, &payload.auth_mode)?;
let public_key_bytes = windows::export_public_key(&key)?;
let public_key = base64::engine::general_purpose::STANDARD.encode(&public_key_bytes);
let hardware_backing = match payload.auth_mode {
crate::models::AuthenticationMode::PinOrBiometric => "ngc", _ => "tpm", };
Ok(GenerateSecureKeyResponse {
key_name: payload.key_name,
public_key,
hardware_backing: hardware_backing.to_string(),
})
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let _ = payload;
Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Secure element not available on this platform",
)))
}
}
pub fn list_keys(&self, payload: ListKeysRequest) -> crate::Result<ListKeysResponse> {
#[cfg(target_os = "macos")]
{
let (key_name_ptr, _key_name_cstr) =
ffi_helpers::optional_to_cstring(payload.key_name.as_ref());
let (public_key_ptr, _public_key_cstr) =
ffi_helpers::optional_to_cstring(payload.public_key.as_ref());
let result_ptr = unsafe { secure_element_list_keys(key_name_ptr, public_key_ptr) };
let json = unsafe { ffi_helpers::ffi_string_to_owned(result_ptr)? };
ffi_helpers::parse_ffi_response(&json)
}
#[cfg(target_os = "windows")]
{
let app_id = self.get_app_id();
let keys = windows::list_keys(
&app_id,
payload.key_name.as_deref(),
payload.public_key.as_deref(),
)?;
Ok(ListKeysResponse { keys })
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let _ = payload;
Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Secure element not available on this platform",
)))
}
}
pub fn sign_with_key(&self, payload: SignWithKeyRequest) -> crate::Result<SignWithKeyResponse> {
#[cfg(target_os = "macos")]
{
use base64::Engine;
use std::ffi::CString;
let key_name_cstr = CString::new(payload.key_name.as_str()).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid key_name: {}", e),
))
})?;
let data_base64 = base64::engine::general_purpose::STANDARD.encode(&payload.data);
let data_base64_cstr = CString::new(data_base64.as_str()).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid data: {}", e),
))
})?;
let result_ptr = unsafe {
secure_element_sign_with_key(key_name_cstr.as_ptr(), data_base64_cstr.as_ptr())
};
let json = unsafe { ffi_helpers::ffi_string_to_owned(result_ptr)? };
let value: serde_json::Value = serde_json::from_str(&json).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to parse JSON: {}", e),
))
})?;
if let Some(error_msg) = value.get("error").and_then(|v| v.as_str()) {
return Err(crate::Error::Io(std::io::Error::other(error_msg)));
}
let signature_base64 =
value
.get("signature")
.and_then(|v| v.as_str())
.ok_or_else(|| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Missing signature in response",
))
})?;
let signature = base64::engine::general_purpose::STANDARD
.decode(signature_base64)
.map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to decode signature: {}", e),
))
})?;
Ok(SignWithKeyResponse { signature })
}
#[cfg(target_os = "windows")]
{
let app_id = self.get_app_id();
let (key, provider_type) = windows::open_key_auto(&app_id, &payload.key_name)?;
let hash = windows::sha256_hash(&payload.data)?;
let signature = match provider_type {
windows::KeyProviderType::Ngc => {
let hwnd = self.get_main_window_hwnd();
windows::sign_hash_with_window(&key, &hash, hwnd)?
}
windows::KeyProviderType::Tpm => {
windows::sign_hash(&key, &hash)?
}
};
Ok(SignWithKeyResponse { signature })
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let _ = payload;
Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Secure element not available on this platform",
)))
}
}
pub fn delete_key(&self, payload: DeleteKeyRequest) -> crate::Result<DeleteKeyResponse> {
#[cfg(target_os = "macos")]
{
let (key_name_ptr, _key_name_cstr) =
ffi_helpers::optional_to_cstring(payload.key_name.as_ref());
let (public_key_ptr, _public_key_cstr) =
ffi_helpers::optional_to_cstring(payload.public_key.as_ref());
let result_ptr = unsafe { secure_element_delete_key(key_name_ptr, public_key_ptr) };
let json = unsafe { ffi_helpers::ffi_string_to_owned(result_ptr)? };
let value: serde_json::Value = serde_json::from_str(&json).map_err(|e| {
crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to parse JSON: {}", e),
))
})?;
if let Some(error_msg) = value.get("error").and_then(|v| v.as_str()) {
return Err(crate::Error::Io(std::io::Error::other(error_msg)));
}
let success = value
.get("success")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(DeleteKeyResponse { success })
}
#[cfg(target_os = "windows")]
{
let app_id = self.get_app_id();
let key_name = if let Some(name) = &payload.key_name {
name.clone()
} else if let Some(public_key) = &payload.public_key {
let keys = match windows::list_keys(&app_id, None, Some(public_key)) {
Ok(keys) => keys,
Err(_) => return Ok(DeleteKeyResponse { success: true }),
};
if keys.is_empty() {
return Ok(DeleteKeyResponse { success: true });
}
keys[0].key_name.clone()
} else {
return Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Either key_name or public_key must be provided",
)));
};
let (key, _provider_type) = match windows::open_key_auto(&app_id, &key_name) {
Ok(result) => result,
Err(_) => return Ok(DeleteKeyResponse { success: true }),
};
let success = windows::delete_key(key)?;
Ok(DeleteKeyResponse { success })
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let _ = payload;
Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Secure element not available on this platform",
)))
}
}
pub fn check_secure_element_support(&self) -> crate::Result<CheckSecureElementSupportResponse> {
#[cfg(target_os = "macos")]
{
let result_ptr = unsafe { secure_element_check_support() };
let json = unsafe { ffi_helpers::ffi_string_to_owned(result_ptr)? };
ffi_helpers::parse_ffi_response(&json)
}
#[cfg(target_os = "windows")]
{
Ok(windows::get_secure_element_capabilities())
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
Ok(CheckSecureElementSupportResponse {
discrete: false,
integrated: false,
firmware: false,
emulated: false,
strongest: crate::models::SecureElementBacking::None,
can_enforce_biometric_only: false,
})
}
}
}