Skip to main content

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