mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-08 18:34:58 +02:00
2255 lines
74 KiB
Rust
2255 lines
74 KiB
Rust
//! 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<BasicIdentityProvider, WithCryptoProvider<RustCryptoProvider, BaseConfig>>;
|
|
type PrivacyClient = Client<PrivacyConfig>;
|
|
type PrivacyGroup = Group<PrivacyConfig>;
|
|
|
|
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<u8>,
|
|
signer_secret_bytes: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct KeyPackageState {
|
|
message: MlsMessage,
|
|
owner_identity: Option<IdentityHandle>,
|
|
}
|
|
|
|
struct GroupState {
|
|
family_id: FamilyId,
|
|
owner_identity: IdentityHandle,
|
|
group: PrivacyGroup,
|
|
}
|
|
|
|
struct CommitState {
|
|
family_id: FamilyId,
|
|
commit_message: Vec<u8>,
|
|
welcome_messages: Vec<Vec<u8>>,
|
|
joined_group_handles: Vec<GroupHandle>,
|
|
}
|
|
|
|
struct DMSessionState {
|
|
owner_identity: IdentityHandle,
|
|
group: PrivacyGroup,
|
|
welcome_message: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct PublicBundle {
|
|
label: String,
|
|
cipher_suite: &'static str,
|
|
signing_public_key: Vec<u8>,
|
|
credential: Vec<u8>,
|
|
}
|
|
|
|
#[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<Mutex<String>> = OnceLock::new();
|
|
static IDENTITIES: OnceLock<Mutex<HashMap<IdentityHandle, IdentityState>>> = OnceLock::new();
|
|
static KEY_PACKAGES: OnceLock<Mutex<HashMap<KeyPackageHandle, KeyPackageState>>> = OnceLock::new();
|
|
static GROUPS: OnceLock<Mutex<HashMap<GroupHandle, GroupState>>> = OnceLock::new();
|
|
static COMMITS: OnceLock<Mutex<HashMap<CommitHandle, CommitState>>> = OnceLock::new();
|
|
static DM_SESSIONS: OnceLock<Mutex<HashMap<DMSessionHandle, DMSessionState>>> = OnceLock::new();
|
|
static FAMILIES: OnceLock<Mutex<HashMap<FamilyId, Vec<GroupHandle>>>> = OnceLock::new();
|
|
static EXPORTED_KEY_PACKAGES: OnceLock<Mutex<HashMap<Vec<u8>, IdentityHandle>>> = OnceLock::new();
|
|
static PENDING_DM_OUTPUTS: OnceLock<Mutex<HashMap<(u8, u64, u64), (Vec<u8>, Instant)>>> =
|
|
OnceLock::new();
|
|
static PENDING_DM_OUTPUT_LOOKUPS: OnceLock<
|
|
Mutex<HashMap<(u8, u64, u64), VecDeque<(u8, u64, u64)>>>,
|
|
> = OnceLock::new();
|
|
static PENDING_DM_OUTPUT_COUNTERS: OnceLock<Mutex<HashMap<(u8, u64), u64>>> = OnceLock::new();
|
|
|
|
fn identities() -> &'static Mutex<HashMap<IdentityHandle, IdentityState>> {
|
|
IDENTITIES.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn key_packages() -> &'static Mutex<HashMap<KeyPackageHandle, KeyPackageState>> {
|
|
KEY_PACKAGES.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn groups() -> &'static Mutex<HashMap<GroupHandle, GroupState>> {
|
|
GROUPS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn commits() -> &'static Mutex<HashMap<CommitHandle, CommitState>> {
|
|
COMMITS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn dm_sessions() -> &'static Mutex<HashMap<DMSessionHandle, DMSessionState>> {
|
|
DM_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn families() -> &'static Mutex<HashMap<FamilyId, Vec<GroupHandle>>> {
|
|
FAMILIES.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn exported_key_packages() -> &'static Mutex<HashMap<Vec<u8>, IdentityHandle>> {
|
|
EXPORTED_KEY_PACKAGES.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn pending_dm_outputs() -> &'static Mutex<HashMap<(u8, u64, u64), (Vec<u8>, Instant)>> {
|
|
PENDING_DM_OUTPUTS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn pending_dm_output_lookups() -> &'static Mutex<HashMap<(u8, u64, u64), VecDeque<(u8, u64, u64)>>>
|
|
{
|
|
PENDING_DM_OUTPUT_LOOKUPS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn pending_dm_output_counters() -> &'static Mutex<HashMap<(u8, u64), u64>> {
|
|
PENDING_DM_OUTPUT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn last_error() -> &'static Mutex<String> {
|
|
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<String>) {
|
|
*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<u8>) {
|
|
if !bytes.is_empty() {
|
|
wipe_bytes(bytes.as_mut_slice());
|
|
}
|
|
}
|
|
|
|
fn to_buffer(mut bytes: Vec<u8>) -> 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<E: std::fmt::Display>(err: E) -> String {
|
|
err.to_string()
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn wasm_handles_from_json(json: &str) -> Result<Vec<u64>, String> {
|
|
let trimmed = json.trim();
|
|
if trimmed.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
serde_json::from_str::<Vec<u64>>(trimmed).map_err(map_err)
|
|
}
|
|
|
|
fn make_client(label: &[u8]) -> Result<(PrivacyClient, SigningIdentity, Vec<u8>), 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<PrivacyClient, String> {
|
|
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<GroupHandle> {
|
|
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<GroupHandle, String> {
|
|
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<IdentityHandle, String> {
|
|
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<Vec<u8>, 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<KeyPackageHandle, String> {
|
|
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<GroupHandle, String> {
|
|
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<CommitHandle, String> {
|
|
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::<Result<Vec<_>, _>>()?,
|
|
joined_group_handles,
|
|
},
|
|
);
|
|
Ok(commit_handle)
|
|
}
|
|
|
|
pub fn remove_member(
|
|
group_handle: GroupHandle,
|
|
member_ref: MemberRef,
|
|
) -> Result<CommitHandle, String> {
|
|
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::<Vec<_>>()
|
|
};
|
|
|
|
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::<Result<Vec<_>, _>>()?,
|
|
joined_group_handles: Vec::new(),
|
|
},
|
|
);
|
|
Ok(commit_handle)
|
|
}
|
|
|
|
pub fn encrypt_group_message(
|
|
group_handle: GroupHandle,
|
|
plaintext: &[u8],
|
|
) -> Result<Vec<u8>, 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<Vec<u8>, 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<DMSessionHandle, String> {
|
|
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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<DMSessionHandle, String> {
|
|
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<u8>, v: u32) {
|
|
buf.extend_from_slice(&v.to_be_bytes());
|
|
}
|
|
|
|
fn write_u64_be(buf: &mut Vec<u8>, v: u64) {
|
|
buf.extend_from_slice(&v.to_be_bytes());
|
|
}
|
|
|
|
fn write_blob(buf: &mut Vec<u8>, 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<u32, String> {
|
|
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<u64, String> {
|
|
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<Vec<u8>, 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<Vec<u8>, String> {
|
|
// Phase 1: snapshot identity data (under identities lock only).
|
|
struct IdSnapshot {
|
|
handle: u64,
|
|
label: Vec<u8>,
|
|
signer_secret_bytes: Vec<u8>,
|
|
signing_identity_bytes: Vec<u8>,
|
|
}
|
|
let mut id_snapshots: HashMap<u64, IdSnapshot> = 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<u8>,
|
|
welcome: Vec<u8>,
|
|
}
|
|
let mut session_snapshots: Vec<SessionSnapshot> = 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<u64> =
|
|
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<u64, Vec<u8>> = 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<Vec<u8>, 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<u64, u64> = 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<u64, u64> = 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::<HashMap<String, u64>>(),
|
|
"dm_sessions": session_handle_map.iter().map(|(k, v)| (k.to_string(), *v)).collect::<HashMap<String, u64>>(),
|
|
});
|
|
serde_json::to_vec(&result).map_err(map_err)
|
|
}
|
|
|
|
pub fn release_dm_session(handle: DMSessionHandle) -> Result<i32, String> {
|
|
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<Vec<u8>, String> {
|
|
// Phase 1: snapshot requested identities.
|
|
struct IdSnapshot {
|
|
handle: u64,
|
|
label: Vec<u8>,
|
|
signer_secret_bytes: Vec<u8>,
|
|
signing_identity_bytes: Vec<u8>,
|
|
}
|
|
let mut id_snapshots: Vec<IdSnapshot> = 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<u8>,
|
|
}
|
|
let mut group_snapshots: Vec<GroupSnapshot> = 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<u64, Vec<u8>> = 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<Vec<u8>, 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<u64, u64> = 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<u64, u64> = HashMap::new();
|
|
let mut family_id_map: HashMap<u64, u64> = 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::<HashMap<String, u64>>(),
|
|
"groups": group_handle_map.iter()
|
|
.map(|(k, v)| (k.to_string(), *v))
|
|
.collect::<HashMap<String, u64>>(),
|
|
});
|
|
serde_json::to_vec(&result).map_err(map_err)
|
|
}
|
|
|
|
pub fn export_public_bundle(identity: IdentityHandle) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<GroupHandle, 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
|
|
.joined_group_handles
|
|
.get(index)
|
|
.copied()
|
|
.ok_or_else(|| format!("no joined group handle at index {index}"))
|
|
}
|
|
|
|
fn with_handle_result<F>(operation: F) -> u64
|
|
where
|
|
F: FnOnce() -> Result<u64, String>,
|
|
{
|
|
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<F>(operation: F) -> bool
|
|
where
|
|
F: FnOnce() -> Result<bool, String>,
|
|
{
|
|
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<F>(operation: F) -> ByteBuffer
|
|
where
|
|
F: FnOnce() -> Result<Vec<u8>, 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<F>(operation: F) -> i64
|
|
where
|
|
F: FnOnce() -> Result<i64, String>,
|
|
{
|
|
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<F>(operation: F) -> i32
|
|
where
|
|
F: FnOnce() -> Result<i32, String>,
|
|
{
|
|
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<i64, String> {
|
|
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<F>(
|
|
opcode: u8,
|
|
session: u64,
|
|
input_fingerprint: u64,
|
|
out_buf: *mut u8,
|
|
out_cap: usize,
|
|
producer: F,
|
|
) -> Result<i64, String>
|
|
where
|
|
F: FnOnce() -> Result<Vec<u8>, 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<String, JsValue> {
|
|
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<Box<[u8]>, 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<Box<[u8]>, 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<Box<[u8]>, 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<Mutex<()>> = 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");
|
|
}
|
|
}
|