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