libsignal_service/
configuration.rs

1use core::fmt;
2use std::{borrow::Cow, collections::HashMap, str::FromStr};
3
4use crate::utils::BASE64_RELAXED;
5use base64::prelude::*;
6use libsignal_protocol::PublicKey;
7use serde::{Deserialize, Serialize};
8use url::Url;
9use zkgroup::ServerPublicParams;
10
11use crate::{
12    envelope::{CIPHER_KEY_SIZE, MAC_KEY_SIZE},
13    push_service::{HttpAuth, DEFAULT_DEVICE_ID},
14};
15
16#[derive(Clone)]
17pub struct ServiceConfiguration {
18    service_url: Url,
19    storage_url: Url,
20    cdn_urls: HashMap<u32, Url>,
21    contact_discovery_url: Url,
22    pub certificate_authority: String,
23    pub unidentified_sender_trust_root: PublicKey,
24    pub zkgroup_server_public_params: ServerPublicParams,
25}
26
27pub type SignalingKey = [u8; CIPHER_KEY_SIZE + MAC_KEY_SIZE];
28
29#[derive(Clone)]
30pub struct ServiceCredentials {
31    pub aci: Option<uuid::Uuid>,
32    pub pni: Option<uuid::Uuid>,
33    pub phonenumber: phonenumber::PhoneNumber,
34    pub password: Option<String>,
35    pub signaling_key: Option<SignalingKey>,
36    pub device_id: Option<u32>,
37}
38
39impl ServiceCredentials {
40    pub fn authorization(&self) -> Option<HttpAuth> {
41        self.password.as_ref().map(|password| HttpAuth {
42            username: self.login(),
43            password: password.clone(),
44        })
45    }
46
47    pub fn e164(&self) -> String {
48        self.phonenumber
49            .format()
50            .mode(phonenumber::Mode::E164)
51            .to_string()
52    }
53
54    pub fn login(&self) -> String {
55        let identifier = {
56            if let Some(uuid) = self.aci.as_ref() {
57                uuid.to_string()
58            } else {
59                self.e164()
60            }
61        };
62
63        match self.device_id {
64            None | Some(DEFAULT_DEVICE_ID) => identifier,
65            Some(id) => format!("{}.{}", identifier, id),
66        }
67    }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum SignalServers {
72    Staging,
73    Production,
74}
75
76#[derive(Debug)]
77pub enum Endpoint<'a> {
78    Absolute(Url),
79    Service {
80        path: Cow<'a, str>,
81    },
82    Storage {
83        path: Cow<'a, str>,
84    },
85    Cdn {
86        cdn_id: u32,
87        path: Cow<'a, str>,
88        query: Option<Cow<'a, str>>,
89    },
90    ContactDiscovery {
91        path: Cow<'a, str>,
92    },
93}
94
95impl fmt::Display for Endpoint<'_> {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            Endpoint::Absolute(url) => write!(f, "absolute URL {url}"),
99            Endpoint::Service { path } => {
100                write!(f, "service API call to {path}")
101            },
102            Endpoint::Storage { path } => {
103                write!(f, "storage API call to {path}")
104            },
105            Endpoint::Cdn { cdn_id, path, .. } => {
106                write!(f, "CDN{cdn_id} call to {path}")
107            },
108            Endpoint::ContactDiscovery { path } => {
109                write!(f, "Contact discovery API call to {path}")
110            },
111        }
112    }
113}
114
115impl<'a> Endpoint<'a> {
116    pub fn service(path: impl Into<Cow<'a, str>>) -> Self {
117        Self::Service { path: path.into() }
118    }
119
120    pub fn cdn(cdn_id: u32, path: impl Into<Cow<'a, str>>) -> Self {
121        Self::Cdn {
122            cdn_id,
123            path: path.into(),
124            query: None,
125        }
126    }
127
128    pub fn cdn_url(cdn_id: u32, url: &'a Url) -> Self {
129        Self::Cdn {
130            cdn_id,
131            path: url.path().into(),
132            query: url.query().map(Into::into),
133        }
134    }
135
136    pub fn storage(path: impl Into<Cow<'a, str>>) -> Self {
137        Self::Storage { path: path.into() }
138    }
139
140    pub fn into_url(
141        self,
142        service_configuration: &ServiceConfiguration,
143    ) -> Result<Url, url::ParseError> {
144        match self {
145            Endpoint::Service { path } => {
146                service_configuration.service_url.join(&path)
147            },
148            Endpoint::Storage { path } => {
149                service_configuration.storage_url.join(&path)
150            },
151            Endpoint::Cdn {
152                ref cdn_id,
153                path,
154                query,
155            } => {
156                let mut url = service_configuration.cdn_urls[cdn_id].clone();
157                url.set_path(&path);
158                url.set_query(query.as_deref());
159                Ok(url)
160            },
161            Endpoint::ContactDiscovery { path } => {
162                service_configuration.contact_discovery_url.join(&path)
163            },
164            Endpoint::Absolute(url) => Ok(url),
165        }
166    }
167}
168
169impl FromStr for SignalServers {
170    type Err = std::io::Error;
171
172    fn from_str(s: &str) -> Result<Self, Self::Err> {
173        use std::io::ErrorKind;
174        match s {
175            "staging" => Ok(Self::Staging),
176            "production" => Ok(Self::Production),
177            _ => Err(Self::Err::new(
178                ErrorKind::InvalidInput,
179                "invalid signal servers, can be either: staging or production",
180            )),
181        }
182    }
183}
184
185impl fmt::Display for SignalServers {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        match self {
188            Self::Staging => f.write_str("staging"),
189            Self::Production => f.write_str("production"),
190        }
191    }
192}
193
194impl From<SignalServers> for ServiceConfiguration {
195    fn from(val: SignalServers) -> Self {
196        ServiceConfiguration::from(&val)
197    }
198}
199
200impl From<&SignalServers> for ServiceConfiguration {
201    fn from(val: &SignalServers) -> Self {
202        // base configuration from https://github.com/signalapp/Signal-Desktop/blob/development/config/default.json
203        match val {
204            // configuration with the Signal API staging endpoints
205            // see: https://github.com/signalapp/Signal-Desktop/blob/master/config/default.json
206            SignalServers::Staging => ServiceConfiguration {
207                service_url: "https://chat.staging.signal.org".parse().unwrap(),
208                storage_url:"https://storage-staging.signal.org".parse().unwrap(),
209                cdn_urls: {
210                    let mut map = HashMap::new();
211                    map.insert(0, "https://cdn-staging.signal.org".parse().unwrap());
212                    map.insert(2, "https://cdn2-staging.signal.org".parse().unwrap());
213                    map.insert(3, "https://cdn3-staging.signal.org".parse().unwrap());
214                    map
215                },
216                contact_discovery_url:
217                    "https://api-staging.directory.signal.org".parse().unwrap(),
218                certificate_authority: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/certs/staging-root-ca.pem")).to_string(),
219                unidentified_sender_trust_root:
220                    PublicKey::deserialize(&BASE64_RELAXED.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx").unwrap()).unwrap(),
221                zkgroup_server_public_params: bincode::deserialize(&BASE64_RELAXED.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==").unwrap()).unwrap(),
222            },
223            // configuration with the Signal API production endpoints
224            // https://github.com/signalapp/Signal-Desktop/blob/master/config/production.json
225            SignalServers::Production => ServiceConfiguration {
226                service_url:
227                    "https://chat.signal.org".parse().unwrap(),
228                storage_url: "https://storage.signal.org".parse().unwrap(),
229                cdn_urls: {
230                    let mut map = HashMap::new();
231                    map.insert(0, "https://cdn.signal.org".parse().unwrap());
232                    map.insert(2, "https://cdn2.signal.org".parse().unwrap());
233                    map.insert(3, "https://cdn3.signal.org".parse().unwrap());
234                    map
235                },
236                contact_discovery_url: "https://api.directory.signal.org".parse().unwrap(),
237                certificate_authority: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/certs/production-root-ca.pem")).to_string(),
238                unidentified_sender_trust_root:
239                    PublicKey::deserialize(&BASE64_RELAXED.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF").unwrap()).unwrap(),
240                zkgroup_server_public_params: bincode::deserialize(
241                    &BASE64_RELAXED.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==").unwrap()).unwrap(),
242            },
243        }
244    }
245}