libsignal_service/websocket/
directory.rs

1//! Contact Discovery Service (CDSI) authentication
2//!
3//! Provides authentication credentials for CDSI contact lookup operations.
4//!
5use libsignal_core::ServiceId;
6use libsignal_net::auth::Auth;
7use libsignal_net::cdsi::{CdsiConnection, LookupResponseEntry};
8use libsignal_net::connect_state::{
9    ConnectState, ConnectionResources, SUGGESTED_CONNECT_CONFIG,
10};
11use libsignal_net_infra::dns::DnsResolver;
12use libsignal_net_infra::utils::no_network_change_events;
13use reqwest::Method;
14use serde::Deserialize;
15use tracing::warn;
16
17use crate::content::ServiceError;
18use crate::utils::TryIntoE164;
19use crate::websocket::{Identified, SignalWebSocket};
20
21pub use libsignal_net::cdsi::LookupRequest;
22
23/// CDSI authentication credentials
24#[derive(Debug, Deserialize)]
25struct CdsiAuth {
26    pub username: String,
27    pub password: String,
28}
29
30impl SignalWebSocket<Identified> {
31    /// Get CDSI authentication credentials from the chat server.
32    ///
33    /// Returns username/password credentials for establishing an
34    /// authenticated connection to the Contact Discovery Service.
35    ///
36    /// # Returns
37    /// * `Ok(CdsiAuth)` - Authentication credentials
38    /// * `Err(ServiceError)` - Network or protocol error
39    ///
40    /// # Example
41    /// ```no_run
42    /// # use libsignal_service::websocket::{SignalWebSocket, Identified};
43    /// # async fn example(mut ws: SignalWebSocket<Identified>) {
44    /// let auth = ws.get_cdsi_auth().await.unwrap();
45    /// // Use auth.username and auth.password for CDSI connection
46    /// # }
47    /// ```
48    async fn get_cdsi_auth(&mut self) -> Result<CdsiAuth, ServiceError> {
49        let response = self
50            .http_request(Method::GET, "/v2/directory/auth")?
51            .send()
52            .await?
53            .service_error_for_status()
54            .await?;
55
56        response.json().await
57    }
58
59    /// Resolve phone numbers (with possible extra information) to accounts.
60    ///
61    /// Uses Contact Discovery Service (CDSI) via libsignal-net. The phone numbers
62    /// are looked up inside an SGX enclave for privacy.
63    ///
64    /// # Arguments
65    /// * `lookup_request` - The CDSI lookup request containing phone numbers and other parameters
66    ///
67    /// # Returns
68    /// * `Ok(Vec<Option<ServiceId>>)` - Vector of resolved ServiceIds (None if not found)
69    /// * `Err(ServiceError)` - Network or protocol error
70    pub async fn discover_contacts(
71        &mut self,
72        lookup_request: LookupRequest,
73    ) -> Result<Vec<(libsignal_core::E164, Option<ServiceId>)>, ServiceError>
74    {
75        let env: libsignal_net::env::Env<'_> = self.servers().into();
76
77        // 1. Get CDSI auth credentials from chat server
78        let cdsi_auth_response = self.get_cdsi_auth().await?;
79
80        let auth = Auth {
81            username: cdsi_auth_response.username,
82            password: cdsi_auth_response.password,
83        };
84
85        // 2. Set up connection infrastructure
86        let connect_state = ConnectState::new(SUGGESTED_CONNECT_CONFIG);
87        let network_change_event = no_network_change_events();
88        let static_map = std::collections::HashMap::from([env
89            .cdsi
90            .domain_config
91            .static_fallback(libsignal_net::env::StaticIpOrder::HARDCODED)]);
92        let dns_resolver = DnsResolver::new_with_static_fallback(
93            static_map,
94            &network_change_event,
95        );
96
97        let connection_resources = ConnectionResources {
98            connect_state: &connect_state,
99            dns_resolver: &dns_resolver,
100            network_change_event: &network_change_event,
101            confirmation_header_name: None,
102        };
103
104        // 3. Connect to CDSI using DirectOrProxyProvider::direct() wrapper
105        let cdsi_endpoint = &env.cdsi;
106        let cdsi_connection = CdsiConnection::connect_with(
107            connection_resources,
108            libsignal_net_infra::route::DirectOrProxyProvider::direct(
109                cdsi_endpoint.enclave_websocket_provider(
110                    libsignal_net_infra::EnableDomainFronting::No,
111                ),
112            ),
113            cdsi_endpoint.ws_config,
114            &cdsi_endpoint.params,
115            &auth,
116        )
117        .await?;
118
119        let (_token, collector) =
120            cdsi_connection.send_request(lookup_request).await?;
121        let response = collector.collect().await?;
122
123        Ok(response.records.into_iter().map(|LookupResponseEntry { e164, aci, pni }| match (pni, aci) {
124            (None, None) => (e164, None),
125            (None, Some(aci)) => (e164, Some(aci.into())),
126            (Some(pni), None) => (e164, Some(pni.into())),
127            (Some(_), Some(aci)) => {
128                warn!("got both ACI and PNI for a phone number, this is unexpected, using ACI!");
129                (e164, Some(aci.into()))
130            },
131        }).collect())
132    }
133
134    /// Resolve a single phone number (E.164 format, e.g., "+15551234567") to a ServiceId.
135    ///
136    /// Convenience wrapper that looks up a single phone number using CDSI.
137    ///
138    /// # Arguments
139    /// * `phone_number` - Phone number in E.164 format (e.g., "+15551234567")
140    ///
141    /// # Returns
142    /// * `Ok(Option<ServiceId>)` - The resolved ServiceId (None if not found)
143    /// * `Err(ServiceError)` - Network or protocol error
144    pub async fn discover_contact_by_phone_number(
145        &mut self,
146        phone_number: impl TryIntoE164,
147    ) -> Result<Option<ServiceId>, ServiceError> {
148        let lookup_request = LookupRequest {
149            new_e164s: vec![phone_number
150                .try_into_e164()
151                .map_err(|_| ServiceError::InvalidPhoneNumber)?],
152            ..Default::default()
153        };
154
155        let results = self.discover_contacts(lookup_request).await?;
156        Ok(results
157            .into_iter()
158            .next()
159            .and_then(|(_, service_id)| service_id))
160    }
161}