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