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