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