app/src-tauri/src/commands/identity.rs

324 lines
9.9 KiB
Rust

use std::time::{SystemTime, UNIX_EPOCH};
use tauri::State;
use crate::node::gossip;
use crate::node::NodeCommand;
use crate::protocol::identity::{DirectoryEntry, DuskIdentity, PublicIdentity};
use crate::protocol::messages::{GossipMessage, ProfileAnnouncement, ProfileRevocation};
use crate::storage::UserSettings;
use crate::verification::{self, ChallengeSubmission};
use crate::AppState;
// build a signed profile announcement and publish it on the directory topic
// so all connected peers immediately learn about the updated profile.
// silently no-ops if the node isn't running yet.
async fn announce_profile(id: &DuskIdentity, state: &AppState) {
let mut announcement = ProfileAnnouncement {
peer_id: id.peer_id.to_string(),
display_name: id.display_name.clone(),
bio: id.bio.clone(),
public_key: hex::encode(id.keypair.public().encode_protobuf()),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
verification_proof: id.verification_proof.clone(),
signature: String::new(),
};
announcement.signature = verification::sign_announcement(&id.keypair, &announcement);
let node_handle = state.node_handle.lock().await;
if let Some(ref handle) = *node_handle {
let msg = GossipMessage::ProfileAnnounce(announcement);
if let Ok(data) = serde_json::to_vec(&msg) {
let _ = handle
.command_tx
.send(NodeCommand::SendMessage {
topic: gossip::topic_for_directory(),
data,
})
.await;
}
}
}
#[tauri::command]
pub async fn has_identity(state: State<'_, AppState>) -> Result<bool, String> {
Ok(state.storage.has_identity())
}
#[tauri::command]
pub async fn load_identity(state: State<'_, AppState>) -> Result<Option<PublicIdentity>, String> {
let mut identity = state.identity.lock().await;
if identity.is_some() {
return Ok(identity.as_ref().map(|id| id.public_identity()));
}
match DuskIdentity::load(&state.storage) {
Ok(loaded) => {
let public = loaded.public_identity();
*identity = Some(loaded);
Ok(Some(public))
}
Err(_) => Ok(None),
}
}
#[tauri::command]
pub async fn create_identity(
state: State<'_, AppState>,
display_name: String,
bio: Option<String>,
challenge_data: Option<ChallengeSubmission>,
) -> Result<PublicIdentity, String> {
// require challenge data and re-validate behavioral analysis in rust
let challenge = challenge_data.ok_or("verification required")?;
let result = verification::analyze_challenge(&challenge);
if !result.is_human {
return Err("verification failed".to_string());
}
let mut new_identity = DuskIdentity::generate(&display_name, &bio.unwrap_or_default());
// generate a cryptographic proof binding the verification to this keypair
let proof = verification::generate_proof(
&challenge,
&new_identity.keypair,
&new_identity.peer_id.to_string(),
)?;
state
.storage
.save_verification_proof(&proof)
.map_err(|e| format!("failed to save verification proof: {}", e))?;
new_identity.verification_proof = Some(proof);
new_identity.save(&state.storage)?;
// also save initial settings with this display name so they're in sync
let mut settings = state.storage.load_settings().unwrap_or_default();
settings.display_name = display_name.clone();
state
.storage
.save_settings(&settings)
.map_err(|e| format!("failed to save initial settings: {}", e))?;
let public = new_identity.public_identity();
let mut identity = state.identity.lock().await;
*identity = Some(new_identity);
Ok(public)
}
#[tauri::command]
pub async fn update_display_name(state: State<'_, AppState>, name: String) -> Result<(), String> {
let mut identity = state.identity.lock().await;
let id = identity.as_mut().ok_or("no identity loaded")?;
id.display_name = name;
id.save(&state.storage)?;
announce_profile(id, &state).await;
Ok(())
}
#[tauri::command]
pub async fn update_profile(
state: State<'_, AppState>,
display_name: String,
bio: String,
) -> Result<PublicIdentity, String> {
let mut identity = state.identity.lock().await;
let id = identity.as_mut().ok_or("no identity loaded")?;
id.display_name = display_name;
id.bio = bio;
id.save(&state.storage)?;
let public = id.public_identity();
// re-announce so connected peers see the change immediately
announce_profile(id, &state).await;
Ok(public)
}
#[tauri::command]
pub async fn load_settings(state: State<'_, AppState>) -> Result<UserSettings, String> {
state
.storage
.load_settings()
.map_err(|e| format!("failed to load settings: {}", e))
}
#[tauri::command]
pub async fn save_settings(
state: State<'_, AppState>,
settings: UserSettings,
) -> Result<(), String> {
// also update the identity display name if it changed
let mut identity = state.identity.lock().await;
let mut name_changed = false;
if let Some(id) = identity.as_mut() {
if id.display_name != settings.display_name {
id.display_name = settings.display_name.clone();
id.save(&state.storage)?;
name_changed = true;
}
}
// re-announce if the display name was updated through settings
if name_changed {
if let Some(id) = identity.as_ref() {
announce_profile(id, &state).await;
}
}
drop(identity);
state
.storage
.save_settings(&settings)
.map_err(|e| format!("failed to save settings: {}", e))
}
// -- user directory commands --
#[tauri::command]
pub async fn get_known_peers(state: State<'_, AppState>) -> Result<Vec<DirectoryEntry>, String> {
let entries = state
.storage
.load_directory()
.map_err(|e| format!("failed to load directory: {}", e))?;
let mut peers: Vec<DirectoryEntry> = entries.into_values().collect();
// sort by last seen (most recent first)
peers.sort_by(|a, b| b.last_seen.cmp(&a.last_seen));
Ok(peers)
}
#[tauri::command]
pub async fn search_directory(
state: State<'_, AppState>,
query: String,
) -> Result<Vec<DirectoryEntry>, String> {
let entries = state
.storage
.load_directory()
.map_err(|e| format!("failed to load directory: {}", e))?;
let query_lower = query.to_lowercase();
let mut results: Vec<DirectoryEntry> = entries
.into_values()
.filter(|entry| {
entry.display_name.to_lowercase().contains(&query_lower)
|| entry.peer_id.to_lowercase().contains(&query_lower)
})
.collect();
results.sort_by(|a, b| b.last_seen.cmp(&a.last_seen));
Ok(results)
}
#[tauri::command]
pub async fn get_friends(state: State<'_, AppState>) -> Result<Vec<DirectoryEntry>, String> {
let entries = state
.storage
.load_directory()
.map_err(|e| format!("failed to load directory: {}", e))?;
let mut friends: Vec<DirectoryEntry> = entries
.into_values()
.filter(|entry| entry.is_friend)
.collect();
friends.sort_by(|a, b| {
a.display_name
.to_lowercase()
.cmp(&b.display_name.to_lowercase())
});
Ok(friends)
}
#[tauri::command]
pub async fn add_friend(state: State<'_, AppState>, peer_id: String) -> Result<(), String> {
state
.storage
.set_friend_status(&peer_id, true)
.map_err(|e| format!("failed to add friend: {}", e))
}
#[tauri::command]
pub async fn remove_friend(state: State<'_, AppState>, peer_id: String) -> Result<(), String> {
state
.storage
.set_friend_status(&peer_id, false)
.map_err(|e| format!("failed to remove friend: {}", e))
}
// broadcast a revocation to all peers, stop the node, and wipe all local data
#[tauri::command]
pub async fn reset_identity(state: State<'_, AppState>) -> Result<(), String> {
let mut identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
// build the revocation message before we destroy the identity
let mut revocation = ProfileRevocation {
peer_id: id.peer_id.to_string(),
public_key: hex::encode(id.keypair.public().encode_protobuf()),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
signature: String::new(),
};
revocation.signature = verification::sign_revocation(&id.keypair, &revocation);
// broadcast revocation on the directory gossip topic
let node_handle = state.node_handle.lock().await;
if let Some(ref handle) = *node_handle {
let msg = GossipMessage::ProfileRevoke(revocation);
if let Ok(data) = serde_json::to_vec(&msg) {
let _ = handle
.command_tx
.send(NodeCommand::SendMessage {
topic: gossip::topic_for_directory(),
data,
})
.await;
}
// give the message a moment to propagate before shutting down
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
drop(node_handle);
// stop the p2p node
{
let mut node_handle = state.node_handle.lock().await;
if let Some(handle) = node_handle.take() {
let _ = handle.command_tx.send(NodeCommand::Shutdown).await;
let _ = handle.task.await;
}
}
// clear the crdt engine so no community data lingers in memory
{
let mut engine = state.crdt_engine.lock().await;
engine.clear();
}
// clear in-memory identity
*identity = None;
// wipe all data from disk
state
.storage
.wipe_all_data()
.map_err(|e| format!("failed to wipe data: {}", e))?;
Ok(())
}