libsignal_service/
configuration.rs

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