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