Skip to main content

ap_client/clients/
remote_client.rs

1use std::time::Duration;
2
3use ap_noise::{InitiatorHandshake, MultiDeviceTransport, Psk};
4use ap_proxy_client::IncomingMessage;
5use ap_proxy_protocol::{IdentityFingerprint, RendezvousCode};
6use base64::{Engine, engine::general_purpose::STANDARD};
7use rand::RngCore;
8
9use crate::compat::{now_millis, timeout};
10use crate::proxy::ProxyClient;
11use tokio::sync::{mpsc, oneshot};
12use tracing::{debug, warn};
13
14use crate::traits::{IdentityProvider, SessionStore};
15use crate::{
16    error::ClientError,
17    types::{
18        CredentialData, CredentialQuery, CredentialRequestPayload, CredentialResponsePayload,
19        ProtocolMessage,
20    },
21};
22
23const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
24
25// =============================================================================
26// Public types: Notifications (fire-and-forget) and Requests (with reply)
27// =============================================================================
28
29/// Fire-and-forget status updates emitted by the remote client.
30#[derive(Debug, Clone)]
31pub enum RemoteClientNotification {
32    /// Connecting to the proxy server
33    Connecting,
34    /// Successfully connected to the proxy
35    Connected {
36        /// The device's identity fingerprint (hex-encoded)
37        fingerprint: IdentityFingerprint,
38    },
39    /// Reconnecting to an existing session
40    ReconnectingToSession {
41        /// The fingerprint being reconnected to
42        fingerprint: IdentityFingerprint,
43    },
44    /// Rendezvous code resolution starting
45    RendezvousResolving {
46        /// The rendezvous code being resolved
47        code: String,
48    },
49    /// Rendezvous code resolved to fingerprint
50    RendezvousResolved {
51        /// The resolved identity fingerprint
52        fingerprint: IdentityFingerprint,
53    },
54    /// Using PSK mode for connection
55    PskMode {
56        /// The fingerprint being connected to
57        fingerprint: IdentityFingerprint,
58    },
59    /// Noise handshake starting
60    HandshakeStart,
61    /// Noise handshake progress
62    HandshakeProgress {
63        /// Progress message
64        message: String,
65    },
66    /// Noise handshake complete
67    HandshakeComplete,
68    /// Handshake fingerprint (informational — for PSK or non-verified connections)
69    HandshakeFingerprint {
70        /// The 6-character hex fingerprint
71        fingerprint: String,
72    },
73    /// User verified the fingerprint
74    FingerprintVerified,
75    /// User rejected the fingerprint
76    FingerprintRejected {
77        /// Reason for rejection
78        reason: String,
79    },
80    /// Client is ready for credential requests
81    Ready {
82        /// Whether credentials can be requested
83        can_request_credentials: bool,
84    },
85    /// Credential request was sent
86    CredentialRequestSent {
87        /// The query used for the request
88        query: CredentialQuery,
89    },
90    /// Credential was received
91    CredentialReceived {
92        /// The credential data
93        credential: CredentialData,
94    },
95    /// An error occurred
96    Error {
97        /// Error message
98        message: String,
99        /// Context where error occurred
100        context: Option<String>,
101    },
102    /// Client was disconnected
103    Disconnected {
104        /// Reason for disconnection
105        reason: Option<String>,
106    },
107}
108
109/// Reply for fingerprint verification requests.
110#[derive(Debug)]
111pub struct RemoteClientFingerprintReply {
112    /// Whether user approved the fingerprint
113    pub approved: bool,
114}
115
116/// Requests that require a caller response, carrying a oneshot reply channel.
117#[derive(Debug)]
118pub enum RemoteClientRequest {
119    /// Handshake fingerprint requires verification.
120    VerifyFingerprint {
121        /// The 6-character hex fingerprint for visual verification
122        fingerprint: String,
123        /// Channel to send the verification reply
124        reply: oneshot::Sender<RemoteClientFingerprintReply>,
125    },
126}
127
128// =============================================================================
129// Command channel for RemoteClient handle → event loop communication
130// =============================================================================
131
132/// Type alias matching `SessionStore::list_sessions()` return type.
133type SessionList = Vec<(IdentityFingerprint, Option<String>, u64, u64)>;
134
135/// Commands sent from a `RemoteClient` handle to the running event loop.
136enum RemoteClientCommand {
137    PairWithHandshake {
138        rendezvous_code: String,
139        verify_fingerprint: bool,
140        reply: oneshot::Sender<Result<IdentityFingerprint, ClientError>>,
141    },
142    PairWithPsk {
143        psk: Psk,
144        remote_fingerprint: IdentityFingerprint,
145        reply: oneshot::Sender<Result<(), ClientError>>,
146    },
147    LoadCachedSession {
148        remote_fingerprint: IdentityFingerprint,
149        reply: oneshot::Sender<Result<(), ClientError>>,
150    },
151    RequestCredential {
152        query: CredentialQuery,
153        reply: oneshot::Sender<Result<CredentialData, ClientError>>,
154    },
155    ListSessions {
156        reply: oneshot::Sender<SessionList>,
157    },
158    HasSession {
159        fingerprint: IdentityFingerprint,
160        reply: oneshot::Sender<bool>,
161    },
162}
163
164// =============================================================================
165// Handle — cloneable, Send, all methods take &self
166// =============================================================================
167
168/// A cloneable handle for controlling the remote client.
169///
170/// Obtained from [`RemoteClient::connect()`], which authenticates with the proxy,
171/// spawns the event loop internally, and returns this handle. All methods
172/// communicate with the event loop through an internal command channel.
173///
174/// `Clone` and `Send` — share freely across tasks and threads.
175/// Dropping all handles shuts down the event loop and disconnects from the proxy.
176#[derive(Clone)]
177pub struct RemoteClient {
178    command_tx: mpsc::Sender<RemoteClientCommand>,
179}
180
181impl RemoteClient {
182    /// Connect to the proxy server, spawn the event loop, and return a handle.
183    ///
184    /// This is the single entry point. After `connect()` returns, the client is
185    /// authenticated with the proxy and ready for pairing. Use one of the pairing
186    /// methods to establish a secure channel:
187    /// - [`pair_with_handshake()`](Self::pair_with_handshake) for rendezvous-based pairing
188    /// - [`pair_with_psk()`](Self::pair_with_psk) for PSK-based pairing
189    /// - [`load_cached_session()`](Self::load_cached_session) for reconnecting with a cached session
190    pub async fn connect(
191        identity_provider: Box<dyn IdentityProvider>,
192        session_store: Box<dyn SessionStore>,
193        mut proxy_client: Box<dyn ProxyClient>,
194        notification_tx: mpsc::Sender<RemoteClientNotification>,
195        request_tx: mpsc::Sender<RemoteClientRequest>,
196    ) -> Result<Self, ClientError> {
197        let own_fingerprint = identity_provider.fingerprint().await;
198
199        debug!("Connecting to proxy with identity {:?}", own_fingerprint);
200
201        notification_tx
202            .send(RemoteClientNotification::Connecting)
203            .await
204            .ok();
205
206        let incoming_rx = proxy_client.connect().await?;
207
208        notification_tx
209            .send(RemoteClientNotification::Connected {
210                fingerprint: own_fingerprint,
211            })
212            .await
213            .ok();
214
215        debug!("Connected to proxy successfully");
216
217        // Create command channel
218        let (command_tx, command_rx) = mpsc::channel(32);
219
220        // Build inner state
221        let inner = RemoteClientInner {
222            session_store,
223            proxy_client,
224            transport: None,
225            remote_fingerprint: None,
226        };
227
228        // Spawn the event loop — use spawn_local on WASM (no Tokio runtime)
229        #[cfg(target_arch = "wasm32")]
230        wasm_bindgen_futures::spawn_local(inner.run_event_loop(
231            incoming_rx,
232            command_rx,
233            notification_tx,
234            request_tx,
235        ));
236        #[cfg(not(target_arch = "wasm32"))]
237        tokio::spawn(inner.run_event_loop(incoming_rx, command_rx, notification_tx, request_tx));
238
239        Ok(Self { command_tx })
240    }
241
242    /// Pair with a remote device using a rendezvous code.
243    ///
244    /// Resolves the rendezvous code to a fingerprint, performs the Noise handshake,
245    /// and optionally waits for user fingerprint verification. If `verify_fingerprint`
246    /// is true, a [`RemoteClientRequest::VerifyFingerprint`] will be sent on the
247    /// request channel and must be answered before this method returns.
248    pub async fn pair_with_handshake(
249        &self,
250        rendezvous_code: String,
251        verify_fingerprint: bool,
252    ) -> Result<IdentityFingerprint, ClientError> {
253        let (tx, rx) = oneshot::channel();
254        self.command_tx
255            .send(RemoteClientCommand::PairWithHandshake {
256                rendezvous_code,
257                verify_fingerprint,
258                reply: tx,
259            })
260            .await
261            .map_err(|_| ClientError::ChannelClosed)?;
262        rx.await.map_err(|_| ClientError::ChannelClosed)?
263    }
264
265    /// Pair with a remote device using a pre-shared key.
266    ///
267    /// Uses the PSK for authentication, skipping fingerprint verification
268    /// since trust is established through the PSK.
269    pub async fn pair_with_psk(
270        &self,
271        psk: Psk,
272        remote_fingerprint: IdentityFingerprint,
273    ) -> Result<(), ClientError> {
274        let (tx, rx) = oneshot::channel();
275        self.command_tx
276            .send(RemoteClientCommand::PairWithPsk {
277                psk,
278                remote_fingerprint,
279                reply: tx,
280            })
281            .await
282            .map_err(|_| ClientError::ChannelClosed)?;
283        rx.await.map_err(|_| ClientError::ChannelClosed)?
284    }
285
286    /// Reconnect to a remote device using a cached session.
287    ///
288    /// Verifies the session exists in the session store and reconnects
289    /// without requiring fingerprint verification.
290    pub async fn load_cached_session(
291        &self,
292        remote_fingerprint: IdentityFingerprint,
293    ) -> Result<(), ClientError> {
294        let (tx, rx) = oneshot::channel();
295        self.command_tx
296            .send(RemoteClientCommand::LoadCachedSession {
297                remote_fingerprint,
298                reply: tx,
299            })
300            .await
301            .map_err(|_| ClientError::ChannelClosed)?;
302        rx.await.map_err(|_| ClientError::ChannelClosed)?
303    }
304
305    /// Request a credential over the secure channel.
306    pub async fn request_credential(
307        &self,
308        query: &CredentialQuery,
309    ) -> Result<CredentialData, ClientError> {
310        let (tx, rx) = oneshot::channel();
311        self.command_tx
312            .send(RemoteClientCommand::RequestCredential {
313                query: query.clone(),
314                reply: tx,
315            })
316            .await
317            .map_err(|_| ClientError::ChannelClosed)?;
318        rx.await.map_err(|_| ClientError::ChannelClosed)?
319    }
320
321    /// List all cached sessions.
322    pub async fn list_sessions(
323        &self,
324    ) -> Result<Vec<(IdentityFingerprint, Option<String>, u64, u64)>, ClientError> {
325        let (tx, rx) = oneshot::channel();
326        self.command_tx
327            .send(RemoteClientCommand::ListSessions { reply: tx })
328            .await
329            .map_err(|_| ClientError::ChannelClosed)?;
330        rx.await.map_err(|_| ClientError::ChannelClosed)
331    }
332
333    /// Check if a session exists for a fingerprint.
334    pub async fn has_session(&self, fingerprint: IdentityFingerprint) -> Result<bool, ClientError> {
335        let (tx, rx) = oneshot::channel();
336        self.command_tx
337            .send(RemoteClientCommand::HasSession {
338                fingerprint,
339                reply: tx,
340            })
341            .await
342            .map_err(|_| ClientError::ChannelClosed)?;
343        rx.await.map_err(|_| ClientError::ChannelClosed)
344    }
345}
346
347// =============================================================================
348// Internal state — lives inside the spawned event loop task
349// =============================================================================
350
351/// All mutable state for the remote client, owned by the spawned event loop task.
352struct RemoteClientInner {
353    session_store: Box<dyn SessionStore>,
354    proxy_client: Box<dyn ProxyClient>,
355    transport: Option<MultiDeviceTransport>,
356    remote_fingerprint: Option<IdentityFingerprint>,
357}
358
359impl RemoteClientInner {
360    /// Run the main event loop (consumes self).
361    async fn run_event_loop(
362        mut self,
363        mut incoming_rx: mpsc::UnboundedReceiver<IncomingMessage>,
364        mut command_rx: mpsc::Receiver<RemoteClientCommand>,
365        notification_tx: mpsc::Sender<RemoteClientNotification>,
366        request_tx: mpsc::Sender<RemoteClientRequest>,
367    ) {
368        loop {
369            tokio::select! {
370                msg = incoming_rx.recv() => {
371                    match msg {
372                        Some(_) => {
373                            // Stray message while idle — ignore
374                            debug!("Received message while idle");
375                        }
376                        None => {
377                            // Proxy disconnected
378                            notification_tx.send(RemoteClientNotification::Disconnected {
379                                reason: Some("Proxy connection closed".to_string()),
380                            }).await.ok();
381                            return;
382                        }
383                    }
384                }
385                cmd = command_rx.recv() => {
386                    match cmd {
387                        Some(cmd) => {
388                            self.handle_command(
389                                cmd,
390                                &mut incoming_rx,
391                                &notification_tx,
392                                &request_tx,
393                            ).await;
394                        }
395                        None => {
396                            // All handles dropped — shut down
397                            debug!("All RemoteClient handles dropped, shutting down event loop");
398                            self.proxy_client.disconnect().await.ok();
399                            return;
400                        }
401                    }
402                }
403            }
404        }
405    }
406
407    /// Dispatch a command from the handle.
408    async fn handle_command(
409        &mut self,
410        cmd: RemoteClientCommand,
411        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
412        notification_tx: &mpsc::Sender<RemoteClientNotification>,
413        request_tx: &mpsc::Sender<RemoteClientRequest>,
414    ) {
415        match cmd {
416            RemoteClientCommand::PairWithHandshake {
417                rendezvous_code,
418                verify_fingerprint,
419                reply,
420            } => {
421                let result = self
422                    .do_pair_with_handshake(
423                        rendezvous_code,
424                        verify_fingerprint,
425                        incoming_rx,
426                        notification_tx,
427                        request_tx,
428                    )
429                    .await;
430                let _ = reply.send(result);
431            }
432            RemoteClientCommand::PairWithPsk {
433                psk,
434                remote_fingerprint,
435                reply,
436            } => {
437                let result = self
438                    .do_pair_with_psk(psk, remote_fingerprint, incoming_rx, notification_tx)
439                    .await;
440                let _ = reply.send(result);
441            }
442            RemoteClientCommand::LoadCachedSession {
443                remote_fingerprint,
444                reply,
445            } => {
446                let result = self
447                    .do_load_cached_session(remote_fingerprint, notification_tx)
448                    .await;
449                let _ = reply.send(result);
450            }
451            RemoteClientCommand::RequestCredential { query, reply } => {
452                let result = self
453                    .do_request_credential(query, incoming_rx, notification_tx)
454                    .await;
455                let _ = reply.send(result);
456            }
457            RemoteClientCommand::ListSessions { reply } => {
458                let sessions = self.session_store.list_sessions().await;
459                let _ = reply.send(sessions);
460            }
461            RemoteClientCommand::HasSession { fingerprint, reply } => {
462                let has = self.session_store.has_session(&fingerprint).await;
463                let _ = reply.send(has);
464            }
465        }
466    }
467
468    // ── Pairing: Rendezvous handshake ────────────────────────────────
469
470    async fn do_pair_with_handshake(
471        &mut self,
472        rendezvous_code: String,
473        verify_fingerprint: bool,
474        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
475        notification_tx: &mpsc::Sender<RemoteClientNotification>,
476        request_tx: &mpsc::Sender<RemoteClientRequest>,
477    ) -> Result<IdentityFingerprint, ClientError> {
478        // Resolve rendezvous code to fingerprint
479        notification_tx
480            .send(RemoteClientNotification::RendezvousResolving {
481                code: rendezvous_code.clone(),
482            })
483            .await
484            .ok();
485
486        let remote_fingerprint =
487            Self::resolve_rendezvous(self.proxy_client.as_ref(), incoming_rx, &rendezvous_code)
488                .await?;
489
490        notification_tx
491            .send(RemoteClientNotification::RendezvousResolved {
492                fingerprint: remote_fingerprint,
493            })
494            .await
495            .ok();
496
497        // Perform Noise handshake (no PSK)
498        notification_tx
499            .send(RemoteClientNotification::HandshakeStart)
500            .await
501            .ok();
502
503        let (transport, fingerprint_str) = Self::perform_handshake(
504            self.proxy_client.as_ref(),
505            incoming_rx,
506            remote_fingerprint,
507            None,
508        )
509        .await?;
510
511        notification_tx
512            .send(RemoteClientNotification::HandshakeComplete)
513            .await
514            .ok();
515
516        // Always emit fingerprint (informational or for verification)
517        notification_tx
518            .send(RemoteClientNotification::HandshakeFingerprint {
519                fingerprint: fingerprint_str.clone(),
520            })
521            .await
522            .ok();
523
524        if verify_fingerprint {
525            // Send verification request via request channel
526            let (fp_tx, fp_rx) = oneshot::channel();
527            request_tx
528                .send(RemoteClientRequest::VerifyFingerprint {
529                    fingerprint: fingerprint_str,
530                    reply: fp_tx,
531                })
532                .await
533                .map_err(|_| ClientError::ChannelClosed)?;
534
535            // Wait for user verification (60s timeout)
536            match timeout(Duration::from_secs(60), fp_rx).await {
537                Ok(Ok(RemoteClientFingerprintReply { approved: true })) => {
538                    notification_tx
539                        .send(RemoteClientNotification::FingerprintVerified)
540                        .await
541                        .ok();
542                }
543                Ok(Ok(RemoteClientFingerprintReply { approved: false })) => {
544                    self.proxy_client.disconnect().await.ok();
545                    notification_tx
546                        .send(RemoteClientNotification::FingerprintRejected {
547                            reason: "User rejected fingerprint verification".to_string(),
548                        })
549                        .await
550                        .ok();
551                    return Err(ClientError::FingerprintRejected);
552                }
553                Ok(Err(_)) => {
554                    return Err(ClientError::ChannelClosed);
555                }
556                Err(_) => {
557                    self.proxy_client.disconnect().await.ok();
558                    return Err(ClientError::Timeout(
559                        "Fingerprint verification timeout".to_string(),
560                    ));
561                }
562            }
563        }
564
565        // Finalize connection
566        self.finalize_pairing(transport, remote_fingerprint, notification_tx)
567            .await?;
568
569        Ok(remote_fingerprint)
570    }
571
572    // ── Pairing: PSK ─────────────────────────────────────────────────
573
574    async fn do_pair_with_psk(
575        &mut self,
576        psk: Psk,
577        remote_fingerprint: IdentityFingerprint,
578        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
579        notification_tx: &mpsc::Sender<RemoteClientNotification>,
580    ) -> Result<(), ClientError> {
581        notification_tx
582            .send(RemoteClientNotification::PskMode {
583                fingerprint: remote_fingerprint,
584            })
585            .await
586            .ok();
587
588        // Perform Noise handshake with PSK
589        notification_tx
590            .send(RemoteClientNotification::HandshakeStart)
591            .await
592            .ok();
593
594        let (transport, _fingerprint_str) = Self::perform_handshake(
595            self.proxy_client.as_ref(),
596            incoming_rx,
597            remote_fingerprint,
598            Some(psk),
599        )
600        .await?;
601
602        notification_tx
603            .send(RemoteClientNotification::HandshakeComplete)
604            .await
605            .ok();
606
607        // Skip fingerprint verification (trust via PSK)
608        notification_tx
609            .send(RemoteClientNotification::FingerprintVerified)
610            .await
611            .ok();
612
613        // Finalize connection
614        self.finalize_pairing(transport, remote_fingerprint, notification_tx)
615            .await?;
616
617        Ok(())
618    }
619
620    // ── Cached session reconnection ──────────────────────────────────
621
622    async fn do_load_cached_session(
623        &mut self,
624        remote_fingerprint: IdentityFingerprint,
625        notification_tx: &mpsc::Sender<RemoteClientNotification>,
626    ) -> Result<(), ClientError> {
627        if !self.session_store.has_session(&remote_fingerprint).await {
628            return Err(ClientError::SessionNotFound);
629        }
630
631        notification_tx
632            .send(RemoteClientNotification::ReconnectingToSession {
633                fingerprint: remote_fingerprint,
634            })
635            .await
636            .ok();
637
638        let transport = self
639            .session_store
640            .load_transport_state(&remote_fingerprint)
641            .await?
642            .ok_or(ClientError::SessionNotFound)?;
643
644        notification_tx
645            .send(RemoteClientNotification::HandshakeComplete)
646            .await
647            .ok();
648
649        // Skip fingerprint verification (already trusted)
650        notification_tx
651            .send(RemoteClientNotification::FingerprintVerified)
652            .await
653            .ok();
654
655        // Update last_connected_at
656        self.session_store
657            .update_last_connected(&remote_fingerprint)
658            .await?;
659
660        // Save transport state and store locally
661        self.session_store
662            .save_transport_state(&remote_fingerprint, transport.clone())
663            .await?;
664
665        self.transport = Some(transport);
666        self.remote_fingerprint = Some(remote_fingerprint);
667
668        notification_tx
669            .send(RemoteClientNotification::Ready {
670                can_request_credentials: true,
671            })
672            .await
673            .ok();
674
675        debug!("Reconnected to cached session");
676        Ok(())
677    }
678
679    // ── Shared pairing finalization ──────────────────────────────────
680
681    async fn finalize_pairing(
682        &mut self,
683        transport: MultiDeviceTransport,
684        remote_fingerprint: IdentityFingerprint,
685        notification_tx: &mpsc::Sender<RemoteClientNotification>,
686    ) -> Result<(), ClientError> {
687        // Cache session
688        self.session_store.cache_session(remote_fingerprint).await?;
689
690        // Save transport state for session resumption
691        self.session_store
692            .save_transport_state(&remote_fingerprint, transport.clone())
693            .await?;
694
695        // Store transport and remote fingerprint
696        self.transport = Some(transport);
697        self.remote_fingerprint = Some(remote_fingerprint);
698
699        // Emit Ready event
700        notification_tx
701            .send(RemoteClientNotification::Ready {
702                can_request_credentials: true,
703            })
704            .await
705            .ok();
706
707        debug!("Connection established successfully");
708        Ok(())
709    }
710
711    // ── Credential request ───────────────────────────────────────────
712
713    async fn do_request_credential(
714        &mut self,
715        query: CredentialQuery,
716        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
717        notification_tx: &mpsc::Sender<RemoteClientNotification>,
718    ) -> Result<CredentialData, ClientError> {
719        let remote_fingerprint = self.remote_fingerprint.ok_or(ClientError::NotInitialized)?;
720
721        // Sliced string is a UUID and isn't going to contain wide chars
722        #[allow(clippy::string_slice)]
723        let request_id = format!("req-{}-{}", now_millis(), &uuid_v4()[..8]);
724
725        debug!("Requesting credential for query: {:?}", query);
726
727        // Create and encrypt request
728        let request = CredentialRequestPayload {
729            request_type: "credential_request".to_string(),
730            query: query.clone(),
731            timestamp: now_millis(),
732            request_id: request_id.clone(),
733        };
734
735        let request_json = serde_json::to_string(&request)?;
736
737        let encrypted_data = {
738            let transport = self
739                .transport
740                .as_mut()
741                .ok_or(ClientError::SecureChannelNotEstablished)?;
742            let encrypted_packet = transport
743                .encrypt(request_json.as_bytes())
744                .map_err(|e| ClientError::NoiseProtocol(e.to_string()))?;
745            STANDARD.encode(encrypted_packet.encode())
746        };
747
748        let msg = ProtocolMessage::CredentialRequest {
749            encrypted: encrypted_data,
750        };
751
752        // Send via proxy
753        let msg_json = serde_json::to_string(&msg)?;
754        self.proxy_client
755            .send_to(remote_fingerprint, msg_json.into_bytes())
756            .await?;
757
758        // Emit event
759        notification_tx
760            .send(RemoteClientNotification::CredentialRequestSent {
761                query: query.clone(),
762            })
763            .await
764            .ok();
765
766        // Wait for matching response inline
767        match timeout(
768            DEFAULT_TIMEOUT,
769            self.receive_credential_response(&request_id, incoming_rx, notification_tx),
770        )
771        .await
772        {
773            Ok(result) => result,
774            Err(_) => Err(ClientError::Timeout(format!(
775                "Timeout waiting for credential response for query: {query:?}"
776            ))),
777        }
778    }
779
780    /// Wait for a credential response matching the given request_id.
781    ///
782    /// Stale responses from previous requests (e.g. duplicate multi-device
783    /// responses) are decrypted, logged, and silently discarded.
784    async fn receive_credential_response(
785        &mut self,
786        request_id: &str,
787        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
788        notification_tx: &mpsc::Sender<RemoteClientNotification>,
789    ) -> Result<CredentialData, ClientError> {
790        loop {
791            match incoming_rx.recv().await {
792                Some(IncomingMessage::Send { payload, .. }) => {
793                    if let Ok(text) = String::from_utf8(payload)
794                        && let Ok(ProtocolMessage::CredentialResponse { encrypted }) =
795                            serde_json::from_str::<ProtocolMessage>(&text)
796                    {
797                        match self
798                            .decrypt_credential_response(&encrypted, request_id, notification_tx)
799                            .await
800                        {
801                            Ok(credential) => return Ok(credential),
802                            Err(ClientError::CredentialRequestFailed(ref msg))
803                                if msg.contains("request_id mismatch") =>
804                            {
805                                // Stale response from a previous request — skip it
806                                debug!("Skipping stale credential response: {msg}");
807                                continue;
808                            }
809                            Err(e) => return Err(e),
810                        }
811                    }
812                }
813                Some(_) => {
814                    // Non-Send messages (RendezvousInfo, IdentityInfo) — ignore
815                }
816                None => {
817                    return Err(ClientError::ChannelClosed);
818                }
819            }
820        }
821    }
822
823    /// Decrypt and validate a credential response.
824    async fn decrypt_credential_response(
825        &mut self,
826        encrypted: &str,
827        request_id: &str,
828        notification_tx: &mpsc::Sender<RemoteClientNotification>,
829    ) -> Result<CredentialData, ClientError> {
830        let encrypted_bytes = STANDARD
831            .decode(encrypted)
832            .map_err(|e| ClientError::Serialization(format!("Invalid base64: {e}")))?;
833
834        let packet = ap_noise::TransportPacket::decode(&encrypted_bytes)
835            .map_err(|e| ClientError::NoiseProtocol(format!("Invalid packet: {e}")))?;
836
837        let transport = self
838            .transport
839            .as_mut()
840            .ok_or(ClientError::SecureChannelNotEstablished)?;
841
842        let decrypted = transport
843            .decrypt(&packet)
844            .map_err(|e| ClientError::NoiseProtocol(e.to_string()))?;
845
846        let response: CredentialResponsePayload = serde_json::from_slice(&decrypted)?;
847
848        // Verify request_id matches
849        if response.request_id.as_deref() != Some(request_id) {
850            warn!(
851                "Ignoring response with mismatched request_id: {:?}",
852                response.request_id
853            );
854            return Err(ClientError::CredentialRequestFailed(
855                "Response request_id mismatch".to_string(),
856            ));
857        }
858
859        if let Some(error) = response.error {
860            return Err(ClientError::CredentialRequestFailed(error));
861        }
862
863        if let Some(credential) = response.credential {
864            notification_tx
865                .send(RemoteClientNotification::CredentialReceived {
866                    credential: credential.clone(),
867                })
868                .await
869                .ok();
870            Ok(credential)
871        } else {
872            Err(ClientError::CredentialRequestFailed(
873                "Response contains neither credential nor error".to_string(),
874            ))
875        }
876    }
877
878    // ── Handshake helpers (associated functions) ─────────────────────
879
880    /// Resolve rendezvous code to identity fingerprint.
881    async fn resolve_rendezvous(
882        proxy_client: &dyn ProxyClient,
883        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
884        rendezvous_code: &str,
885    ) -> Result<IdentityFingerprint, ClientError> {
886        // Send GetIdentity request
887        proxy_client
888            .request_identity(RendezvousCode::from_string(rendezvous_code.to_string()))
889            .await
890            .map_err(|e| ClientError::RendezvousResolutionFailed(e.to_string()))?;
891
892        // Wait for IdentityInfo response with timeout
893        let timeout_duration = Duration::from_secs(10);
894        match timeout(timeout_duration, async {
895            while let Some(msg) = incoming_rx.recv().await {
896                if let IncomingMessage::IdentityInfo { fingerprint, .. } = msg {
897                    return Some(fingerprint);
898                }
899            }
900            None
901        })
902        .await
903        {
904            Ok(Some(fingerprint)) => Ok(fingerprint),
905            Ok(None) => Err(ClientError::RendezvousResolutionFailed(
906                "Connection closed while waiting for identity response".to_string(),
907            )),
908            Err(_) => Err(ClientError::RendezvousResolutionFailed(
909                "Timeout waiting for identity response. The rendezvous code may be invalid, expired, or the target client may be disconnected.".to_string(),
910            )),
911        }
912    }
913
914    /// Perform Noise handshake as initiator.
915    async fn perform_handshake(
916        proxy_client: &dyn ProxyClient,
917        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
918        remote_fingerprint: IdentityFingerprint,
919        psk: Option<Psk>,
920    ) -> Result<(MultiDeviceTransport, String), ClientError> {
921        // Compute PSK ID before moving the PSK into the handshake
922        let psk_id = psk.as_ref().map(|p| p.id());
923
924        // Create initiator handshake (with or without PSK)
925        let mut handshake = if let Some(psk) = psk {
926            InitiatorHandshake::with_psk(psk)
927        } else {
928            InitiatorHandshake::new()
929        };
930
931        // Generate handshake init
932        let init_packet = handshake.send_start()?;
933
934        // Send HandshakeInit message
935        let msg = ProtocolMessage::HandshakeInit {
936            data: STANDARD.encode(init_packet.encode()?),
937            ciphersuite: format!("{:?}", handshake.ciphersuite()),
938            psk_id,
939        };
940
941        let msg_json = serde_json::to_string(&msg)?;
942        proxy_client
943            .send_to(remote_fingerprint, msg_json.into_bytes())
944            .await?;
945
946        debug!("Sent handshake init");
947
948        // Wait for HandshakeResponse
949        let response_timeout = Duration::from_secs(10);
950        let response: String = timeout(response_timeout, async {
951            loop {
952                if let Some(incoming) = incoming_rx.recv().await {
953                    match incoming {
954                        IncomingMessage::Send { payload, .. } => {
955                            // Try to parse as ProtocolMessage
956                            if let Ok(text) = String::from_utf8(payload)
957                                && let Ok(ProtocolMessage::HandshakeResponse { data, .. }) =
958                                    serde_json::from_str::<ProtocolMessage>(&text)
959                            {
960                                return Ok::<String, ClientError>(data);
961                            }
962                        }
963                        _ => continue,
964                    }
965                }
966            }
967        })
968        .await
969        .map_err(|_| ClientError::Timeout("Waiting for handshake response".to_string()))??;
970
971        // Decode and process response
972        let response_bytes = STANDARD
973            .decode(&response)
974            .map_err(|e| ClientError::Serialization(format!("Invalid base64: {e}")))?;
975
976        let response_packet = ap_noise::HandshakePacket::decode(&response_bytes)
977            .map_err(|e| ClientError::NoiseProtocol(format!("Invalid packet: {e}")))?;
978
979        // Complete handshake
980        handshake.receive_finish(&response_packet)?;
981        let (transport, fingerprint) = handshake.finalize()?;
982
983        debug!("Handshake complete");
984        Ok((transport, fingerprint.to_string()))
985    }
986}
987
988fn uuid_v4() -> String {
989    // Simple UUID v4 generation without external dependency
990    let mut bytes = [0u8; 16];
991    let mut rng = rand::thread_rng();
992    rng.fill_bytes(&mut bytes);
993
994    // Set version (4) and variant bits
995    bytes[6] = (bytes[6] & 0x0f) | 0x40;
996    bytes[8] = (bytes[8] & 0x3f) | 0x80;
997
998    format!(
999        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1000        bytes[0],
1001        bytes[1],
1002        bytes[2],
1003        bytes[3],
1004        bytes[4],
1005        bytes[5],
1006        bytes[6],
1007        bytes[7],
1008        bytes[8],
1009        bytes[9],
1010        bytes[10],
1011        bytes[11],
1012        bytes[12],
1013        bytes[13],
1014        bytes[14],
1015        bytes[15]
1016    )
1017}