use crate::identity_to_seed;
use crate::message_sync::MessageSyncService;
use crate::types::{DeviceType, UserProfile};
use fips204::traits::{SerDes, Signer, Verifier};
use rand_chacha::ChaCha8Rng;
use rand_chacha::rand_core::SeedableRng;
use saorsa_pqc::ml_dsa_87::{PrivateKey, PublicKey, try_keygen_with_rng};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::info;
pub struct CoreContext {
pub profile: UserProfile,
pub signing_key: PrivateKey,
pub public_key: PublicKey,
pub four_words: String,
pub display_name: String,
pub device_name: String,
pub crdt_manager: Arc<crate::CrdtManager>,
pub entity_service: Arc<crate::EntityService>,
pub message_service: Arc<crate::MessageService>,
pub message_sync: Arc<MessageSyncService>,
pub doc_replicator: Arc<crate::doc_replicator::DocReplicator>,
pub listen_address: Option<SocketAddr>,
pub connection_identity: Option<String>,
pub gossip: Option<Arc<crate::gossip::GossipContext>>,
pub group_keys: HashMap<String, GroupKeyPairPlaceholder>,
}
impl std::fmt::Debug for CoreContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CoreContext")
.field("profile", &self.profile)
.field("four_words", &self.four_words)
.field("display_name", &self.display_name)
.field("device_name", &self.device_name)
.field("crdt_manager", &"<active>")
.field("entity_service", &"<active>")
.field("message_service", &"<active>")
.field("listen_address", &self.listen_address)
.field("connection_identity", &self.connection_identity)
.field("signing_key", &"<redacted>")
.field("public_key", &"<key_bytes>")
.field("doc_replicator", &"<active>")
.field("gossip", &self.gossip.as_ref().map(|_| "<active>"))
.field("group_keys", &self.group_keys)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct GroupKeyPairPlaceholder {
pub group_id: String,
}
impl CoreContext {
pub async fn initialize(
four_words: String,
display_name: String,
device_name: String,
device_type: DeviceType,
storage_dir: PathBuf,
) -> Result<Self, String> {
let words: Vec<&str> = four_words.split('-').collect();
if words.len() != 4 {
return Err(format!(
"Invalid four-word format: expected 4 words, got {}",
words.len()
));
}
let seed = identity_to_seed(&four_words)
.map_err(|e| format!("Failed to derive seed from identity: {}", e))?;
let mut rng = ChaCha8Rng::from_seed(seed);
let (public_key, signing_key) = try_keygen_with_rng(&mut rng)
.map_err(|e| format!("Failed to generate ML-DSA-87 keypair: {}", e))?;
let pubkey_bytes = public_key.clone().into_bytes();
info!(
"Generated ML-DSA-87 keypair from identity '{}' (Level 5 PQC security)",
four_words
);
if !storage_dir.exists() {
std::fs::create_dir_all(&storage_dir).map_err(|e| {
format!(
"Failed to create storage directory {:?}: {}",
storage_dir, e
)
})?;
}
let pubkey_array: [u8; 32] = pubkey_bytes[..32]
.try_into()
.map_err(|_| "Public key too short".to_string())?;
let profile = UserProfile::new(
four_words.clone(),
display_name.clone(),
pubkey_array,
device_type,
storage_dir.clone(),
);
let crdt_manager = Arc::new(
crate::CrdtManager::new(&storage_dir.join("crdt.db"))
.await
.map_err(|e| format!("Failed to initialize CrdtManager: {}", e))?,
);
let entity_service = Arc::new(crate::EntityService::new(crdt_manager.clone()));
let message_service = Arc::new(crate::MessageService::new(four_words.clone()));
let message_sync = Arc::new(MessageSyncService::new(four_words.clone()));
let doc_config = crate::doc_replicator::DocReplicatorConfig {
files_storage_enabled: true,
web_storage_enabled: true,
};
let doc_replicator = Arc::new(
crate::doc_replicator::DocReplicator::new(doc_config)
.await
.map_err(|e| format!("Failed to initialize DocReplicator: {}", e))?,
);
info!(
"CoreContext initialized for user '{}' ({}) with EntityService, MessageService, and DocReplicator",
display_name, four_words
);
Ok(Self {
profile,
signing_key,
public_key,
four_words,
display_name,
device_name,
crdt_manager,
entity_service,
message_service,
message_sync,
doc_replicator,
listen_address: None,
connection_identity: None,
gossip: None,
group_keys: HashMap::new(),
})
}
pub async fn start_networking(
&mut self,
preferred_port: Option<u16>,
) -> Result<String, String> {
info!("Starting gossip networking for {}", self.four_words);
let mut port_manager = if let Some(port) = preferred_port {
crate::gossip::PortManager::with_preferred_port(port)
} else {
crate::gossip::PortManager::new()
};
let listen_port = port_manager
.allocate_port()
.map_err(|e| format!("Failed to allocate port: {}", e))?;
info!("Allocated port {} for QUIC transport", listen_port);
let gossip = crate::gossip::GossipContext::initialize(
self.four_words.clone(),
self.display_name.clone(),
self.device_name.clone(),
)
.await
.map_err(|e| format!("Failed to initialize gossip: {}", e))?;
let local_ip =
local_ip_address::local_ip().map_err(|e| format!("Failed to get local IP: {}", e))?;
let listen_addr = std::net::SocketAddr::new(local_ip, listen_port);
let connection_identity = crate::conn_words(&listen_addr)
.map_err(|e| format!("Failed to encode connection address: {}", e))?;
info!(
"Gossip networking started on {} ({})",
listen_addr, connection_identity
);
self.listen_address = Some(listen_addr);
self.connection_identity = Some(connection_identity.clone());
self.gossip = Some(Arc::new(gossip));
Ok(connection_identity)
}
pub async fn stop_networking(&mut self) -> Result<(), String> {
if let Some(_gossip) = self.gossip.take() {
info!("Stopping gossip networking for {}", self.four_words);
self.listen_address = None;
self.connection_identity = None;
}
Ok(())
}
pub async fn connect_to_peer(&self, peer_four_words: &str) -> Result<(), String> {
let gossip = self
.gossip
.as_ref()
.ok_or("Networking not started. Call start_networking() first")?;
info!("Adding peer {} to favourites", peer_four_words);
gossip
.add_favourite_contact(peer_four_words.to_string())
.await
.map_err(|e| format!("Failed to add favourite contact: {}", e))?;
info!(
"Peer {} added. FOAF discovery will locate and connect automatically",
peer_four_words
);
Ok(())
}
pub fn add_group_key(&mut self, group_id: String, key: GroupKeyPairPlaceholder) {
self.group_keys.insert(group_id, key);
}
pub fn get_public_key(&self) -> &PublicKey {
&self.public_key
}
pub fn public_key_bytes(&self) -> [u8; 2592] {
self.public_key.clone().into_bytes()
}
pub fn sign(&self, message: &[u8]) -> Result<[u8; 4627], String> {
self.signing_key
.try_sign(message, &[]) .map_err(|e| format!("ML-DSA-87 signing failed: {}", e))
}
pub fn verify(&self, message: &[u8], signature: &[u8; 4627]) -> bool {
self.public_key.verify(message, signature, &[]) }
pub fn storage_dir(&self) -> &PathBuf {
&self.profile.storage_dir
}
pub fn is_networking_active(&self) -> bool {
self.gossip.is_some() && self.listen_address.is_some()
}
pub fn connection_identity(&self) -> Option<&str> {
self.connection_identity.as_deref()
}
pub fn set_display_name(&mut self, display_name: String) {
self.display_name = display_name.clone();
self.profile.display_name = display_name;
}
pub fn device_type(&self) -> DeviceType {
self.profile.device_type
}
pub fn has_passkey(&self) -> bool {
self.profile.has_passkey()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_core_context_initialization() {
let temp_dir = TempDir::new().unwrap();
let storage_dir = temp_dir.path().to_path_buf();
let context = CoreContext::initialize(
"ocean-forest-moon-star".to_string(),
"Test User".to_string(),
"Test Device".to_string(),
DeviceType::Desktop,
storage_dir.clone(),
)
.await;
assert!(context.is_ok());
let ctx = context.unwrap();
assert_eq!(ctx.display_name, "Test User");
assert_eq!(ctx.device_name, "Test Device");
assert_eq!(ctx.profile.device_type, DeviceType::Desktop);
assert_eq!(ctx.four_words, "ocean-forest-moon-star");
assert!(!ctx.is_networking_active());
assert!(storage_dir.exists());
}
#[tokio::test]
async fn test_invalid_four_word_format() {
let temp_dir = TempDir::new().unwrap();
let context = CoreContext::initialize(
"only-three-words".to_string(),
"Test User".to_string(),
"Test Device".to_string(),
DeviceType::Desktop,
temp_dir.path().to_path_buf(),
)
.await;
assert!(context.is_err());
assert!(context.unwrap_err().contains("Invalid four-word format"));
}
#[tokio::test]
async fn test_display_name_update() {
let temp_dir = TempDir::new().unwrap();
let mut context = CoreContext::initialize(
"ocean-forest-moon-star".to_string(),
"Old Name".to_string(),
"Test Device".to_string(),
DeviceType::Desktop,
temp_dir.path().to_path_buf(),
)
.await
.unwrap();
context.set_display_name("New Name".to_string());
assert_eq!(context.display_name, "New Name");
assert_eq!(context.profile.display_name, "New Name");
}
#[tokio::test]
async fn test_signing() {
let temp_dir = TempDir::new().unwrap();
let context = CoreContext::initialize(
"ocean-forest-moon-star".to_string(),
"Test User".to_string(),
"Test Device".to_string(),
DeviceType::Desktop,
temp_dir.path().to_path_buf(),
)
.await
.unwrap();
let message = b"test message";
let signature = context.sign(message).unwrap();
assert!(context.verify(message, &signature));
let wrong_message = b"wrong message";
assert!(!context.verify(wrong_message, &signature));
}
#[tokio::test]
async fn test_group_key_management() {
let temp_dir = TempDir::new().unwrap();
let mut context = CoreContext::initialize(
"ocean-forest-moon-star".to_string(),
"Test User".to_string(),
"Test Device".to_string(),
DeviceType::Desktop,
temp_dir.path().to_path_buf(),
)
.await
.unwrap();
let group_id = "test-group".to_string();
let key = GroupKeyPairPlaceholder {
group_id: group_id.clone(),
};
context.add_group_key(group_id.clone(), key);
assert!(context.group_keys.contains_key(&group_id));
}
#[tokio::test]
async fn test_networking_not_active_by_default() {
let temp_dir = TempDir::new().unwrap();
let context = CoreContext::initialize(
"ocean-forest-moon-star".to_string(),
"Test User".to_string(),
"Test Device".to_string(),
DeviceType::Desktop,
temp_dir.path().to_path_buf(),
)
.await
.unwrap();
assert!(!context.is_networking_active());
assert!(context.connection_identity().is_none());
}
}