Skip to main content

libsignal_service/groups_v2/
manager.rs

1use std::{collections::HashMap, convert::TryInto};
2
3use crate::{
4    configuration::Endpoint,
5    groups_v2::{
6        model::{Group, GroupChanges},
7        operations::{GroupDecodingError, GroupOperations},
8    },
9    prelude::{PushService, ServiceError},
10    proto::GroupContextV2,
11    push_service::{
12        HttpAuth, HttpAuthOverride, ServiceIds, SignalServiceResponse,
13    },
14    utils::BASE64_RELAXED,
15    websocket::{self, SignalWebSocket},
16};
17
18use base64::prelude::*;
19use bytes::Bytes;
20use chrono::{Days, NaiveDate, NaiveTime, Utc};
21use futures::AsyncReadExt;
22use rand::{CryptoRng, Rng};
23use reqwest::Method;
24use serde::Deserialize;
25use zkgroup::{
26    auth::{AuthCredentialWithPni, AuthCredentialWithPniResponse},
27    groups::{GroupMasterKey, GroupSecretParams},
28    ServerPublicParams,
29};
30
31#[derive(Debug, serde::Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct TemporalCredential {
34    credential: String,
35    redemption_time: u64,
36}
37
38#[derive(Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct CredentialResponse {
41    credentials: Vec<TemporalCredential>,
42}
43
44impl CredentialResponse {
45    pub fn parse(
46        self,
47    ) -> Result<HashMap<u64, AuthCredentialWithPniResponse>, ServiceError> {
48        self.credentials
49            .into_iter()
50            .map(|c| {
51                let bytes = BASE64_RELAXED.decode(c.credential)?;
52                let data = AuthCredentialWithPniResponse::new(&bytes)?;
53                Ok((c.redemption_time, data))
54            })
55            .collect::<Result<_, ServiceError>>()
56    }
57}
58
59#[derive(Debug, thiserror::Error)]
60pub enum CredentialsCacheError {
61    #[error("failed to read values from cache: {0}")]
62    ReadError(String),
63    #[error("failed to write values from cache: {0}")]
64    WriteError(String),
65}
66
67/// Global cache for groups v2 credentials, as demonstrated in the libsignal-service
68/// java library of Signal-Android.
69///
70/// A basic in-memory implementation is provided with `InMemoryCredentialsCache`.
71pub trait CredentialsCache {
72    fn clear(&mut self) -> Result<(), CredentialsCacheError>;
73
74    /// Get an entry of the cache, key usually represents the day number since EPOCH.
75    fn get(
76        &self,
77        key: &u64,
78    ) -> Result<Option<&AuthCredentialWithPniResponse>, CredentialsCacheError>;
79
80    /// Overwrite the entire contents of the cache with new data.
81    fn write(
82        &mut self,
83        map: HashMap<u64, AuthCredentialWithPniResponse>,
84    ) -> Result<(), CredentialsCacheError>;
85}
86
87#[derive(Default)]
88pub struct InMemoryCredentialsCache {
89    map: HashMap<u64, AuthCredentialWithPniResponse>,
90}
91
92impl CredentialsCache for InMemoryCredentialsCache {
93    fn clear(&mut self) -> Result<(), CredentialsCacheError> {
94        self.map.clear();
95        Ok(())
96    }
97
98    fn get(
99        &self,
100        key: &u64,
101    ) -> Result<Option<&AuthCredentialWithPniResponse>, CredentialsCacheError>
102    {
103        Ok(self.map.get(key))
104    }
105
106    fn write(
107        &mut self,
108        map: HashMap<u64, AuthCredentialWithPniResponse>,
109    ) -> Result<(), CredentialsCacheError> {
110        self.map = map;
111        Ok(())
112    }
113}
114
115impl<T: CredentialsCache> CredentialsCache for &mut T {
116    fn clear(&mut self) -> Result<(), CredentialsCacheError> {
117        (**self).clear()
118    }
119
120    fn get(
121        &self,
122        key: &u64,
123    ) -> Result<Option<&AuthCredentialWithPniResponse>, CredentialsCacheError>
124    {
125        (**self).get(key)
126    }
127
128    fn write(
129        &mut self,
130        map: HashMap<u64, AuthCredentialWithPniResponse>,
131    ) -> Result<(), CredentialsCacheError> {
132        (**self).write(map)
133    }
134}
135
136pub struct GroupsManager<C: CredentialsCache> {
137    service_ids: ServiceIds,
138    identified_push_service: PushService,
139    unidentified_websocket: SignalWebSocket<websocket::Unidentified>,
140    credentials_cache: C,
141    server_public_params: ServerPublicParams,
142}
143
144impl<C: CredentialsCache> GroupsManager<C> {
145    pub fn new(
146        service_ids: ServiceIds,
147        identified_push_service: PushService,
148        unidentified_websocket: SignalWebSocket<websocket::Unidentified>,
149        credentials_cache: C,
150        server_public_params: ServerPublicParams,
151    ) -> Self {
152        Self {
153            service_ids,
154            identified_push_service,
155            unidentified_websocket,
156            credentials_cache,
157            server_public_params,
158        }
159    }
160
161    pub async fn get_authorization_for_today<R: Rng + CryptoRng>(
162        &mut self,
163        csprng: &mut R,
164        group_secret_params: GroupSecretParams,
165    ) -> Result<HttpAuth, ServiceError> {
166        let (today, today_plus_7_days) = current_days_seconds();
167
168        let auth_credential_response = if let Some(auth_credential_response) =
169            self.credentials_cache.get(&today)?
170        {
171            auth_credential_response
172        } else {
173            let path =
174            format!("/v1/certificate/auth/group?redemptionStartSeconds={}&redemptionEndSeconds={}&pniAsServiceId=true", today, today_plus_7_days);
175
176            let credentials_response: CredentialResponse = self
177                .identified_push_service
178                .request(
179                    Method::GET,
180                    Endpoint::service(path),
181                    HttpAuthOverride::NoOverride,
182                )?
183                .send()
184                .await?
185                .service_error_for_status()
186                .await?
187                .json()
188                .await?;
189            self.credentials_cache
190                .write(credentials_response.parse()?)?;
191            self.credentials_cache.get(&today)?.ok_or({
192                ServiceError::InvalidFrame {
193                    reason:
194                        "credentials received did not contain requested day",
195                }
196            })?
197        };
198
199        self.get_authorization_string(
200            csprng,
201            group_secret_params,
202            auth_credential_response.clone(),
203            today,
204        )
205    }
206
207    fn get_authorization_string<R: Rng + CryptoRng>(
208        &self,
209        csprng: &mut R,
210        group_secret_params: GroupSecretParams,
211        credential_response: AuthCredentialWithPniResponse,
212        today: u64,
213    ) -> Result<HttpAuth, ServiceError> {
214        let redemption_time = zkgroup::Timestamp::from_epoch_seconds(today);
215
216        let auth_credential_bytes =
217            zkgroup::serialize(&credential_response.receive(
218                &self.server_public_params,
219                self.service_ids.aci(),
220                self.service_ids.pni(),
221                redemption_time,
222            )?);
223
224        let auth_credential =
225            AuthCredentialWithPni::new(&auth_credential_bytes)
226                .expect("just validated");
227
228        let mut random_bytes = [0u8; 32];
229        csprng.fill_bytes(&mut random_bytes);
230
231        let auth_credential_presentation =
232            zkgroup::serialize(&auth_credential.present(
233                &self.server_public_params,
234                &group_secret_params,
235                random_bytes,
236            ));
237
238        // see simpleapi.rs GroupSecretParams_getPublicParams, everything is bincode encoded
239        // across the boundary of Rust/Java
240        let username = hex::encode(bincode::serialize(
241            &group_secret_params.get_public_params(),
242        )?);
243
244        let password = hex::encode(&auth_credential_presentation);
245
246        Ok(HttpAuth { username, password })
247    }
248
249    pub async fn fetch_encrypted_group<R: Rng + CryptoRng>(
250        &mut self,
251        csprng: &mut R,
252        master_key_bytes: &[u8],
253    ) -> Result<crate::proto::Group, ServiceError> {
254        let group_master_key = GroupMasterKey::new(
255            master_key_bytes
256                .try_into()
257                .map_err(|_| ServiceError::GroupsV2Error)?,
258        );
259        let group_secret_params =
260            GroupSecretParams::derive_from_master_key(group_master_key);
261        let authorization = self
262            .get_authorization_for_today(csprng, group_secret_params)
263            .await?;
264        self.identified_push_service.get_group(authorization).await
265    }
266
267    #[tracing::instrument(
268        skip(self, group_secret_params),
269        fields(path = %path[..4.min(path.len())]),
270    )]
271    pub async fn retrieve_avatar(
272        &mut self,
273        path: &str,
274        group_secret_params: GroupSecretParams,
275    ) -> Result<Option<Vec<u8>>, ServiceError> {
276        let mut encrypted_avatar = self
277            .unidentified_websocket
278            .retrieve_groups_v2_profile_avatar(path)
279            .await?;
280        let mut result = Vec::with_capacity(10 * 1024 * 1024);
281        encrypted_avatar.read_to_end(&mut result).await?;
282        Ok(GroupOperations::new(group_secret_params).decrypt_avatar(&result))
283    }
284
285    pub fn decrypt_group_context(
286        &self,
287        group_context: GroupContextV2,
288    ) -> Result<Option<GroupChanges>, GroupDecodingError> {
289        match (group_context.master_key, group_context.group_change) {
290            (Some(master_key), Some(group_change)) => {
291                let master_key_bytes: [u8; 32] = master_key
292                    .try_into()
293                    .map_err(|_| GroupDecodingError::WrongBlob)?;
294                let group_master_key = GroupMasterKey::new(master_key_bytes);
295                let group_secret_params =
296                    GroupSecretParams::derive_from_master_key(group_master_key);
297                let encrypted_group_change =
298                    prost::Message::decode(Bytes::from(group_change))?;
299                let group_change = GroupOperations::new(group_secret_params)
300                    .decrypt_group_change(encrypted_group_change)?;
301                Ok(Some(group_change))
302            },
303            _ => Ok(None),
304        }
305    }
306}
307
308pub fn decrypt_group(
309    master_key_bytes: &[u8],
310    encrypted_group: crate::proto::Group,
311) -> Result<Group, ServiceError> {
312    let group_master_key = GroupMasterKey::new(
313        master_key_bytes
314            .try_into()
315            .expect("wrong master key bytes length"),
316    );
317    let group_secret_params =
318        GroupSecretParams::derive_from_master_key(group_master_key);
319
320    Ok(GroupOperations::new(group_secret_params)
321        .decrypt_group(encrypted_group)?)
322}
323
324fn current_days_seconds() -> (u64, u64) {
325    let days_seconds = |date: NaiveDate| {
326        date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
327            .and_utc()
328            .timestamp() as u64
329    };
330
331    let today = Utc::now().naive_utc().date();
332    let today_plus_7_days = today + Days::new(7);
333
334    (days_seconds(today), days_seconds(today_plus_7_days))
335}