libsignal_service/groups_v2/
manager.rs1use 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};
14
15use base64::prelude::*;
16use bytes::Bytes;
17use chrono::{Days, NaiveDate, NaiveTime, Utc};
18use futures::AsyncReadExt;
19use rand::{CryptoRng, Rng};
20use reqwest::Method;
21use serde::Deserialize;
22use zkgroup::{
23 auth::AuthCredentialWithPniResponse,
24 groups::{GroupMasterKey, GroupSecretParams},
25 ServerPublicParams,
26};
27
28#[derive(Debug, serde::Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct TemporalCredential {
31 credential: String,
32 redemption_time: u64,
33}
34
35#[derive(Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct CredentialResponse {
38 credentials: Vec<TemporalCredential>,
39}
40
41#[expect(clippy::result_large_err)]
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
65pub trait CredentialsCache {
70 fn clear(&mut self) -> Result<(), CredentialsCacheError>;
71
72 fn get(
74 &self,
75 key: &u64,
76 ) -> Result<Option<&AuthCredentialWithPniResponse>, CredentialsCacheError>;
77
78 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 push_service: PushService,
137 credentials_cache: C,
138 server_public_params: ServerPublicParams,
139}
140
141impl<C: CredentialsCache> GroupsManager<C> {
142 pub fn new(
143 service_ids: ServiceIds,
144 push_service: PushService,
145 credentials_cache: C,
146 server_public_params: ServerPublicParams,
147 ) -> Self {
148 Self {
149 service_ids,
150 push_service,
151 credentials_cache,
152 server_public_params,
153 }
154 }
155
156 pub async fn get_authorization_for_today<R: Rng + CryptoRng>(
157 &mut self,
158 csprng: &mut R,
159 group_secret_params: GroupSecretParams,
160 ) -> Result<HttpAuth, ServiceError> {
161 let (today, today_plus_7_days) = current_days_seconds();
162
163 let auth_credential_response = if let Some(auth_credential_response) =
164 self.credentials_cache.get(&today)?
165 {
166 auth_credential_response
167 } else {
168 let path =
169 format!("/v1/certificate/auth/group?redemptionStartSeconds={}&redemptionEndSeconds={}&pniAsServiceId=true", today, today_plus_7_days);
170
171 let credentials_response: CredentialResponse = self
172 .push_service
173 .request(
174 Method::GET,
175 Endpoint::service(path),
176 HttpAuthOverride::NoOverride,
177 )?
178 .send()
179 .await?
180 .service_error_for_status()
181 .await?
182 .json()
183 .await?;
184 self.credentials_cache
185 .write(credentials_response.parse()?)?;
186 self.credentials_cache.get(&today)?.ok_or({
187 ServiceError::InvalidFrame {
188 reason:
189 "credentials received did not contain requested day",
190 }
191 })?
192 };
193
194 self.get_authorization_string(
195 csprng,
196 group_secret_params,
197 auth_credential_response.clone(),
198 today,
199 )
200 }
201
202 #[expect(clippy::result_large_err)]
203 fn get_authorization_string<R: Rng + CryptoRng>(
204 &self,
205 csprng: &mut R,
206 group_secret_params: GroupSecretParams,
207 credential_response: AuthCredentialWithPniResponse,
208 today: u64,
209 ) -> Result<HttpAuth, ServiceError> {
210 let auth_credential = self
211 .server_public_params
212 .receive_auth_credential_with_pni_as_service_id(
213 self.service_ids.aci(),
214 self.service_ids.pni(),
215 zkgroup::Timestamp::from_epoch_seconds(today),
216 credential_response,
217 )
218 .map_err(|e| {
219 tracing::error!(
220 "failed to receive auth credentials with PNI: {:?}",
221 e
222 );
223 ServiceError::GroupsV2Error
224 })?;
225
226 let mut random_bytes = [0u8; 32];
227 csprng.fill_bytes(&mut random_bytes);
228
229 let auth_credential_presentation = self
230 .server_public_params
231 .create_auth_credential_with_pni_presentation(
232 random_bytes,
233 group_secret_params,
234 auth_credential,
235 );
236
237 let username = hex::encode(bincode::serialize(
240 &group_secret_params.get_public_params(),
241 )?);
242
243 let password =
244 hex::encode(bincode::serialize(&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.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 .push_service
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
308#[expect(clippy::result_large_err)]
309pub fn decrypt_group(
310 master_key_bytes: &[u8],
311 encrypted_group: crate::proto::Group,
312) -> Result<Group, ServiceError> {
313 let group_master_key = GroupMasterKey::new(
314 master_key_bytes
315 .try_into()
316 .expect("wrong master key bytes length"),
317 );
318 let group_secret_params =
319 GroupSecretParams::derive_from_master_key(group_master_key);
320
321 Ok(GroupOperations::new(group_secret_params)
322 .decrypt_group(encrypted_group)?)
323}
324
325fn current_days_seconds() -> (u64, u64) {
326 let days_seconds = |date: NaiveDate| {
327 date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
328 .and_utc()
329 .timestamp() as u64
330 };
331
332 let today = Utc::now().naive_utc().date();
333 let today_plus_7_days = today + Days::new(7);
334
335 (days_seconds(today), days_seconds(today_plus_7_days))
336}