//! Privacy Core skeleton for the ShadowBroker / Infonet migration. //! //! Sprint 1 scope is intentionally narrow: //! - keep private protocol state opaque to Python //! - expose only handle-based FFI //! - prove the repo has a single Rust home for MLS group operations //! - use in-memory provider/storage only for now //! //! This crate follows the architecture docs in `extra/docs-internal/` and keeps //! group/session state on the Rust side. Persistent storage is deferred to a //! later sprint. use std::collections::{hash_map::DefaultHasher, HashMap, VecDeque}; use std::hash::{Hash, Hasher}; use std::panic::{self, AssertUnwindSafe}; use std::slice; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; use mls_rs::client_builder::{BaseConfig, WithCryptoProvider, WithIdentityProvider}; use mls_rs::group::{Group, ReceivedMessage}; use mls_rs::identity::{ basic::{BasicCredential, BasicIdentityProvider}, SigningIdentity, }; use mls_rs::mls_rs_codec::{MlsDecode, MlsEncode}; use mls_rs::{ CipherSuite, CipherSuiteProvider, Client, CryptoProvider, ExtensionList, GroupStateStorage, MlsMessage, }; use mls_rs_core::crypto::SignatureSecretKey; use mls_rs_core::group::GroupState as MlsGroupState; use mls_rs_crypto_rustcrypto::RustCryptoProvider; use serde::Serialize; use sha2::{Digest, Sha256}; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; use zeroize::Zeroizing; type IdentityHandle = u64; type KeyPackageHandle = u64; type GroupHandle = u64; type CommitHandle = u64; type DMSessionHandle = u64; type FamilyId = u64; type MemberRef = u32; type PrivacyConfig = WithIdentityProvider>; type PrivacyClient = Client; type PrivacyGroup = Group; const CIPHER_SUITE: CipherSuite = CipherSuite::CURVE25519_AES128; const VERSION: &str = concat!("privacy-core/", env!("CARGO_PKG_VERSION")); const MAX_KEY_PACKAGE_SIZE: usize = 65_536; const MAX_DM_PLAINTEXT_SIZE: usize = 65_536; const MAX_GROUP_PLAINTEXT_SIZE: usize = 65_536; const MAX_IDENTITIES: usize = 1_024; const MAX_GROUPS: usize = 512; const MAX_DM_SESSIONS: usize = 512; const MAX_PENDING_DM_OUTPUTS: usize = 256; const PENDING_DM_OUTPUT_TTL: Duration = Duration::from_secs(10); #[repr(C)] pub struct ByteBuffer { pub data: *mut u8, pub len: usize, } impl ByteBuffer { fn empty() -> Self { Self { data: std::ptr::null_mut(), len: 0, } } } #[derive(Clone)] struct IdentityState { client: PrivacyClient, signing_identity: SigningIdentity, label: Vec, signer_secret_bytes: Vec, } #[derive(Clone)] struct KeyPackageState { message: MlsMessage, owner_identity: Option, } struct GroupState { family_id: FamilyId, owner_identity: IdentityHandle, group: PrivacyGroup, } struct CommitState { family_id: FamilyId, commit_message: Vec, welcome_messages: Vec>, joined_group_handles: Vec, } struct DMSessionState { owner_identity: IdentityHandle, group: PrivacyGroup, welcome_message: Vec, } #[derive(Serialize)] struct PublicBundle { label: String, cipher_suite: &'static str, signing_public_key: Vec, credential: Vec, } #[derive(Serialize)] struct HandleStats { identities: usize, groups: usize, dm_sessions: usize, max_identities: usize, max_groups: usize, max_dm_sessions: usize, } // Monotonic counter starting at 1. Handle 0 is the FFI error sentinel. // Wraparound at 2^64 is not handled and is assumed unreachable in practice. static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1); static NEXT_FAMILY_ID: AtomicU64 = AtomicU64::new(1); static LAST_ERROR: OnceLock> = OnceLock::new(); static IDENTITIES: OnceLock>> = OnceLock::new(); static KEY_PACKAGES: OnceLock>> = OnceLock::new(); static GROUPS: OnceLock>> = OnceLock::new(); static COMMITS: OnceLock>> = OnceLock::new(); static DM_SESSIONS: OnceLock>> = OnceLock::new(); static FAMILIES: OnceLock>>> = OnceLock::new(); static EXPORTED_KEY_PACKAGES: OnceLock, IdentityHandle>>> = OnceLock::new(); static PENDING_DM_OUTPUTS: OnceLock, Instant)>>> = OnceLock::new(); static PENDING_DM_OUTPUT_LOOKUPS: OnceLock< Mutex>>, > = OnceLock::new(); static PENDING_DM_OUTPUT_COUNTERS: OnceLock>> = OnceLock::new(); fn identities() -> &'static Mutex> { IDENTITIES.get_or_init(|| Mutex::new(HashMap::new())) } fn key_packages() -> &'static Mutex> { KEY_PACKAGES.get_or_init(|| Mutex::new(HashMap::new())) } fn groups() -> &'static Mutex> { GROUPS.get_or_init(|| Mutex::new(HashMap::new())) } fn commits() -> &'static Mutex> { COMMITS.get_or_init(|| Mutex::new(HashMap::new())) } fn dm_sessions() -> &'static Mutex> { DM_SESSIONS.get_or_init(|| Mutex::new(HashMap::new())) } fn families() -> &'static Mutex>> { FAMILIES.get_or_init(|| Mutex::new(HashMap::new())) } fn exported_key_packages() -> &'static Mutex, IdentityHandle>> { EXPORTED_KEY_PACKAGES.get_or_init(|| Mutex::new(HashMap::new())) } fn pending_dm_outputs() -> &'static Mutex, Instant)>> { PENDING_DM_OUTPUTS.get_or_init(|| Mutex::new(HashMap::new())) } fn pending_dm_output_lookups() -> &'static Mutex>> { PENDING_DM_OUTPUT_LOOKUPS.get_or_init(|| Mutex::new(HashMap::new())) } fn pending_dm_output_counters() -> &'static Mutex> { PENDING_DM_OUTPUT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new())) } fn last_error() -> &'static Mutex { LAST_ERROR.get_or_init(|| Mutex::new(String::new())) } fn next_handle() -> u64 { NEXT_HANDLE.fetch_add(1, Ordering::Relaxed) } fn next_family_id() -> u64 { NEXT_FAMILY_ID.fetch_add(1, Ordering::Relaxed) } fn set_last_error(message: impl Into) { *last_error().lock().expect("last error mutex poisoned") = message.into(); } fn clear_last_error() { set_last_error(""); } fn wipe_bytes(bytes: &mut [u8]) { bytes.fill(0); } fn wipe_vec(bytes: &mut Vec) { if !bytes.is_empty() { wipe_bytes(bytes.as_mut_slice()); } } fn to_buffer(mut bytes: Vec) -> ByteBuffer { if bytes.is_empty() { return ByteBuffer::empty(); } let len = bytes.len(); let ptr = bytes.as_mut_ptr(); std::mem::forget(bytes); ByteBuffer { data: ptr, len } } fn from_buffer(buffer: ByteBuffer) { if buffer.data.is_null() || buffer.len == 0 { return; } unsafe { let mut bytes = Vec::from_raw_parts(buffer.data, buffer.len, buffer.len); wipe_vec(&mut bytes); } } fn bytes_from_raw<'a>(ptr: *const u8, len: usize) -> Result<&'a [u8], String> { // SAFETY: len is checked before ptr is dereferenced. Do not reorder these checks. if len == 0 { return Ok(&[]); } if ptr.is_null() { return Err("received null pointer for non-empty buffer".to_string()); } Ok(unsafe { slice::from_raw_parts(ptr, len) }) } fn map_err(err: E) -> String { err.to_string() } #[cfg(target_arch = "wasm32")] fn wasm_handles_from_json(json: &str) -> Result, String> { let trimmed = json.trim(); if trimmed.is_empty() { return Ok(Vec::new()); } serde_json::from_str::>(trimmed).map_err(map_err) } fn make_client(label: &[u8]) -> Result<(PrivacyClient, SigningIdentity, Vec), String> { let crypto_provider = RustCryptoProvider::default(); let cipher_suite_provider = crypto_provider .cipher_suite_provider(CIPHER_SUITE) .ok_or_else(|| "cipher suite is not supported by RustCrypto provider".to_string())?; let (secret, public) = cipher_suite_provider .signature_key_generate() .map_err(map_err)?; let signer_bytes = secret.as_bytes().to_vec(); let credential = BasicCredential::new(label.to_vec()); let signing_identity = SigningIdentity::new(credential.into_credential(), public); let client = Client::builder() .identity_provider(BasicIdentityProvider::new()) .crypto_provider(crypto_provider) .signing_identity(signing_identity.clone(), secret, CIPHER_SUITE) .build(); Ok((client, signing_identity, signer_bytes)) } fn make_client_from_parts( label: &[u8], signing_identity: SigningIdentity, signer_secret_bytes: &[u8], ) -> Result { let crypto_provider = RustCryptoProvider::default(); let secret = SignatureSecretKey::new(signer_secret_bytes.to_vec()); let client = Client::builder() .identity_provider(BasicIdentityProvider::new()) .crypto_provider(crypto_provider) .signing_identity(signing_identity, secret, CIPHER_SUITE) .build(); Ok(client) } fn family_handles(family_id: FamilyId) -> Vec { families() .lock() .expect("families mutex poisoned") .get(&family_id) .cloned() .unwrap_or_default() } fn register_group_handle( family_id: FamilyId, owner_identity: IdentityHandle, group: PrivacyGroup, ) -> Result { let handle = next_handle(); let mut groups_guard = groups().lock().expect("groups mutex poisoned"); if groups_guard.len() >= MAX_GROUPS && !groups_guard.contains_key(&handle) { return Err("maximum group limit reached".to_string()); } groups_guard.insert( handle, GroupState { family_id, owner_identity, group, }, ); drop(groups_guard); families() .lock() .expect("families mutex poisoned") .entry(family_id) .or_default() .push(handle); Ok(handle) } fn process_commit_for_family( family_id: FamilyId, commit_message: &MlsMessage, actor_handle: GroupHandle, skip_handles: &[GroupHandle], ) -> Result<(), String> { let handles = family_handles(family_id); let mut groups_guard = groups().lock().expect("groups mutex poisoned"); for handle in handles { if handle == actor_handle || skip_handles.contains(&handle) { continue; } if let Some(state) = groups_guard.get_mut(&handle) { state .group .process_incoming_message(commit_message.clone()) .map_err(map_err)?; } } Ok(()) } fn remove_group_handles(handles_to_remove: &[GroupHandle]) { let mut groups_guard = groups().lock().expect("groups mutex poisoned"); let mut families_guard = families().lock().expect("families mutex poisoned"); for handle in handles_to_remove { if let Some(state) = groups_guard.remove(handle) { if let Some(entries) = families_guard.get_mut(&state.family_id) { entries.retain(|candidate| candidate != handle); } } } } pub fn create_identity() -> Result { let handle = next_handle(); let label = format!("identity-{handle}").into_bytes(); let (client, signing_identity, signer_secret_bytes) = make_client(&label)?; let mut guard = identities().lock().expect("identities mutex poisoned"); if guard.len() >= MAX_IDENTITIES { return Err("identity limit reached".to_string()); } guard.insert( handle, IdentityState { client, signing_identity, label, signer_secret_bytes, }, ); Ok(handle) } pub fn export_key_package(identity: IdentityHandle) -> Result, String> { let identities_guard = identities().lock().expect("identities mutex poisoned"); let identity_state = identities_guard .get(&identity) .ok_or_else(|| format!("unknown identity handle: {identity}"))?; let message = identity_state .client .generate_key_package_message(Default::default(), Default::default(), None) .map_err(map_err)?; let bytes = message.mls_encode_to_vec().map_err(map_err)?; drop(identities_guard); exported_key_packages() .lock() .expect("key package export mutex poisoned") .insert(bytes.clone(), identity); Ok(bytes) } pub fn import_key_package(data: &[u8]) -> Result { if data.len() > MAX_KEY_PACKAGE_SIZE { return Err(format!( "key package exceeds maximum size: {} > {} bytes", data.len(), MAX_KEY_PACKAGE_SIZE )); } let mut cursor = data; let message = MlsMessage::mls_decode(&mut cursor).map_err(map_err)?; let owner_identity = exported_key_packages() .lock() .expect("key package export mutex poisoned") .get(data) .copied(); let handle = next_handle(); key_packages() .lock() .expect("key packages mutex poisoned") .insert( handle, KeyPackageState { message, owner_identity, }, ); Ok(handle) } pub fn create_group(creator: IdentityHandle) -> Result { let identities_guard = identities().lock().expect("identities mutex poisoned"); let identity_state = identities_guard .get(&creator) .ok_or_else(|| format!("unknown identity handle: {creator}"))?; let group = identity_state .client .create_group(ExtensionList::default(), Default::default(), None) .map_err(map_err)?; drop(identities_guard); let family_id = next_family_id(); let handle = next_handle(); let mut groups_guard = groups().lock().expect("groups mutex poisoned"); if groups_guard.len() >= MAX_GROUPS { return Err("group limit reached".to_string()); } groups_guard.insert( handle, GroupState { family_id, owner_identity: creator, group, }, ); drop(groups_guard); families() .lock() .expect("families mutex poisoned") .entry(family_id) .or_default() .push(handle); Ok(handle) } pub fn add_member( group_handle: GroupHandle, key_package: KeyPackageHandle, ) -> Result { let package_state = key_packages() .lock() .expect("key packages mutex poisoned") .get(&key_package) .cloned() .ok_or_else(|| format!("unknown key package handle: {key_package}"))?; let family_id = { let groups_guard = groups().lock().expect("groups mutex poisoned"); groups_guard .get(&group_handle) .map(|state| state.family_id) .ok_or_else(|| format!("unknown group handle: {group_handle}"))? }; let commit_output = { let mut groups_guard = groups().lock().expect("groups mutex poisoned"); let group_state = groups_guard .get_mut(&group_handle) .ok_or_else(|| format!("unknown group handle: {group_handle}"))?; let output = group_state .group .commit_builder() .add_member(package_state.message.clone()) .map_err(map_err)? .build() .map_err(map_err)?; group_state.group.apply_pending_commit().map_err(map_err)?; output }; let commit_message = commit_output.commit_message.clone(); process_commit_for_family(family_id, &commit_message, group_handle, &[])?; let welcome = commit_output .welcome_messages .first() .cloned() .ok_or_else(|| "add_member did not produce a welcome message".to_string())?; let joined_group_handles = if let Some(owner_identity) = package_state.owner_identity { let recipient_client = { let identities_guard = identities().lock().expect("identities mutex poisoned"); identities_guard .get(&owner_identity) .map(|state| state.client.clone()) .ok_or_else(|| { format!( "missing identity for imported key package owner: {}", owner_identity ) })? }; let (joined_group, _) = recipient_client .join_group(None, &welcome, None) .map_err(map_err)?; vec![register_group_handle( family_id, owner_identity, joined_group, )?] } else { Vec::new() }; let commit_handle = next_handle(); commits().lock().expect("commits mutex poisoned").insert( commit_handle, CommitState { family_id, commit_message: commit_output .commit_message .mls_encode_to_vec() .map_err(map_err)?, welcome_messages: commit_output .welcome_messages .iter() .map(|message| message.mls_encode_to_vec().map_err(map_err)) .collect::, _>>()?, joined_group_handles, }, ); Ok(commit_handle) } pub fn remove_member( group_handle: GroupHandle, member_ref: MemberRef, ) -> Result { let (family_id, target_signing_identity) = { let groups_guard = groups().lock().expect("groups mutex poisoned"); let group_state = groups_guard .get(&group_handle) .ok_or_else(|| format!("unknown group handle: {group_handle}"))?; let member = group_state .group .member_at_index(member_ref) .ok_or_else(|| format!("no member at index {member_ref}"))?; (group_state.family_id, member.signing_identity) }; let handles_to_remove = { let groups_guard = groups().lock().expect("groups mutex poisoned"); family_handles(family_id) .into_iter() .filter(|handle| { groups_guard .get(handle) .and_then(|state| state.group.current_member_signing_identity().ok()) .map(|identity| identity == &target_signing_identity) .unwrap_or(false) }) .collect::>() }; let commit_output = { let mut groups_guard = groups().lock().expect("groups mutex poisoned"); let group_state = groups_guard .get_mut(&group_handle) .ok_or_else(|| format!("unknown group handle: {group_handle}"))?; let output = group_state .group .commit_builder() .remove_member(member_ref) .map_err(map_err)? .build() .map_err(map_err)?; group_state.group.apply_pending_commit().map_err(map_err)?; output }; let commit_message = commit_output.commit_message.clone(); process_commit_for_family(family_id, &commit_message, group_handle, &handles_to_remove)?; remove_group_handles(&handles_to_remove); let commit_handle = next_handle(); commits().lock().expect("commits mutex poisoned").insert( commit_handle, CommitState { family_id, commit_message: commit_output .commit_message .mls_encode_to_vec() .map_err(map_err)?, welcome_messages: commit_output .welcome_messages .iter() .map(|message| message.mls_encode_to_vec().map_err(map_err)) .collect::, _>>()?, joined_group_handles: Vec::new(), }, ); Ok(commit_handle) } pub fn encrypt_group_message( group_handle: GroupHandle, plaintext: &[u8], ) -> Result, String> { if plaintext.len() > MAX_GROUP_PLAINTEXT_SIZE { return Err(format!( "group plaintext too large: {} bytes (max {})", plaintext.len(), MAX_GROUP_PLAINTEXT_SIZE )); } let mut groups_guard = groups().lock().expect("groups mutex poisoned"); let group_state = groups_guard .get_mut(&group_handle) .ok_or_else(|| format!("unknown group handle: {group_handle}"))?; group_state .group .encrypt_application_message(plaintext, Vec::new()) .map_err(map_err)? .mls_encode_to_vec() .map_err(map_err) } pub fn decrypt_group_message( group_handle: GroupHandle, ciphertext: &[u8], ) -> Result, String> { let mut cursor = ciphertext; let message = MlsMessage::mls_decode(&mut cursor).map_err(map_err)?; let mut groups_guard = groups().lock().expect("groups mutex poisoned"); let group_state = groups_guard .get_mut(&group_handle) .ok_or_else(|| format!("unknown group handle: {group_handle}"))?; match group_state .group .process_incoming_message(message) .map_err(map_err)? { ReceivedMessage::ApplicationMessage(description) => Ok(description.data().to_vec()), other => Err(format!("expected application message, received {other:?}")), } } pub fn release_identity(handle: IdentityHandle) -> bool { identities() .lock() .expect("identities mutex poisoned") .remove(&handle) .is_some() } pub fn release_group(handle: GroupHandle) -> bool { if let Some(state) = groups() .lock() .expect("groups mutex poisoned") .remove(&handle) { let mut families_guard = families().lock().expect("families mutex poisoned"); if let Some(handles) = families_guard.get_mut(&state.family_id) { handles.retain(|existing| *existing != handle); if handles.is_empty() { families_guard.remove(&state.family_id); } } true } else { false } } pub fn reset_all_state() -> bool { identities() .lock() .expect("identities mutex poisoned") .clear(); key_packages() .lock() .expect("key packages mutex poisoned") .clear(); groups().lock().expect("groups mutex poisoned").clear(); commits().lock().expect("commits mutex poisoned").clear(); dm_sessions() .lock() .expect("dm sessions mutex poisoned") .clear(); families().lock().expect("families mutex poisoned").clear(); exported_key_packages() .lock() .expect("exported key packages mutex poisoned") .clear(); pending_dm_outputs() .lock() .expect("pending dm outputs mutex poisoned") .clear(); pending_dm_output_lookups() .lock() .expect("pending dm output lookups mutex poisoned") .clear(); pending_dm_output_counters() .lock() .expect("pending dm output counters mutex poisoned") .clear(); clear_last_error(); true } pub fn create_dm_session( initiator_identity: IdentityHandle, responder_key_package: KeyPackageHandle, ) -> Result { let identities_guard = identities().lock().expect("identities mutex poisoned"); let identity_state = identities_guard .get(&initiator_identity) .ok_or_else(|| format!("unknown identity handle: {initiator_identity}"))?; let initiator_client = identity_state.client.clone(); drop(identities_guard); let package_state = key_packages() .lock() .expect("key packages mutex poisoned") .get(&responder_key_package) .cloned() .ok_or_else(|| format!("unknown key package handle: {responder_key_package}"))?; let mut group = initiator_client .create_group(ExtensionList::default(), Default::default(), None) .map_err(map_err)?; let output = group .commit_builder() .add_member(package_state.message.clone()) .map_err(map_err)? .build() .map_err(map_err)?; group.apply_pending_commit().map_err(map_err)?; let welcome = output .welcome_messages .first() .cloned() .ok_or_else(|| "dm session creation did not produce a welcome message".to_string())? .mls_encode_to_vec() .map_err(map_err)?; let handle = next_handle(); let mut sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); if sessions_guard.len() >= MAX_DM_SESSIONS { return Err("dm session limit reached".to_string()); } sessions_guard.insert( handle, DMSessionState { owner_identity: initiator_identity, group, welcome_message: welcome, }, ); Ok(handle) } pub fn dm_encrypt(session: DMSessionHandle, plaintext: &[u8]) -> Result, String> { if plaintext.len() > MAX_DM_PLAINTEXT_SIZE { return Err("plaintext exceeds maximum size".to_string()); } let mut sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); let state = sessions_guard .get_mut(&session) .ok_or_else(|| format!("unknown dm session handle: {session}"))?; state .group .encrypt_application_message(plaintext, Vec::new()) .map_err(map_err)? .mls_encode_to_vec() .map_err(map_err) } pub fn dm_decrypt(session: DMSessionHandle, ciphertext: &[u8]) -> Result, String> { let mut cursor = ciphertext; let message = MlsMessage::mls_decode(&mut cursor).map_err(map_err)?; let mut sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); let state = sessions_guard .get_mut(&session) .ok_or_else(|| format!("unknown dm session handle: {session}"))?; match state .group .process_incoming_message(message) .map_err(map_err)? { ReceivedMessage::ApplicationMessage(description) => Ok(description.data().to_vec()), other => Err(format!("expected application message, received {other:?}")), } } pub fn dm_session_welcome(session: DMSessionHandle) -> Result, String> { let sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); let state = sessions_guard .get(&session) .ok_or_else(|| format!("unknown dm session handle: {session}"))?; if state.welcome_message.is_empty() { return Err("dm session does not have a welcome message".to_string()); } Ok(state.welcome_message.clone()) } pub fn dm_session_fingerprint(session: DMSessionHandle) -> Result, String> { let (owner_identity, group_id) = { let mut sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); let state = sessions_guard .get_mut(&session) .ok_or_else(|| format!("unknown dm session handle: {session}"))?; state.group.write_to_storage().map_err(map_err)?; (state.owner_identity, state.group.group_id().to_vec()) }; let state_bytes = { let identities_guard = identities().lock().expect("identities mutex poisoned"); let id_state = identities_guard .get(&owner_identity) .ok_or_else(|| format!("identity {owner_identity} not found for dm session fingerprint"))?; let storage = id_state.client.group_state_storage(); let state = storage .state(&group_id) .unwrap_or(None) .ok_or_else(|| "dm session fingerprint missing group state".to_string())?; storage.delete_group(&group_id); state.to_vec() }; let digest = Sha256::digest(&state_bytes); Ok(digest .iter() .flat_map(|byte| format!("{byte:02x}").into_bytes()) .collect()) } pub fn join_dm_session( responder_identity: IdentityHandle, welcome_bytes: &[u8], ) -> Result { let identities_guard = identities().lock().expect("identities mutex poisoned"); let identity_state = identities_guard .get(&responder_identity) .ok_or_else(|| format!("unknown identity handle: {responder_identity}"))?; let responder_client = identity_state.client.clone(); drop(identities_guard); let mut cursor = welcome_bytes; let welcome = MlsMessage::mls_decode(&mut cursor).map_err(map_err)?; let (group, _) = responder_client .join_group(None, &welcome, None) .map_err(map_err)?; let handle = next_handle(); let mut sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); if sessions_guard.len() >= MAX_DM_SESSIONS { return Err("dm session limit reached".to_string()); } sessions_guard.insert( handle, DMSessionState { owner_identity: responder_identity, group, welcome_message: welcome_bytes.to_vec(), }, ); Ok(handle) } const DM_STATE_MAGIC: &[u8; 4] = b"SBD1"; const DM_STATE_VERSION: u32 = 1; fn write_u32_be(buf: &mut Vec, v: u32) { buf.extend_from_slice(&v.to_be_bytes()); } fn write_u64_be(buf: &mut Vec, v: u64) { buf.extend_from_slice(&v.to_be_bytes()); } fn write_blob(buf: &mut Vec, data: &[u8]) { write_u32_be(buf, data.len() as u32); buf.extend_from_slice(data); } fn read_u32_be(data: &[u8], offset: &mut usize) -> Result { if *offset + 4 > data.len() { return Err("dm state blob truncated (u32)".to_string()); } let v = u32::from_be_bytes(data[*offset..*offset + 4].try_into().unwrap()); *offset += 4; Ok(v) } fn read_u64_be(data: &[u8], offset: &mut usize) -> Result { if *offset + 8 > data.len() { return Err("dm state blob truncated (u64)".to_string()); } let v = u64::from_be_bytes(data[*offset..*offset + 8].try_into().unwrap()); *offset += 8; Ok(v) } fn read_blob(data: &[u8], offset: &mut usize) -> Result, String> { let len = read_u32_be(data, offset)? as usize; if *offset + len > data.len() { return Err("dm state blob truncated (blob)".to_string()); } let v = data[*offset..*offset + len].to_vec(); *offset += len; Ok(v) } pub fn export_dm_state() -> Result, String> { // Phase 1: snapshot identity data (under identities lock only). struct IdSnapshot { handle: u64, label: Vec, signer_secret_bytes: Vec, signing_identity_bytes: Vec, } let mut id_snapshots: HashMap = HashMap::new(); { let guard = identities().lock().expect("identities mutex poisoned"); for (&handle, state) in guard.iter() { let si_bytes = state .signing_identity .mls_encode_to_vec() .map_err(map_err)?; id_snapshots.insert( handle, IdSnapshot { handle, label: state.label.clone(), signer_secret_bytes: state.signer_secret_bytes.clone(), signing_identity_bytes: si_bytes, }, ); } } // identities lock released // Phase 2: snapshot DM sessions (under dm_sessions lock), call write_to_storage. struct SessionSnapshot { handle: u64, owner_identity: u64, group_id: Vec, welcome: Vec, } let mut session_snapshots: Vec = Vec::new(); { let mut sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); for (&handle, state) in sessions_guard.iter_mut() { state.group.write_to_storage().map_err(map_err)?; session_snapshots.push(SessionSnapshot { handle, owner_identity: state.owner_identity, group_id: state.group.group_id().to_vec(), welcome: state.welcome_message.clone(), }); } } // dm_sessions lock released // Phase 3: read group state bytes from identity storages (no global locks needed). // Filter identities to only those referenced by DM sessions. let referenced_ids: std::collections::HashSet = session_snapshots.iter().map(|s| s.owner_identity).collect(); let id_list: Vec<&IdSnapshot> = id_snapshots .values() .filter(|snap| referenced_ids.contains(&snap.handle)) .collect(); // Read group state bytes from each identity's storage. let mut session_group_states: HashMap> = HashMap::new(); { let guard = identities().lock().expect("identities mutex poisoned"); for session in &session_snapshots { let id_state = guard.get(&session.owner_identity).ok_or_else(|| { format!( "identity {} not found for dm session export", session.owner_identity ) })?; let storage = id_state.client.group_state_storage(); let state_bytes = storage .state(&session.group_id) .unwrap_or(None) .ok_or_else(|| { "group state not found in storage after write_to_storage".to_string() })?; session_group_states.insert(session.handle, state_bytes.to_vec()); // Clean up storage entry. storage.delete_group(&session.group_id); } } // Phase 4: serialize the blob. let mut buf = Vec::new(); buf.extend_from_slice(DM_STATE_MAGIC); write_u32_be(&mut buf, DM_STATE_VERSION); write_u32_be(&mut buf, id_list.len() as u32); for snap in &id_list { write_u64_be(&mut buf, snap.handle); write_blob(&mut buf, &snap.label); write_blob(&mut buf, &snap.signer_secret_bytes); write_blob(&mut buf, &snap.signing_identity_bytes); } write_u32_be(&mut buf, session_snapshots.len() as u32); for session in &session_snapshots { write_u64_be(&mut buf, session.handle); write_u64_be(&mut buf, session.owner_identity); write_blob(&mut buf, &session.group_id); let group_state = session_group_states .get(&session.handle) .ok_or_else(|| "missing group state for session".to_string())?; write_blob(&mut buf, group_state); write_blob(&mut buf, &session.welcome); } Ok(buf) } pub fn import_dm_state(data: &[u8]) -> Result, String> { // Validate magic and version. if data.len() < 8 { return Err("dm state blob too short".to_string()); } if &data[0..4] != DM_STATE_MAGIC { return Err("dm state blob invalid magic".to_string()); } let mut offset = 4; let version = read_u32_be(data, &mut offset)?; if version != DM_STATE_VERSION { return Err(format!( "dm state blob version mismatch: expected {DM_STATE_VERSION}, got {version}" )); } // Parse and import identities. let num_identities = read_u32_be(data, &mut offset)? as usize; let mut id_handle_map: HashMap = HashMap::new(); // old→new { let mut guard = identities().lock().expect("identities mutex poisoned"); for _ in 0..num_identities { let old_handle = read_u64_be(data, &mut offset)?; let label = read_blob(data, &mut offset)?; let signer_bytes = read_blob(data, &mut offset)?; let si_bytes = read_blob(data, &mut offset)?; let mut si_cursor = &si_bytes[..]; let signing_identity = SigningIdentity::mls_decode(&mut si_cursor).map_err(map_err)?; let client = make_client_from_parts(&label, signing_identity.clone(), &signer_bytes)?; if guard.len() >= MAX_IDENTITIES { return Err("identity limit reached during dm state import".to_string()); } let new_handle = next_handle(); guard.insert( new_handle, IdentityState { client, signing_identity, label, signer_secret_bytes: signer_bytes, }, ); id_handle_map.insert(old_handle, new_handle); } } // Parse and import DM sessions. let num_sessions = read_u32_be(data, &mut offset)? as usize; let mut session_handle_map: HashMap = HashMap::new(); // old→new for _ in 0..num_sessions { let old_handle = read_u64_be(data, &mut offset)?; let old_owner = read_u64_be(data, &mut offset)?; let group_id = read_blob(data, &mut offset)?; let group_state_bytes = read_blob(data, &mut offset)?; let welcome = read_blob(data, &mut offset)?; let new_owner = *id_handle_map .get(&old_owner) .ok_or_else(|| format!("dm session references unknown identity {old_owner}"))?; // Inject group state into the identity's storage and load the group. let group = { let guard = identities().lock().expect("identities mutex poisoned"); let id_state = guard .get(&new_owner) .ok_or_else(|| format!("imported identity {new_owner} not found"))?; let mut storage = id_state.client.group_state_storage(); storage .write( MlsGroupState { id: group_id.clone(), data: Zeroizing::new(group_state_bytes), }, Vec::new(), Vec::new(), ) .map_err(|e| format!("storage write failed: {e:?}"))?; let loaded = id_state.client.load_group(&group_id).map_err(map_err)?; storage.delete_group(&group_id); loaded }; let new_handle = next_handle(); let mut sessions_guard = dm_sessions().lock().expect("dm sessions mutex poisoned"); if sessions_guard.len() >= MAX_DM_SESSIONS { return Err("dm session limit reached during import".to_string()); } sessions_guard.insert( new_handle, DMSessionState { owner_identity: new_owner, group, welcome_message: welcome, }, ); drop(sessions_guard); session_handle_map.insert(old_handle, new_handle); } // Return JSON handle mapping. let result = serde_json::json!({ "version": DM_STATE_VERSION, "identities": id_handle_map.iter().map(|(k, v)| (k.to_string(), *v)).collect::>(), "dm_sessions": session_handle_map.iter().map(|(k, v)| (k.to_string(), *v)).collect::>(), }); serde_json::to_vec(&result).map_err(map_err) } pub fn release_dm_session(handle: DMSessionHandle) -> Result { let Ok(mut sessions_guard) = dm_sessions().lock() else { return Err("dm sessions mutex poisoned".to_string()); }; Ok(if sessions_guard.remove(&handle).is_some() { 1 } else { 0 }) } const GATE_STATE_MAGIC: &[u8; 4] = b"SBG1"; const GATE_STATE_VERSION: u32 = 1; pub fn export_gate_state( identity_handles: &[u64], group_handles: &[u64], ) -> Result, String> { // Phase 1: snapshot requested identities. struct IdSnapshot { handle: u64, label: Vec, signer_secret_bytes: Vec, signing_identity_bytes: Vec, } let mut id_snapshots: Vec = Vec::new(); { let guard = identities().lock().expect("identities mutex poisoned"); for &handle in identity_handles { let state = guard .get(&handle) .ok_or_else(|| format!("identity {} not found for gate state export", handle))?; let si_bytes = state .signing_identity .mls_encode_to_vec() .map_err(map_err)?; id_snapshots.push(IdSnapshot { handle, label: state.label.clone(), signer_secret_bytes: state.signer_secret_bytes.clone(), signing_identity_bytes: si_bytes, }); } } // Phase 2: snapshot requested groups — call write_to_storage to flush. struct GroupSnapshot { handle: u64, owner_identity: u64, family_id: u64, group_id: Vec, } let mut group_snapshots: Vec = Vec::new(); { let mut guard = groups().lock().expect("groups mutex poisoned"); for &handle in group_handles { let state = guard .get_mut(&handle) .ok_or_else(|| format!("group {} not found for gate state export", handle))?; state.group.write_to_storage().map_err(map_err)?; group_snapshots.push(GroupSnapshot { handle, owner_identity: state.owner_identity, family_id: state.family_id, group_id: state.group.group_id().to_vec(), }); } } // Phase 3: read group state bytes from identity storages. let mut group_state_bytes: HashMap> = HashMap::new(); { let guard = identities().lock().expect("identities mutex poisoned"); for snapshot in &group_snapshots { let id_state = guard.get(&snapshot.owner_identity).ok_or_else(|| { format!( "identity {} not found for gate group export", snapshot.owner_identity ) })?; let storage = id_state.client.group_state_storage(); let state_bytes = storage .state(&snapshot.group_id) .unwrap_or(None) .ok_or_else(|| { "group state not found in storage after write_to_storage".to_string() })?; group_state_bytes.insert(snapshot.handle, state_bytes.to_vec()); storage.delete_group(&snapshot.group_id); } } // Phase 4: serialize the blob. let mut buf = Vec::new(); buf.extend_from_slice(GATE_STATE_MAGIC); write_u32_be(&mut buf, GATE_STATE_VERSION); write_u32_be(&mut buf, id_snapshots.len() as u32); for snap in &id_snapshots { write_u64_be(&mut buf, snap.handle); write_blob(&mut buf, &snap.label); write_blob(&mut buf, &snap.signer_secret_bytes); write_blob(&mut buf, &snap.signing_identity_bytes); } write_u32_be(&mut buf, group_snapshots.len() as u32); for snapshot in &group_snapshots { write_u64_be(&mut buf, snapshot.handle); write_u64_be(&mut buf, snapshot.owner_identity); write_u64_be(&mut buf, snapshot.family_id); write_blob(&mut buf, &snapshot.group_id); let state = group_state_bytes .get(&snapshot.handle) .ok_or_else(|| "missing group state for gate export".to_string())?; write_blob(&mut buf, state); } Ok(buf) } pub fn import_gate_state(data: &[u8]) -> Result, String> { if data.len() < 8 { return Err("gate state blob too short".to_string()); } if &data[0..4] != GATE_STATE_MAGIC { return Err("gate state blob invalid magic".to_string()); } let mut offset = 4; let version = read_u32_be(data, &mut offset)?; if version != GATE_STATE_VERSION { return Err(format!( "gate state blob version mismatch: expected {GATE_STATE_VERSION}, got {version}" )); } // Import identities. let num_identities = read_u32_be(data, &mut offset)? as usize; let mut id_handle_map: HashMap = HashMap::new(); { let mut guard = identities().lock().expect("identities mutex poisoned"); for _ in 0..num_identities { let old_handle = read_u64_be(data, &mut offset)?; let label = read_blob(data, &mut offset)?; let signer_bytes = read_blob(data, &mut offset)?; let si_bytes = read_blob(data, &mut offset)?; let mut si_cursor = &si_bytes[..]; let signing_identity = SigningIdentity::mls_decode(&mut si_cursor).map_err(map_err)?; let client = make_client_from_parts(&label, signing_identity.clone(), &signer_bytes)?; if guard.len() >= MAX_IDENTITIES { return Err("identity limit reached during gate state import".to_string()); } let new_handle = next_handle(); guard.insert( new_handle, IdentityState { client, signing_identity, label, signer_secret_bytes: signer_bytes, }, ); id_handle_map.insert(old_handle, new_handle); } } // Import groups with family remapping. let num_groups = read_u32_be(data, &mut offset)? as usize; let mut group_handle_map: HashMap = HashMap::new(); let mut family_id_map: HashMap = HashMap::new(); for _ in 0..num_groups { let old_handle = read_u64_be(data, &mut offset)?; let old_owner = read_u64_be(data, &mut offset)?; let old_family_id = read_u64_be(data, &mut offset)?; let group_id = read_blob(data, &mut offset)?; let group_state_bytes_raw = read_blob(data, &mut offset)?; let new_owner = *id_handle_map .get(&old_owner) .ok_or_else(|| format!("gate group references unknown identity {old_owner}"))?; let new_family_id = *family_id_map .entry(old_family_id) .or_insert_with(next_family_id); // Load group from persisted state. let group = { let guard = identities().lock().expect("identities mutex poisoned"); let id_state = guard .get(&new_owner) .ok_or_else(|| format!("imported identity {new_owner} not found"))?; let mut storage = id_state.client.group_state_storage(); storage .write( MlsGroupState { id: group_id.clone(), data: Zeroizing::new(group_state_bytes_raw), }, Vec::new(), Vec::new(), ) .map_err(|e| format!("storage write failed: {e:?}"))?; let loaded = id_state.client.load_group(&group_id).map_err(map_err)?; storage.delete_group(&group_id); loaded }; let new_handle = next_handle(); let mut groups_guard = groups().lock().expect("groups mutex poisoned"); if groups_guard.len() >= MAX_GROUPS { return Err("group limit reached during gate state import".to_string()); } groups_guard.insert( new_handle, GroupState { family_id: new_family_id, owner_identity: new_owner, group, }, ); drop(groups_guard); families() .lock() .expect("families mutex poisoned") .entry(new_family_id) .or_default() .push(new_handle); group_handle_map.insert(old_handle, new_handle); } let result = serde_json::json!({ "version": GATE_STATE_VERSION, "identities": id_handle_map.iter() .map(|(k, v)| (k.to_string(), *v)) .collect::>(), "groups": group_handle_map.iter() .map(|(k, v)| (k.to_string(), *v)) .collect::>(), }); serde_json::to_vec(&result).map_err(map_err) } pub fn export_public_bundle(identity: IdentityHandle) -> Result, String> { let identities_guard = identities().lock().expect("identities mutex poisoned"); let state = identities_guard .get(&identity) .ok_or_else(|| format!("unknown identity handle: {identity}"))?; let bundle = PublicBundle { label: String::from_utf8_lossy(&state.label).to_string(), cipher_suite: "CURVE25519_AES128", signing_public_key: state.signing_identity.signature_key.as_bytes().to_vec(), credential: state .signing_identity .credential .mls_encode_to_vec() .map_err(map_err)?, }; serde_json::to_vec(&bundle).map_err(map_err) } fn handle_stats_json() -> Result, String> { let stats = HandleStats { identities: identities() .lock() .expect("identities mutex poisoned") .len(), groups: groups().lock().expect("groups mutex poisoned").len(), dm_sessions: dm_sessions() .lock() .expect("dm sessions mutex poisoned") .len(), max_identities: MAX_IDENTITIES, max_groups: MAX_GROUPS, max_dm_sessions: MAX_DM_SESSIONS, }; serde_json::to_vec(&stats).map_err(map_err) } fn commit_message_bytes(commit: CommitHandle) -> Result, String> { let commits_guard = commits().lock().expect("commits mutex poisoned"); let state = commits_guard .get(&commit) .ok_or_else(|| format!("unknown commit handle: {commit}"))?; Ok(state.commit_message.clone()) } fn commit_welcome_message_bytes(commit: CommitHandle, index: usize) -> Result, String> { let commits_guard = commits().lock().expect("commits mutex poisoned"); let state = commits_guard .get(&commit) .ok_or_else(|| format!("unknown commit handle: {commit}"))?; state .welcome_messages .get(index) .cloned() .ok_or_else(|| format!("no welcome message at index {index}")) } fn commit_joined_group_handle(commit: CommitHandle, index: usize) -> Result { let commits_guard = commits().lock().expect("commits mutex poisoned"); let state = commits_guard .get(&commit) .ok_or_else(|| format!("unknown commit handle: {commit}"))?; state .joined_group_handles .get(index) .copied() .ok_or_else(|| format!("no joined group handle at index {index}")) } fn with_handle_result(operation: F) -> u64 where F: FnOnce() -> Result, { clear_last_error(); match panic::catch_unwind(AssertUnwindSafe(operation)) { Ok(Ok(handle)) => handle, Ok(Err(error)) => { set_last_error(error); 0 } Err(_) => { set_last_error("privacy-core panicked across the FFI boundary"); 0 } } } fn with_bool_result(operation: F) -> bool where F: FnOnce() -> Result, { clear_last_error(); match panic::catch_unwind(AssertUnwindSafe(operation)) { Ok(Ok(value)) => value, Ok(Err(error)) => { set_last_error(error); false } Err(_) => { set_last_error("privacy-core panicked across the FFI boundary"); false } } } fn with_bytes_result(operation: F) -> ByteBuffer where F: FnOnce() -> Result, String>, { clear_last_error(); match panic::catch_unwind(AssertUnwindSafe(operation)) { Ok(Ok(bytes)) => to_buffer(bytes), Ok(Err(error)) => { set_last_error(error); ByteBuffer::empty() } Err(_) => { set_last_error("privacy-core panicked across the FFI boundary"); ByteBuffer::empty() } } } fn with_i64_result(operation: F) -> i64 where F: FnOnce() -> Result, { clear_last_error(); match panic::catch_unwind(AssertUnwindSafe(operation)) { Ok(Ok(value)) => value, Ok(Err(error)) => { set_last_error(error); -1 } Err(_) => { set_last_error("privacy-core panicked across the FFI boundary"); -1 } } } fn with_i32_result(operation: F) -> i32 where F: FnOnce() -> Result, { clear_last_error(); match panic::catch_unwind(AssertUnwindSafe(operation)) { Ok(Ok(value)) => value, Ok(Err(error)) => { set_last_error(error); 0 } Err(_) => { set_last_error("privacy-core panicked across the FFI boundary"); 0 } } } fn write_to_output_buffer(bytes: &[u8], out_buf: *mut u8, out_cap: usize) -> Result { let required = i64::try_from(bytes.len()).map_err(|_| "output too large".to_string())?; if out_buf.is_null() || out_cap == 0 { return Ok(required); } if out_cap < bytes.len() { return Err(format!( "output buffer too small: need {} bytes, got {}", bytes.len(), out_cap )); } unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf, bytes.len()); } Ok(required) } fn input_hash(bytes: &[u8]) -> u64 { let mut hasher = DefaultHasher::new(); bytes.hash(&mut hasher); hasher.finish() } fn cache_key(session: u64, opcode: u8, counter: u64) -> u64 { let mut hasher = DefaultHasher::new(); session.hash(&mut hasher); opcode.hash(&mut hasher); counter.hash(&mut hasher); hasher.finish() } fn next_pending_output_key(opcode: u8, session: u64) -> Result<(u8, u64, u64), String> { let mut counters = pending_dm_output_counters() .lock() .map_err(|_| "pending dm output counters mutex poisoned".to_string())?; let counter = counters.entry((opcode, session)).or_insert(0); *counter = counter.saturating_add(1); Ok((opcode, session, cache_key(session, opcode, *counter))) } fn prune_pending_outputs(now: Instant) { let mut expired: Vec<(u8, u64, u64)> = Vec::new(); { let mut pending = pending_dm_outputs() .lock() .expect("pending dm outputs mutex poisoned"); let expired_keys: Vec<(u8, u64, u64)> = pending .iter() .filter_map(|(key, (_bytes, inserted_at))| { if now.duration_since(*inserted_at) > PENDING_DM_OUTPUT_TTL { Some(*key) } else { None } }) .collect(); for key in expired_keys { if let Some((mut bytes, _inserted_at)) = pending.remove(&key) { wipe_vec(&mut bytes); } expired.push(key); } } if expired.is_empty() { return; } let mut lookups = pending_dm_output_lookups() .lock() .expect("pending dm output lookup mutex poisoned"); lookups.retain(|_, queue| { queue.retain(|key| !expired.contains(key)); !queue.is_empty() }); } fn stage_or_write_output( opcode: u8, session: u64, input_fingerprint: u64, out_buf: *mut u8, out_cap: usize, producer: F, ) -> Result where F: FnOnce() -> Result, String>, { let now = Instant::now(); let lookup_key = (opcode, session, input_fingerprint); if out_buf.is_null() || out_cap == 0 { let bytes = producer()?; let required = i64::try_from(bytes.len()).map_err(|_| "output too large".to_string())?; prune_pending_outputs(now); let output_key = next_pending_output_key(opcode, session)?; let mut pending = pending_dm_outputs() .lock() .expect("pending dm outputs mutex poisoned"); if pending.len() >= MAX_PENDING_DM_OUTPUTS { return Err("pending output buffer full — cannot enqueue".into()); } pending.insert(output_key, (bytes, now)); drop(pending); pending_dm_output_lookups() .lock() .expect("pending dm output lookup mutex poisoned") .entry(lookup_key) .or_default() .push_back(output_key); return Ok(required); } prune_pending_outputs(now); let output_key = { let mut lookups = pending_dm_output_lookups() .lock() .expect("pending dm output lookup mutex poisoned"); let mut remove_lookup = false; let next = if let Some(queue) = lookups.get_mut(&lookup_key) { let next = queue.pop_front(); remove_lookup = queue.is_empty(); next } else { None }; if remove_lookup { lookups.remove(&lookup_key); } next }; let mut bytes = if let Some(output_key) = output_key { if let Some((bytes, _inserted_at)) = pending_dm_outputs() .lock() .expect("pending dm outputs mutex poisoned") .remove(&output_key) { bytes } else { producer()? } } else { producer()? }; let written = write_to_output_buffer(&bytes, out_buf, out_cap); wipe_vec(&mut bytes); Ok(written?) } #[no_mangle] pub extern "C" fn privacy_core_version() -> ByteBuffer { to_buffer(VERSION.as_bytes().to_vec()) } #[no_mangle] pub extern "C" fn privacy_core_last_error_message() -> ByteBuffer { let message = last_error() .lock() .expect("last error mutex poisoned") .clone(); to_buffer(message.into_bytes()) } #[no_mangle] pub extern "C" fn privacy_core_free_buffer(buffer: ByteBuffer) { from_buffer(buffer); } #[no_mangle] pub extern "C" fn privacy_core_create_identity() -> u64 { with_handle_result(create_identity) } #[no_mangle] pub extern "C" fn privacy_core_export_key_package(identity: u64) -> ByteBuffer { with_bytes_result(|| export_key_package(identity)) } #[no_mangle] pub extern "C" fn privacy_core_import_key_package(data: *const u8, len: usize) -> u64 { with_handle_result(|| import_key_package(bytes_from_raw(data, len)?)) } #[no_mangle] pub extern "C" fn privacy_core_create_group(identity: u64) -> u64 { with_handle_result(|| create_group(identity)) } #[no_mangle] pub extern "C" fn privacy_core_add_member(group: u64, key_package: u64) -> u64 { with_handle_result(|| add_member(group, key_package)) } #[no_mangle] pub extern "C" fn privacy_core_remove_member(group: u64, member_ref: u32) -> u64 { with_handle_result(|| remove_member(group, member_ref)) } #[no_mangle] pub extern "C" fn privacy_core_encrypt_group_message( group: u64, plaintext: *const u8, len: usize, ) -> ByteBuffer { with_bytes_result(|| encrypt_group_message(group, bytes_from_raw(plaintext, len)?)) } #[no_mangle] pub extern "C" fn privacy_core_decrypt_group_message( group: u64, ciphertext: *const u8, len: usize, ) -> ByteBuffer { with_bytes_result(|| decrypt_group_message(group, bytes_from_raw(ciphertext, len)?)) } #[no_mangle] pub extern "C" fn privacy_core_export_public_bundle(identity: u64) -> ByteBuffer { with_bytes_result(|| export_public_bundle(identity)) } #[no_mangle] pub extern "C" fn privacy_core_handle_stats(out_buf: *mut u8, out_cap: usize) -> i64 { with_i64_result(|| stage_or_write_output(4, 0, 0, out_buf, out_cap, handle_stats_json)) } #[no_mangle] pub extern "C" fn privacy_core_commit_message_bytes(commit: u64) -> ByteBuffer { with_bytes_result(|| commit_message_bytes(commit)) } #[no_mangle] pub extern "C" fn privacy_core_commit_welcome_message_bytes( commit: u64, index: usize, ) -> ByteBuffer { with_bytes_result(|| commit_welcome_message_bytes(commit, index)) } #[no_mangle] pub extern "C" fn privacy_core_commit_joined_group_handle(commit: u64, index: usize) -> u64 { with_handle_result(|| commit_joined_group_handle(commit, index)) } #[no_mangle] pub extern "C" fn privacy_core_create_dm_session( initiator_identity: u64, responder_key_package: u64, ) -> i64 { with_i64_result(|| { create_dm_session(initiator_identity, responder_key_package).map(|handle| handle as i64) }) } #[no_mangle] pub extern "C" fn privacy_core_dm_encrypt( session: u64, plaintext: *const u8, len: usize, out_buf: *mut u8, out_cap: usize, ) -> i64 { with_i64_result(|| { let plaintext = bytes_from_raw(plaintext, len)?; stage_or_write_output(1, session, input_hash(plaintext), out_buf, out_cap, || { dm_encrypt(session, plaintext) }) }) } #[no_mangle] pub extern "C" fn privacy_core_dm_decrypt( session: u64, ciphertext: *const u8, len: usize, out_buf: *mut u8, out_cap: usize, ) -> i64 { with_i64_result(|| { let ciphertext = bytes_from_raw(ciphertext, len)?; stage_or_write_output(2, session, input_hash(ciphertext), out_buf, out_cap, || { dm_decrypt(session, ciphertext) }) }) } #[no_mangle] pub extern "C" fn privacy_core_dm_session_welcome( session: u64, out_buf: *mut u8, out_cap: usize, ) -> i64 { with_i64_result(|| { stage_or_write_output(3, session, 0, out_buf, out_cap, || { dm_session_welcome(session) }) }) } #[no_mangle] pub extern "C" fn privacy_core_dm_session_fingerprint( session: u64, out_buf: *mut u8, out_cap: usize, ) -> i64 { with_i64_result(|| { stage_or_write_output(5, session, 0, out_buf, out_cap, || { dm_session_fingerprint(session) }) }) } #[no_mangle] pub extern "C" fn privacy_core_join_dm_session( responder_identity: u64, welcome: *const u8, len: usize, ) -> i64 { with_i64_result(|| { join_dm_session(responder_identity, bytes_from_raw(welcome, len)?) .map(|handle| handle as i64) }) } #[no_mangle] pub extern "C" fn privacy_core_release_dm_session(session: u64) -> i32 { with_i32_result(|| release_dm_session(session)) } #[no_mangle] pub extern "C" fn privacy_core_export_dm_state(out_buf: *mut u8, out_cap: usize) -> i64 { with_i64_result(|| { let bytes = export_dm_state()?; write_to_output_buffer(&bytes, out_buf, out_cap) }) } #[no_mangle] pub extern "C" fn privacy_core_import_dm_state( data: *const u8, len: usize, out_buf: *mut u8, out_cap: usize, ) -> i64 { with_i64_result(|| { let input = bytes_from_raw(data, len)?; let fingerprint = input_hash(input); stage_or_write_output(5, 0, fingerprint, out_buf, out_cap, || { import_dm_state(input) }) }) } #[no_mangle] pub extern "C" fn privacy_core_export_gate_state( identity_handles: *const u64, num_identities: usize, group_handles: *const u64, num_groups: usize, out_buf: *mut u8, out_cap: usize, ) -> i64 { with_i64_result(|| { let id_slice = if num_identities == 0 { &[] } else if identity_handles.is_null() { return Err("null identity handles pointer".to_string()); } else { unsafe { slice::from_raw_parts(identity_handles, num_identities) } }; let group_slice = if num_groups == 0 { &[] } else if group_handles.is_null() { return Err("null group handles pointer".to_string()); } else { unsafe { slice::from_raw_parts(group_handles, num_groups) } }; let bytes = export_gate_state(id_slice, group_slice)?; write_to_output_buffer(&bytes, out_buf, out_cap) }) } #[no_mangle] pub extern "C" fn privacy_core_import_gate_state( data: *const u8, len: usize, out_buf: *mut u8, out_cap: usize, ) -> i64 { with_i64_result(|| { let input = bytes_from_raw(data, len)?; let fingerprint = input_hash(input); stage_or_write_output(6, 0, fingerprint, out_buf, out_cap, || { import_gate_state(input) }) }) } #[no_mangle] pub extern "C" fn privacy_core_release_identity(handle: u64) -> bool { with_bool_result(|| { let Ok(mut guard) = identities().lock() else { return Err("identities mutex poisoned".to_string()); }; Ok(guard.remove(&handle).is_some()) }) } #[no_mangle] pub extern "C" fn privacy_core_release_key_package(handle: u64) -> bool { with_bool_result(|| { let Ok(mut guard) = key_packages().lock() else { return Err("key packages mutex poisoned".to_string()); }; Ok(guard.remove(&handle).is_some()) }) } #[no_mangle] pub extern "C" fn privacy_core_release_group(handle: u64) -> bool { with_bool_result(|| { let Ok(mut groups_guard) = groups().lock() else { return Err("groups mutex poisoned".to_string()); }; let removed = groups_guard.remove(&handle); drop(groups_guard); if let Some(state) = removed { let Ok(mut families_guard) = families().lock() else { return Err("families mutex poisoned".to_string()); }; if let Some(entries) = families_guard.get_mut(&state.family_id) { entries.retain(|candidate| candidate != &handle); } Ok(true) } else { Ok(false) } }) } #[no_mangle] pub extern "C" fn privacy_core_release_commit(handle: u64) -> bool { with_bool_result(|| { let Ok(mut guard) = commits().lock() else { return Err("commits mutex poisoned".to_string()); }; Ok(guard.remove(&handle).is_some()) }) } #[no_mangle] pub extern "C" fn privacy_core_reset_all_state() -> bool { with_bool_result(|| { let Ok(mut identities_guard) = identities().lock() else { return Err("identities mutex poisoned".to_string()); }; identities_guard.clear(); drop(identities_guard); let Ok(mut key_packages_guard) = key_packages().lock() else { return Err("key packages mutex poisoned".to_string()); }; key_packages_guard.clear(); drop(key_packages_guard); let Ok(mut groups_guard) = groups().lock() else { return Err("groups mutex poisoned".to_string()); }; groups_guard.clear(); drop(groups_guard); let Ok(mut commits_guard) = commits().lock() else { return Err("commits mutex poisoned".to_string()); }; commits_guard.clear(); drop(commits_guard); let Ok(mut dm_sessions_guard) = dm_sessions().lock() else { return Err("dm sessions mutex poisoned".to_string()); }; dm_sessions_guard.clear(); drop(dm_sessions_guard); let Ok(mut families_guard) = families().lock() else { return Err("families mutex poisoned".to_string()); }; families_guard.clear(); drop(families_guard); let Ok(mut exported_guard) = exported_key_packages().lock() else { return Err("exported key packages mutex poisoned".to_string()); }; exported_guard.clear(); drop(exported_guard); let Ok(mut pending_outputs_guard) = pending_dm_outputs().lock() else { return Err("pending dm outputs mutex poisoned".to_string()); }; for (_key, (mut bytes, _inserted_at)) in pending_outputs_guard.drain() { wipe_vec(&mut bytes); } drop(pending_outputs_guard); let Ok(mut pending_lookup_guard) = pending_dm_output_lookups().lock() else { return Err("pending dm output lookup mutex poisoned".to_string()); }; pending_lookup_guard.clear(); drop(pending_lookup_guard); let Ok(mut pending_counter_guard) = pending_dm_output_counters().lock() else { return Err("pending dm output counters mutex poisoned".to_string()); }; pending_counter_guard.clear(); drop(pending_counter_guard); clear_last_error(); Ok(true) }) } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn wasm_reset_all_state() -> bool { reset_all_state() } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn wasm_gate_import_state(data: &[u8]) -> Result { let mapping = import_gate_state(data).map_err(|e| JsValue::from_str(&e))?; String::from_utf8(mapping).map_err(|e| JsValue::from_str(&e.to_string())) } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn wasm_gate_export_state( identity_handles_json: &str, group_handles_json: &str, ) -> Result, JsValue> { let identity_handles = wasm_handles_from_json(identity_handles_json).map_err(|e| JsValue::from_str(&e))?; let group_handles = wasm_handles_from_json(group_handles_json).map_err(|e| JsValue::from_str(&e))?; let blob = export_gate_state(&identity_handles, &group_handles).map_err(|e| JsValue::from_str(&e))?; Ok(blob.into_boxed_slice()) } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn wasm_gate_encrypt(group_handle: u64, plaintext: &[u8]) -> Result, JsValue> { let ciphertext = encrypt_group_message(group_handle, plaintext).map_err(|e| JsValue::from_str(&e))?; Ok(ciphertext.into_boxed_slice()) } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn wasm_gate_decrypt(group_handle: u64, ciphertext: &[u8]) -> Result, JsValue> { let plaintext = decrypt_group_message(group_handle, ciphertext).map_err(|e| JsValue::from_str(&e))?; Ok(plaintext.into_boxed_slice()) } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn wasm_release_identity(handle: u64) -> bool { release_identity(handle) } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn wasm_release_group(handle: u64) -> bool { release_group(handle) } #[cfg(test)] mod tests { use super::*; use std::sync::{Mutex, OnceLock}; fn test_lock() -> &'static Mutex<()> { static TEST_LOCK: OnceLock> = OnceLock::new(); TEST_LOCK.get_or_init(|| Mutex::new(())) } #[test] fn dm_session_round_trip() { let _guard = test_lock().lock().expect("test lock poisoned"); privacy_core_reset_all_state(); let alice = create_identity().expect("alice identity"); let bob = create_identity().expect("bob identity"); let bob_key_package = export_key_package(bob).expect("bob key package"); let bob_package_handle = import_key_package(&bob_key_package).expect("import bob key package"); let alice_session = create_dm_session(alice, bob_package_handle).expect("alice session"); let welcome = dm_session_welcome(alice_session).expect("welcome"); let bob_session = join_dm_session(bob, &welcome).expect("bob session"); let ct1 = dm_encrypt(alice_session, b"hello bob").expect("encrypt alice->bob"); let pt1 = dm_decrypt(bob_session, &ct1).expect("decrypt alice->bob"); assert_eq!(pt1, b"hello bob"); let ct2 = dm_encrypt(bob_session, b"hello alice").expect("encrypt bob->alice"); let pt2 = dm_decrypt(alice_session, &ct2).expect("decrypt bob->alice"); assert_eq!(pt2, b"hello alice"); assert_eq!(release_dm_session(alice_session).expect("release alice"), 1); assert_eq!(release_dm_session(bob_session).expect("release bob"), 1); assert_eq!( release_dm_session(alice_session).expect("release missing"), 0 ); } #[test] fn identity_limit_rejects_overflow() { let _guard = test_lock().lock().expect("test lock poisoned"); privacy_core_reset_all_state(); for _ in 0..MAX_IDENTITIES { create_identity().expect("identity within limit"); } assert_eq!( create_identity().expect_err("identity overflow"), "identity limit reached" ); } #[test] fn group_encrypt_rejects_oversized_plaintext() { let _guard = test_lock().lock().expect("test lock poisoned"); privacy_core_reset_all_state(); let owner = create_identity().expect("owner identity"); let group = create_group(owner).expect("group"); let ok_plaintext = vec![b'a'; 60 * 1024]; assert!(encrypt_group_message(group, &ok_plaintext).is_ok()); let too_large = vec![b'b'; 100 * 1024]; let err = encrypt_group_message(group, &too_large).expect_err("oversized group plaintext"); assert!(err.contains("group plaintext too large")); } #[test] fn add_member_respects_group_limit_when_join_registers_new_handle() { let _guard = test_lock().lock().expect("test lock poisoned"); privacy_core_reset_all_state(); let owner = create_identity().expect("owner identity"); let recipient = create_identity().expect("recipient identity"); let recipient_bundle = export_key_package(recipient).expect("recipient bundle"); let recipient_package = import_key_package(&recipient_bundle).expect("recipient package"); let mut last_group = 0; for _ in 0..MAX_GROUPS { last_group = create_group(owner).expect("group within limit"); } let err = add_member(last_group, recipient_package).expect_err("group limit overflow"); assert_eq!(err, "maximum group limit reached"); } #[test] fn staged_outputs_keep_sequential_same_session_requests_distinct() { let _guard = test_lock().lock().expect("test lock poisoned"); privacy_core_reset_all_state(); let first_required = stage_or_write_output(1, 77, 99, std::ptr::null_mut(), 0, || { Ok(b"first-output".to_vec()) }) .expect("stage first"); let second_required = stage_or_write_output(1, 77, 99, std::ptr::null_mut(), 0, || { Ok(b"second-output".to_vec()) }) .expect("stage second"); assert_eq!(first_required, 12); assert_eq!(second_required, 13); let mut first_buf = [0u8; 32]; let first_written = stage_or_write_output(1, 77, 99, first_buf.as_mut_ptr(), first_buf.len(), || { Ok(b"unexpected".to_vec()) }) .expect("retrieve first"); assert_eq!(first_written, 12); assert_eq!(&first_buf[..first_written as usize], b"first-output"); let mut second_buf = [0u8; 32]; let second_written = stage_or_write_output(1, 77, 99, second_buf.as_mut_ptr(), second_buf.len(), || { Ok(b"unexpected".to_vec()) }) .expect("retrieve second"); assert_eq!(second_written, 13); assert_eq!(&second_buf[..second_written as usize], b"second-output"); } }