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::RemoteClientError,
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, RemoteClientError>>,
141    },
142    PairWithPsk {
143        psk: Psk,
144        remote_fingerprint: IdentityFingerprint,
145        reply: oneshot::Sender<Result<(), RemoteClientError>>,
146    },
147    LoadCachedSession {
148        remote_fingerprint: IdentityFingerprint,
149        reply: oneshot::Sender<Result<(), RemoteClientError>>,
150    },
151    RequestCredential {
152        query: CredentialQuery,
153        reply: oneshot::Sender<Result<CredentialData, RemoteClientError>>,
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, RemoteClientError> {
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, RemoteClientError> {
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(|_| RemoteClientError::ChannelClosed)?;
262        rx.await.map_err(|_| RemoteClientError::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<(), RemoteClientError> {
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(|_| RemoteClientError::ChannelClosed)?;
283        rx.await.map_err(|_| RemoteClientError::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<(), RemoteClientError> {
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(|_| RemoteClientError::ChannelClosed)?;
302        rx.await.map_err(|_| RemoteClientError::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, RemoteClientError> {
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(|_| RemoteClientError::ChannelClosed)?;
318        rx.await.map_err(|_| RemoteClientError::ChannelClosed)?
319    }
320
321    /// List all cached sessions.
322    pub async fn list_sessions(
323        &self,
324    ) -> Result<Vec<(IdentityFingerprint, Option<String>, u64, u64)>, RemoteClientError> {
325        let (tx, rx) = oneshot::channel();
326        self.command_tx
327            .send(RemoteClientCommand::ListSessions { reply: tx })
328            .await
329            .map_err(|_| RemoteClientError::ChannelClosed)?;
330        rx.await.map_err(|_| RemoteClientError::ChannelClosed)
331    }
332
333    /// Check if a session exists for a fingerprint.
334    pub async fn has_session(
335        &self,
336        fingerprint: IdentityFingerprint,
337    ) -> Result<bool, RemoteClientError> {
338        let (tx, rx) = oneshot::channel();
339        self.command_tx
340            .send(RemoteClientCommand::HasSession {
341                fingerprint,
342                reply: tx,
343            })
344            .await
345            .map_err(|_| RemoteClientError::ChannelClosed)?;
346        rx.await.map_err(|_| RemoteClientError::ChannelClosed)
347    }
348}
349
350// =============================================================================
351// Internal state — lives inside the spawned event loop task
352// =============================================================================
353
354/// All mutable state for the remote client, owned by the spawned event loop task.
355struct RemoteClientInner {
356    session_store: Box<dyn SessionStore>,
357    proxy_client: Box<dyn ProxyClient>,
358    transport: Option<MultiDeviceTransport>,
359    remote_fingerprint: Option<IdentityFingerprint>,
360}
361
362impl RemoteClientInner {
363    /// Run the main event loop (consumes self).
364    async fn run_event_loop(
365        mut self,
366        mut incoming_rx: mpsc::UnboundedReceiver<IncomingMessage>,
367        mut command_rx: mpsc::Receiver<RemoteClientCommand>,
368        notification_tx: mpsc::Sender<RemoteClientNotification>,
369        request_tx: mpsc::Sender<RemoteClientRequest>,
370    ) {
371        loop {
372            tokio::select! {
373                msg = incoming_rx.recv() => {
374                    match msg {
375                        Some(_) => {
376                            // Stray message while idle — ignore
377                            debug!("Received message while idle");
378                        }
379                        None => {
380                            // Proxy disconnected
381                            notification_tx.send(RemoteClientNotification::Disconnected {
382                                reason: Some("Proxy connection closed".to_string()),
383                            }).await.ok();
384                            return;
385                        }
386                    }
387                }
388                cmd = command_rx.recv() => {
389                    match cmd {
390                        Some(cmd) => {
391                            self.handle_command(
392                                cmd,
393                                &mut incoming_rx,
394                                &notification_tx,
395                                &request_tx,
396                            ).await;
397                        }
398                        None => {
399                            // All handles dropped — shut down
400                            debug!("All RemoteClient handles dropped, shutting down event loop");
401                            self.proxy_client.disconnect().await.ok();
402                            return;
403                        }
404                    }
405                }
406            }
407        }
408    }
409
410    /// Dispatch a command from the handle.
411    async fn handle_command(
412        &mut self,
413        cmd: RemoteClientCommand,
414        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
415        notification_tx: &mpsc::Sender<RemoteClientNotification>,
416        request_tx: &mpsc::Sender<RemoteClientRequest>,
417    ) {
418        match cmd {
419            RemoteClientCommand::PairWithHandshake {
420                rendezvous_code,
421                verify_fingerprint,
422                reply,
423            } => {
424                let result = self
425                    .do_pair_with_handshake(
426                        rendezvous_code,
427                        verify_fingerprint,
428                        incoming_rx,
429                        notification_tx,
430                        request_tx,
431                    )
432                    .await;
433                let _ = reply.send(result);
434            }
435            RemoteClientCommand::PairWithPsk {
436                psk,
437                remote_fingerprint,
438                reply,
439            } => {
440                let result = self
441                    .do_pair_with_psk(psk, remote_fingerprint, incoming_rx, notification_tx)
442                    .await;
443                let _ = reply.send(result);
444            }
445            RemoteClientCommand::LoadCachedSession {
446                remote_fingerprint,
447                reply,
448            } => {
449                let result = self
450                    .do_load_cached_session(remote_fingerprint, notification_tx)
451                    .await;
452                let _ = reply.send(result);
453            }
454            RemoteClientCommand::RequestCredential { query, reply } => {
455                let result = self
456                    .do_request_credential(query, incoming_rx, notification_tx)
457                    .await;
458                let _ = reply.send(result);
459            }
460            RemoteClientCommand::ListSessions { reply } => {
461                let sessions = self.session_store.list_sessions().await;
462                let _ = reply.send(sessions);
463            }
464            RemoteClientCommand::HasSession { fingerprint, reply } => {
465                let has = self.session_store.has_session(&fingerprint).await;
466                let _ = reply.send(has);
467            }
468        }
469    }
470
471    // ── Pairing: Rendezvous handshake ────────────────────────────────
472
473    async fn do_pair_with_handshake(
474        &mut self,
475        rendezvous_code: String,
476        verify_fingerprint: bool,
477        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
478        notification_tx: &mpsc::Sender<RemoteClientNotification>,
479        request_tx: &mpsc::Sender<RemoteClientRequest>,
480    ) -> Result<IdentityFingerprint, RemoteClientError> {
481        // Resolve rendezvous code to fingerprint
482        notification_tx
483            .send(RemoteClientNotification::RendezvousResolving {
484                code: rendezvous_code.clone(),
485            })
486            .await
487            .ok();
488
489        let remote_fingerprint =
490            Self::resolve_rendezvous(self.proxy_client.as_ref(), incoming_rx, &rendezvous_code)
491                .await?;
492
493        notification_tx
494            .send(RemoteClientNotification::RendezvousResolved {
495                fingerprint: remote_fingerprint,
496            })
497            .await
498            .ok();
499
500        // Perform Noise handshake (no PSK)
501        notification_tx
502            .send(RemoteClientNotification::HandshakeStart)
503            .await
504            .ok();
505
506        let (transport, fingerprint_str) = Self::perform_handshake(
507            self.proxy_client.as_ref(),
508            incoming_rx,
509            remote_fingerprint,
510            None,
511        )
512        .await?;
513
514        notification_tx
515            .send(RemoteClientNotification::HandshakeComplete)
516            .await
517            .ok();
518
519        // Always emit fingerprint (informational or for verification)
520        notification_tx
521            .send(RemoteClientNotification::HandshakeFingerprint {
522                fingerprint: fingerprint_str.clone(),
523            })
524            .await
525            .ok();
526
527        if verify_fingerprint {
528            // Send verification request via request channel
529            let (fp_tx, fp_rx) = oneshot::channel();
530            request_tx
531                .send(RemoteClientRequest::VerifyFingerprint {
532                    fingerprint: fingerprint_str,
533                    reply: fp_tx,
534                })
535                .await
536                .map_err(|_| RemoteClientError::ChannelClosed)?;
537
538            // Wait for user verification (60s timeout)
539            match timeout(Duration::from_secs(60), fp_rx).await {
540                Ok(Ok(RemoteClientFingerprintReply { approved: true })) => {
541                    notification_tx
542                        .send(RemoteClientNotification::FingerprintVerified)
543                        .await
544                        .ok();
545                }
546                Ok(Ok(RemoteClientFingerprintReply { approved: false })) => {
547                    self.proxy_client.disconnect().await.ok();
548                    notification_tx
549                        .send(RemoteClientNotification::FingerprintRejected {
550                            reason: "User rejected fingerprint verification".to_string(),
551                        })
552                        .await
553                        .ok();
554                    return Err(RemoteClientError::FingerprintRejected);
555                }
556                Ok(Err(_)) => {
557                    return Err(RemoteClientError::ChannelClosed);
558                }
559                Err(_) => {
560                    self.proxy_client.disconnect().await.ok();
561                    return Err(RemoteClientError::Timeout(
562                        "Fingerprint verification timeout".to_string(),
563                    ));
564                }
565            }
566        }
567
568        // Finalize connection
569        self.finalize_pairing(transport, remote_fingerprint, notification_tx)
570            .await?;
571
572        Ok(remote_fingerprint)
573    }
574
575    // ── Pairing: PSK ─────────────────────────────────────────────────
576
577    async fn do_pair_with_psk(
578        &mut self,
579        psk: Psk,
580        remote_fingerprint: IdentityFingerprint,
581        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
582        notification_tx: &mpsc::Sender<RemoteClientNotification>,
583    ) -> Result<(), RemoteClientError> {
584        notification_tx
585            .send(RemoteClientNotification::PskMode {
586                fingerprint: remote_fingerprint,
587            })
588            .await
589            .ok();
590
591        // Perform Noise handshake with PSK
592        notification_tx
593            .send(RemoteClientNotification::HandshakeStart)
594            .await
595            .ok();
596
597        let (transport, _fingerprint_str) = Self::perform_handshake(
598            self.proxy_client.as_ref(),
599            incoming_rx,
600            remote_fingerprint,
601            Some(psk),
602        )
603        .await?;
604
605        notification_tx
606            .send(RemoteClientNotification::HandshakeComplete)
607            .await
608            .ok();
609
610        // Skip fingerprint verification (trust via PSK)
611        notification_tx
612            .send(RemoteClientNotification::FingerprintVerified)
613            .await
614            .ok();
615
616        // Finalize connection
617        self.finalize_pairing(transport, remote_fingerprint, notification_tx)
618            .await?;
619
620        Ok(())
621    }
622
623    // ── Cached session reconnection ──────────────────────────────────
624
625    async fn do_load_cached_session(
626        &mut self,
627        remote_fingerprint: IdentityFingerprint,
628        notification_tx: &mpsc::Sender<RemoteClientNotification>,
629    ) -> Result<(), RemoteClientError> {
630        if !self.session_store.has_session(&remote_fingerprint).await {
631            return Err(RemoteClientError::SessionNotFound);
632        }
633
634        notification_tx
635            .send(RemoteClientNotification::ReconnectingToSession {
636                fingerprint: remote_fingerprint,
637            })
638            .await
639            .ok();
640
641        let transport = self
642            .session_store
643            .load_transport_state(&remote_fingerprint)
644            .await?
645            .ok_or(RemoteClientError::SessionNotFound)?;
646
647        notification_tx
648            .send(RemoteClientNotification::HandshakeComplete)
649            .await
650            .ok();
651
652        // Skip fingerprint verification (already trusted)
653        notification_tx
654            .send(RemoteClientNotification::FingerprintVerified)
655            .await
656            .ok();
657
658        // Update last_connected_at
659        self.session_store
660            .update_last_connected(&remote_fingerprint)
661            .await?;
662
663        // Save transport state and store locally
664        self.session_store
665            .save_transport_state(&remote_fingerprint, transport.clone())
666            .await?;
667
668        self.transport = Some(transport);
669        self.remote_fingerprint = Some(remote_fingerprint);
670
671        notification_tx
672            .send(RemoteClientNotification::Ready {
673                can_request_credentials: true,
674            })
675            .await
676            .ok();
677
678        debug!("Reconnected to cached session");
679        Ok(())
680    }
681
682    // ── Shared pairing finalization ──────────────────────────────────
683
684    async fn finalize_pairing(
685        &mut self,
686        transport: MultiDeviceTransport,
687        remote_fingerprint: IdentityFingerprint,
688        notification_tx: &mpsc::Sender<RemoteClientNotification>,
689    ) -> Result<(), RemoteClientError> {
690        // Cache session
691        self.session_store.cache_session(remote_fingerprint).await?;
692
693        // Save transport state for session resumption
694        self.session_store
695            .save_transport_state(&remote_fingerprint, transport.clone())
696            .await?;
697
698        // Store transport and remote fingerprint
699        self.transport = Some(transport);
700        self.remote_fingerprint = Some(remote_fingerprint);
701
702        // Emit Ready event
703        notification_tx
704            .send(RemoteClientNotification::Ready {
705                can_request_credentials: true,
706            })
707            .await
708            .ok();
709
710        debug!("Connection established successfully");
711        Ok(())
712    }
713
714    // ── Credential request ───────────────────────────────────────────
715
716    async fn do_request_credential(
717        &mut self,
718        query: CredentialQuery,
719        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
720        notification_tx: &mpsc::Sender<RemoteClientNotification>,
721    ) -> Result<CredentialData, RemoteClientError> {
722        let remote_fingerprint = self
723            .remote_fingerprint
724            .ok_or(RemoteClientError::NotInitialized)?;
725
726        // Sliced string is a UUID and isn't going to contain wide chars
727        #[allow(clippy::string_slice)]
728        let request_id = format!("req-{}-{}", now_millis(), &uuid_v4()[..8]);
729
730        debug!("Requesting credential for query: {:?}", query);
731
732        // Create and encrypt request
733        let request = CredentialRequestPayload {
734            request_type: "credential_request".to_string(),
735            query: query.clone(),
736            timestamp: now_millis(),
737            request_id: request_id.clone(),
738        };
739
740        let request_json = serde_json::to_string(&request)?;
741
742        let encrypted_data = {
743            let transport = self
744                .transport
745                .as_mut()
746                .ok_or(RemoteClientError::SecureChannelNotEstablished)?;
747            let encrypted_packet = transport
748                .encrypt(request_json.as_bytes())
749                .map_err(|e| RemoteClientError::NoiseProtocol(e.to_string()))?;
750            STANDARD.encode(encrypted_packet.encode())
751        };
752
753        let msg = ProtocolMessage::CredentialRequest {
754            encrypted: encrypted_data,
755        };
756
757        // Send via proxy
758        let msg_json = serde_json::to_string(&msg)?;
759        self.proxy_client
760            .send_to(remote_fingerprint, msg_json.into_bytes())
761            .await?;
762
763        // Emit event
764        notification_tx
765            .send(RemoteClientNotification::CredentialRequestSent {
766                query: query.clone(),
767            })
768            .await
769            .ok();
770
771        // Wait for matching response inline
772        match timeout(
773            DEFAULT_TIMEOUT,
774            self.receive_credential_response(&request_id, incoming_rx, notification_tx),
775        )
776        .await
777        {
778            Ok(result) => result,
779            Err(_) => Err(RemoteClientError::Timeout(format!(
780                "Timeout waiting for credential response for query: {query:?}"
781            ))),
782        }
783    }
784
785    /// Wait for a credential response matching the given request_id.
786    ///
787    /// Stale responses from previous requests (e.g. duplicate multi-device
788    /// responses) are decrypted, logged, and silently discarded.
789    async fn receive_credential_response(
790        &mut self,
791        request_id: &str,
792        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
793        notification_tx: &mpsc::Sender<RemoteClientNotification>,
794    ) -> Result<CredentialData, RemoteClientError> {
795        loop {
796            match incoming_rx.recv().await {
797                Some(IncomingMessage::Send { payload, .. }) => {
798                    if let Ok(text) = String::from_utf8(payload)
799                        && let Ok(ProtocolMessage::CredentialResponse { encrypted }) =
800                            serde_json::from_str::<ProtocolMessage>(&text)
801                    {
802                        match self
803                            .decrypt_credential_response(&encrypted, request_id, notification_tx)
804                            .await
805                        {
806                            Ok(credential) => return Ok(credential),
807                            Err(RemoteClientError::CredentialRequestFailed(ref msg))
808                                if msg.contains("request_id mismatch") =>
809                            {
810                                // Stale response from a previous request — skip it
811                                debug!("Skipping stale credential response: {msg}");
812                                continue;
813                            }
814                            Err(e) => return Err(e),
815                        }
816                    }
817                }
818                Some(_) => {
819                    // Non-Send messages (RendezvousInfo, IdentityInfo) — ignore
820                }
821                None => {
822                    return Err(RemoteClientError::ChannelClosed);
823                }
824            }
825        }
826    }
827
828    /// Decrypt and validate a credential response.
829    async fn decrypt_credential_response(
830        &mut self,
831        encrypted: &str,
832        request_id: &str,
833        notification_tx: &mpsc::Sender<RemoteClientNotification>,
834    ) -> Result<CredentialData, RemoteClientError> {
835        let encrypted_bytes = STANDARD
836            .decode(encrypted)
837            .map_err(|e| RemoteClientError::Serialization(format!("Invalid base64: {e}")))?;
838
839        let packet = ap_noise::TransportPacket::decode(&encrypted_bytes)
840            .map_err(|e| RemoteClientError::NoiseProtocol(format!("Invalid packet: {e}")))?;
841
842        let transport = self
843            .transport
844            .as_mut()
845            .ok_or(RemoteClientError::SecureChannelNotEstablished)?;
846
847        let decrypted = transport
848            .decrypt(&packet)
849            .map_err(|e| RemoteClientError::NoiseProtocol(e.to_string()))?;
850
851        let response: CredentialResponsePayload = serde_json::from_slice(&decrypted)?;
852
853        // Verify request_id matches
854        if response.request_id.as_deref() != Some(request_id) {
855            warn!(
856                "Ignoring response with mismatched request_id: {:?}",
857                response.request_id
858            );
859            return Err(RemoteClientError::CredentialRequestFailed(
860                "Response request_id mismatch".to_string(),
861            ));
862        }
863
864        if let Some(error) = response.error {
865            return Err(RemoteClientError::CredentialRequestFailed(error));
866        }
867
868        if let Some(credential) = response.credential {
869            notification_tx
870                .send(RemoteClientNotification::CredentialReceived {
871                    credential: credential.clone(),
872                })
873                .await
874                .ok();
875            Ok(credential)
876        } else {
877            Err(RemoteClientError::CredentialRequestFailed(
878                "Response contains neither credential nor error".to_string(),
879            ))
880        }
881    }
882
883    // ── Handshake helpers (associated functions) ─────────────────────
884
885    /// Resolve rendezvous code to identity fingerprint.
886    async fn resolve_rendezvous(
887        proxy_client: &dyn ProxyClient,
888        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
889        rendezvous_code: &str,
890    ) -> Result<IdentityFingerprint, RemoteClientError> {
891        // Send GetIdentity request
892        proxy_client
893            .request_identity(RendezvousCode::from_string(rendezvous_code.to_string()))
894            .await
895            .map_err(|e| RemoteClientError::RendezvousResolutionFailed(e.to_string()))?;
896
897        // Wait for IdentityInfo response with timeout
898        let timeout_duration = Duration::from_secs(10);
899        match timeout(timeout_duration, async {
900            while let Some(msg) = incoming_rx.recv().await {
901                if let IncomingMessage::IdentityInfo { fingerprint, .. } = msg {
902                    return Some(fingerprint);
903                }
904            }
905            None
906        })
907        .await
908        {
909            Ok(Some(fingerprint)) => Ok(fingerprint),
910            Ok(None) => Err(RemoteClientError::RendezvousResolutionFailed(
911                "Connection closed while waiting for identity response".to_string(),
912            )),
913            Err(_) => Err(RemoteClientError::RendezvousResolutionFailed(
914                "Timeout waiting for identity response. The rendezvous code may be invalid, expired, or the target client may be disconnected.".to_string(),
915            )),
916        }
917    }
918
919    /// Perform Noise handshake as initiator.
920    async fn perform_handshake(
921        proxy_client: &dyn ProxyClient,
922        incoming_rx: &mut mpsc::UnboundedReceiver<IncomingMessage>,
923        remote_fingerprint: IdentityFingerprint,
924        psk: Option<Psk>,
925    ) -> Result<(MultiDeviceTransport, String), RemoteClientError> {
926        // Compute PSK ID before moving the PSK into the handshake
927        let psk_id = psk.as_ref().map(|p| p.id());
928
929        // Create initiator handshake (with or without PSK)
930        let mut handshake = if let Some(psk) = psk {
931            InitiatorHandshake::with_psk(psk)
932        } else {
933            InitiatorHandshake::new()
934        };
935
936        // Generate handshake init
937        let init_packet = handshake.send_start()?;
938
939        // Send HandshakeInit message
940        let msg = ProtocolMessage::HandshakeInit {
941            data: STANDARD.encode(init_packet.encode()?),
942            ciphersuite: format!("{:?}", handshake.ciphersuite()),
943            psk_id,
944        };
945
946        let msg_json = serde_json::to_string(&msg)?;
947        proxy_client
948            .send_to(remote_fingerprint, msg_json.into_bytes())
949            .await?;
950
951        debug!("Sent handshake init");
952
953        // Wait for HandshakeResponse
954        let response_timeout = Duration::from_secs(10);
955        let response: String = timeout(response_timeout, async {
956            loop {
957                if let Some(incoming) = incoming_rx.recv().await {
958                    match incoming {
959                        IncomingMessage::Send { payload, .. } => {
960                            // Try to parse as ProtocolMessage
961                            if let Ok(text) = String::from_utf8(payload)
962                                && let Ok(ProtocolMessage::HandshakeResponse { data, .. }) =
963                                    serde_json::from_str::<ProtocolMessage>(&text)
964                            {
965                                return Ok::<String, RemoteClientError>(data);
966                            }
967                        }
968                        _ => continue,
969                    }
970                }
971            }
972        })
973        .await
974        .map_err(|_| RemoteClientError::Timeout("Waiting for handshake response".to_string()))??;
975
976        // Decode and process response
977        let response_bytes = STANDARD
978            .decode(&response)
979            .map_err(|e| RemoteClientError::Serialization(format!("Invalid base64: {e}")))?;
980
981        let response_packet = ap_noise::HandshakePacket::decode(&response_bytes)
982            .map_err(|e| RemoteClientError::NoiseProtocol(format!("Invalid packet: {e}")))?;
983
984        // Complete handshake
985        handshake.receive_finish(&response_packet)?;
986        let (transport, fingerprint) = handshake.finalize()?;
987
988        debug!("Handshake complete");
989        Ok((transport, fingerprint.to_string()))
990    }
991}
992
993fn uuid_v4() -> String {
994    // Simple UUID v4 generation without external dependency
995    let mut bytes = [0u8; 16];
996    let mut rng = rand::thread_rng();
997    rng.fill_bytes(&mut bytes);
998
999    // Set version (4) and variant bits
1000    bytes[6] = (bytes[6] & 0x0f) | 0x40;
1001    bytes[8] = (bytes[8] & 0x3f) | 0x80;
1002
1003    format!(
1004        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1005        bytes[0],
1006        bytes[1],
1007        bytes[2],
1008        bytes[3],
1009        bytes[4],
1010        bytes[5],
1011        bytes[6],
1012        bytes[7],
1013        bytes[8],
1014        bytes[9],
1015        bytes[10],
1016        bytes[11],
1017        bytes[12],
1018        bytes[13],
1019        bytes[14],
1020        bytes[15]
1021    )
1022}