@@ -98,18 +98,34 @@ pub async fn send_message(
9898 peer_id : String ,
9999 body : String ,
100100) -> Result < i64 , String > {
101+ // Parse peer ID from hex
102+ let peer_id_bytes: [ u8 ; 32 ] = hex:: decode ( & peer_id)
103+ . map_err ( |e| format ! ( "Invalid peer ID hex: {}" , e) ) ?
104+ . try_into ( )
105+ . map_err ( |_| "Peer ID must be 32 bytes" ) ?;
106+
101107 let db = state. db . lock ( ) . await ;
102108
103109 // Get or create ratchet for this peer
104110 let mut ratchets = state. ratchets . lock ( ) . await ;
105- let ratchet = ratchets. entry ( peer_id. clone ( ) ) . or_insert_with ( || {
106- // TODO: Initialize with shared secret from key agreement
107- let shared_secret = [ 0u8 ; 32 ] ; // Placeholder
108- DoubleRatchet :: new ( & shared_secret, None ) . unwrap ( )
109- } ) ;
111+ let ratchet = if let Some ( r) = ratchets. get_mut ( & peer_id) {
112+ r
113+ } else {
114+ // Try to load from database
115+ if let Ok ( Some ( state_json) ) = db. load_ratchet_state ( & peer_id) {
116+ let loaded = DoubleRatchet :: from_json ( & state_json) . map_err ( |e| e. to_string ( ) ) ?;
117+ ratchets. insert ( peer_id. clone ( ) , loaded) ;
118+ ratchets. get_mut ( & peer_id) . unwrap ( )
119+ } else {
120+ // No existing session - need to establish one first
121+ return Err (
122+ "No session established with this peer. Call establish_session first." . to_string ( ) ,
123+ ) ;
124+ }
125+ } ;
110126
111127 // Encrypt message with Double Ratchet
112- let _encrypted = ratchet
128+ let encrypted = ratchet
113129 . encrypt ( body. as_bytes ( ) )
114130 . map_err ( |e| e. to_string ( ) ) ?;
115131
@@ -118,11 +134,12 @@ pub async fn send_message(
118134 db. save_ratchet_state ( & peer_id, & ratchet_json)
119135 . map_err ( |e| e. to_string ( ) ) ?;
120136
121- // Create message record
137+ // Create message record (before sending, to track it)
138+ let local_peer_id = state. local_peer_id . lock ( ) . await . clone ( ) ;
122139 let message = Message {
123140 id : 0 ,
124141 conversation_id,
125- sender_peer_id : state . local_peer_id . lock ( ) . await . clone ( ) ,
142+ sender_peer_id : local_peer_id,
126143 content_type : "text" . to_string ( ) ,
127144 body : Some ( body) ,
128145 media_path : None ,
@@ -138,12 +155,28 @@ pub async fn send_message(
138155
139156 let message_id = db. insert_message ( & message) . map_err ( |e| e. to_string ( ) ) ?;
140157
141- // TODO: Send encrypted message via WRAITH protocol
142- // let node = state.node.lock().await;
143- // node.send_message(&peer_id, &encrypted)?;
144-
145- // Mark as sent (for now, immediately mark as sent)
146- // In production, this would be updated after WRAITH protocol confirms delivery
158+ // Serialize encrypted message to send over the wire
159+ let encrypted_bytes = serde_json:: to_vec ( & encrypted)
160+ . map_err ( |e| format ! ( "Failed to serialize encrypted message: {}" , e) ) ?;
161+
162+ // Send encrypted message via WRAITH protocol
163+ let node = state. node . lock ( ) . await ;
164+ if node. is_running ( ) {
165+ match node. send_data ( & peer_id_bytes, & encrypted_bytes) . await {
166+ Ok ( ( ) ) => {
167+ // Update message as sent
168+ db. mark_message_sent ( message_id)
169+ . map_err ( |e| e. to_string ( ) ) ?;
170+ log:: debug!( "Message {} sent successfully" , message_id) ;
171+ }
172+ Err ( e) => {
173+ log:: warn!( "Failed to send message via WRAITH protocol: {}" , e) ;
174+ // Message is saved but not marked as sent - can retry later
175+ }
176+ }
177+ } else {
178+ log:: warn!( "WRAITH node not running, message saved but not sent" ) ;
179+ }
147180
148181 Ok ( message_id)
149182}
@@ -257,37 +290,218 @@ pub async fn start_node(
257290 state : State < ' _ , Arc < AppState > > ,
258291 listen_addr : String ,
259292) -> Result < ( ) , String > {
260- // TODO: Initialize WRAITH node
261293 log:: info!( "Starting WRAITH node on {}" , listen_addr) ;
262294
263- let mut peer_id = state. local_peer_id . lock ( ) . await ;
264- * peer_id = "local-peer-id-placeholder" . to_string ( ) ; // TODO: Get from node
295+ let mut node = state. node . lock ( ) . await ;
296+
297+ // Parse listen address if provided
298+ let config = if !listen_addr. is_empty ( ) {
299+ let addr: std:: net:: SocketAddr = listen_addr
300+ . parse ( )
301+ . map_err ( |e| format ! ( "Invalid listen address: {}" , e) ) ?;
302+ wraith_core:: node:: NodeConfig {
303+ listen_addr : addr,
304+ ..Default :: default ( )
305+ }
306+ } else {
307+ wraith_core:: node:: NodeConfig :: default ( )
308+ } ;
309+
310+ // Initialize node if not already done
311+ if node. node ( ) . is_none ( ) {
312+ node. initialize_with_config ( config) . await ?;
313+ }
314+
315+ // Start the node
316+ node. start ( ) . await ?;
317+
318+ // Update local peer ID cache
319+ if let Some ( peer_id) = node. peer_id ( ) {
320+ let mut local_peer_id = state. local_peer_id . lock ( ) . await ;
321+ * local_peer_id = peer_id;
322+ }
265323
324+ log:: info!( "WRAITH node started successfully" ) ;
325+ Ok ( ( ) )
326+ }
327+
328+ #[ tauri:: command]
329+ pub async fn stop_node ( state : State < ' _ , Arc < AppState > > ) -> Result < ( ) , String > {
330+ log:: info!( "Stopping WRAITH node" ) ;
331+
332+ let mut node = state. node . lock ( ) . await ;
333+ node. stop ( ) . await ?;
334+
335+ // Clear local peer ID cache
336+ let mut local_peer_id = state. local_peer_id . lock ( ) . await ;
337+ local_peer_id. clear ( ) ;
338+
339+ log:: info!( "WRAITH node stopped" ) ;
266340 Ok ( ( ) )
267341}
268342
269343#[ tauri:: command]
270344pub async fn get_node_status ( state : State < ' _ , Arc < AppState > > ) -> Result < NodeStatus , String > {
345+ let node = state. node . lock ( ) . await ;
271346 let peer_id = state. local_peer_id . lock ( ) . await ;
272347
273- // Get session count from active ratchets (cryptographic sessions)
274- let ratchets = state. ratchets . lock ( ) . await ;
275- let session_count = ratchets. len ( ) ;
348+ // Get session count from WRAITH node
349+ let session_count = node. active_route_count ( ) ;
276350
277351 // Get conversation count from database
278352 let db = state. db . lock ( ) . await ;
279353 let active_conversations = db. count_conversations ( ) . unwrap_or ( 0 ) ;
280354
281355 Ok ( NodeStatus {
282- running : !peer_id . is_empty ( ) ,
356+ running : node . is_running ( ) ,
283357 local_peer_id : peer_id. clone ( ) ,
284358 session_count,
285359 active_conversations,
286360 } )
287361}
288362
363+ #[ tauri:: command]
364+ pub async fn get_peer_id ( state : State < ' _ , Arc < AppState > > ) -> Result < String , String > {
365+ let node = state. node . lock ( ) . await ;
366+ node. peer_id ( )
367+ . ok_or_else ( || "Node not initialized" . to_string ( ) )
368+ }
369+
370+ // MARK: - Session Commands
371+
372+ /// Establish an encrypted session with a peer
373+ ///
374+ /// This performs a Noise_XX handshake via the WRAITH protocol and initializes
375+ /// a Double Ratchet for forward-secret message encryption.
376+ #[ tauri:: command]
377+ pub async fn establish_session (
378+ state : State < ' _ , Arc < AppState > > ,
379+ peer_id_hex : String ,
380+ ) -> Result < SessionInfo , String > {
381+ // Parse peer ID from hex
382+ let peer_id_bytes: [ u8 ; 32 ] = hex:: decode ( & peer_id_hex)
383+ . map_err ( |e| format ! ( "Invalid peer ID hex: {}" , e) ) ?
384+ . try_into ( )
385+ . map_err ( |_| "Peer ID must be 32 bytes" ) ?;
386+
387+ // Establish WRAITH session
388+ let node = state. node . lock ( ) . await ;
389+ let session_id = node
390+ . establish_session ( & peer_id_bytes)
391+ . await
392+ . map_err ( |e| format ! ( "Failed to establish session: {}" , e) ) ?;
393+
394+ // Get the X25519 public key from the node for the Double Ratchet
395+ let our_x25519_pub = node. x25519_public_key ( ) . ok_or ( "Node not initialized" ) ?;
396+
397+ // Derive a shared secret for the Double Ratchet from the session ID
398+ // The session ID is derived from the Noise handshake, so it's a secure source
399+ let shared_secret = derive_ratchet_secret ( & session_id, & peer_id_bytes) ;
400+
401+ // Initialize Double Ratchet with the shared secret
402+ // We're the initiator, so we don't have the remote's DH public key yet
403+ let ratchet = DoubleRatchet :: new ( & shared_secret, None )
404+ . map_err ( |e| format ! ( "Failed to create Double Ratchet: {}" , e) ) ?;
405+
406+ // Store ratchet state
407+ let mut ratchets = state. ratchets . lock ( ) . await ;
408+ ratchets. insert ( peer_id_hex. clone ( ) , ratchet) ;
409+
410+ // Also save to database
411+ let db = state. db . lock ( ) . await ;
412+ let ratchet = ratchets. get ( & peer_id_hex) . unwrap ( ) ;
413+ let ratchet_json = ratchet. to_json ( ) . map_err ( |e| e. to_string ( ) ) ?;
414+ db. save_ratchet_state ( & peer_id_hex, & ratchet_json)
415+ . map_err ( |e| e. to_string ( ) ) ?;
416+
417+ log:: info!(
418+ "Established encrypted session with peer {}" ,
419+ & peer_id_hex[ ..16 ]
420+ ) ;
421+
422+ Ok ( SessionInfo {
423+ session_id : hex:: encode ( session_id) ,
424+ peer_id : peer_id_hex,
425+ our_public_key : hex:: encode ( our_x25519_pub) ,
426+ } )
427+ }
428+
429+ /// Initialize a receiving session (when we receive a connection from a peer)
430+ ///
431+ /// This is called when we receive a message from a peer we haven't communicated with yet.
432+ #[ tauri:: command]
433+ pub async fn init_receiving_session (
434+ state : State < ' _ , Arc < AppState > > ,
435+ peer_id_hex : String ,
436+ remote_public_key : Vec < u8 > ,
437+ ) -> Result < ( ) , String > {
438+ // Parse peer ID from hex
439+ let peer_id_bytes: [ u8 ; 32 ] = hex:: decode ( & peer_id_hex)
440+ . map_err ( |e| format ! ( "Invalid peer ID hex: {}" , e) ) ?
441+ . try_into ( )
442+ . map_err ( |_| "Peer ID must be 32 bytes" ) ?;
443+
444+ // Derive shared secret (receiver perspective)
445+ let node = state. node . lock ( ) . await ;
446+
447+ // We need a session ID - try to get from existing session
448+ let session_id = match node. node ( ) {
449+ Some ( n) => {
450+ // Get session ID from any existing connection
451+ let sessions = n. active_sessions ( ) . await ;
452+ if let Some ( sid) = sessions. first ( ) {
453+ * sid
454+ } else {
455+ // Generate a deterministic placeholder from peer ID
456+ // This will be replaced when the actual session is established
457+ peer_id_bytes
458+ }
459+ }
460+ None => peer_id_bytes, // Fallback
461+ } ;
462+
463+ let shared_secret = derive_ratchet_secret ( & session_id, & peer_id_bytes) ;
464+
465+ // Initialize Double Ratchet with remote's public key (we're the responder)
466+ let ratchet = DoubleRatchet :: new ( & shared_secret, Some ( & remote_public_key) )
467+ . map_err ( |e| format ! ( "Failed to create Double Ratchet: {}" , e) ) ?;
468+
469+ // Store ratchet state
470+ let mut ratchets = state. ratchets . lock ( ) . await ;
471+ ratchets. insert ( peer_id_hex. clone ( ) , ratchet) ;
472+
473+ // Also save to database
474+ let db = state. db . lock ( ) . await ;
475+ let ratchet = ratchets. get ( & peer_id_hex) . unwrap ( ) ;
476+ let ratchet_json = ratchet. to_json ( ) . map_err ( |e| e. to_string ( ) ) ?;
477+ db. save_ratchet_state ( & peer_id_hex, & ratchet_json)
478+ . map_err ( |e| e. to_string ( ) ) ?;
479+
480+ log:: info!(
481+ "Initialized receiving session with peer {}" ,
482+ & peer_id_hex[ ..16 ]
483+ ) ;
484+
485+ Ok ( ( ) )
486+ }
487+
289488// MARK: - Helper Functions
290489
490+ /// Derive a shared secret for the Double Ratchet from session data
491+ fn derive_ratchet_secret ( session_id : & [ u8 ; 32 ] , peer_id : & [ u8 ; 32 ] ) -> [ u8 ; 32 ] {
492+ use sha2:: { Digest , Sha256 } ;
493+
494+ let mut hasher = Sha256 :: new ( ) ;
495+ hasher. update ( b"wraith-chat-ratchet-secret-v1" ) ;
496+ hasher. update ( session_id) ;
497+ hasher. update ( peer_id) ;
498+ let hash = hasher. finalize ( ) ;
499+
500+ let mut secret = [ 0u8 ; 32 ] ;
501+ secret. copy_from_slice ( & hash) ;
502+ secret
503+ }
504+
291505fn generate_safety_number ( peer_id : & str , identity_key : & [ u8 ] ) -> String {
292506 use sha2:: { Digest , Sha256 } ;
293507
@@ -318,3 +532,10 @@ pub struct NodeStatus {
318532 pub session_count : usize ,
319533 pub active_conversations : usize ,
320534}
535+
536+ #[ derive( Debug , serde:: Serialize ) ]
537+ pub struct SessionInfo {
538+ pub session_id : String ,
539+ pub peer_id : String ,
540+ pub our_public_key : String ,
541+ }
0 commit comments