Skip to main content

libsignal_service/
storage_service.rs

1//! Signal Storage Service client.
2//!
3//! Storage Service is Signal's encrypted, server-stored, multi-device-shared
4//! account state (contacts, group memberships, account settings, …). It
5//! superseded the legacy `SyncMessage::Contacts` mechanism around 2019:
6//! modern primaries answer a legacy contact-sync request with an empty stub
7//! and expect linked devices to pull state from here instead.
8//!
9//! ## Crypto
10//!
11//! Every blob (the manifest and each item) is AES-256-GCM with a 12-byte IV
12//! prepended — wire format `iv (12B) || ciphertext+tag`.
13//!
14//! - manifest key      = `HMAC-SHA256(storage_key, "Manifest_{version}")`
15//! - item key (modern) = `HKDF-SHA256(ikm = recordIkm, info = "20240801_SIGNAL_STORAGE_SERVICE_ITEM_" || raw_id)`
16//! - item key (legacy) = `HMAC-SHA256(storage_key, "Item_" + base64(raw_id))`
17//!
18//! `storage_key` is the account-derived [`StorageServiceKey`].
19//!
20//! Reference (Signal-Android, tag v8.3.1):
21//! - `lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageServiceApi.kt`
22//! - `lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageCipher.kt`
23//! - `lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/RecordIkm.kt`
24//! - `core/models-jvm/src/main/java/org/signal/core/models/storageservice/StorageKey.kt`
25
26use aes::cipher::typenum::Unsigned;
27use aes_gcm::{
28    aead::{Aead, AeadCore},
29    AeadInOut,
30};
31use aes_gcm::{Aes256Gcm, KeyInit as _};
32use base64::Engine;
33use hkdf::Hkdf;
34use hmac::{Hmac, Mac};
35use prost::Message;
36use rand::TryRngCore;
37use reqwest::Method;
38use serde::Deserialize;
39use sha2::Sha256;
40
41use crate::configuration::Endpoint;
42use crate::master_key::StorageServiceKey;
43use crate::proto::{
44    ManifestRecord, ReadOperation, StorageItem, StorageItems, StorageManifest,
45    StorageRecord,
46};
47use crate::push_service::protobuf::ProtobufResponseExt;
48use crate::push_service::ReqwestExt;
49use crate::push_service::{
50    HttpAuth, HttpAuthOverride, PushService, ServiceError,
51};
52
53const IV_LEN: usize = 12;
54const ITEM_KEY_INFO_PREFIX: &[u8] = b"20240801_SIGNAL_STORAGE_SERVICE_ITEM_";
55
56/// Errors from the Storage Service.
57#[derive(Debug, thiserror::Error)]
58pub enum StorageServiceError {
59    /// The blob couldn't be decrypted or didn't decode — wrong key, tampered,
60    /// truncated, or not the protobuf we expected. Not distinguished further
61    /// because there's nothing the caller can do differently.
62    #[error("invalid storage service blob")]
63    Invalid,
64    #[error("network / service error: {0}")]
65    Service(#[from] ServiceError),
66}
67
68impl From<prost::DecodeError> for StorageServiceError {
69    fn from(_: prost::DecodeError) -> Self {
70        StorageServiceError::Invalid
71    }
72}
73
74impl From<reqwest::Error> for StorageServiceError {
75    fn from(e: reqwest::Error) -> Self {
76        StorageServiceError::Service(e.into())
77    }
78}
79
80/// Body of `GET /v1/storage/auth`.
81#[derive(Debug, Deserialize)]
82struct StorageAuthResponse {
83    username: String,
84    password: String,
85}
86
87/// Authenticated Storage Service handle.
88///
89/// Wraps a [`PushService`] plus the short-lived basic-auth credentials and
90/// the account [`StorageServiceKey`], so callers get decrypted protobufs
91/// straight out and never touch the wire crypto themselves.
92pub struct StorageService {
93    service: PushService,
94    credentials: HttpAuth,
95    storage_key: StorageServiceKey,
96}
97
98impl StorageService {
99    /// Authenticate against the storage service.
100    ///
101    /// Fetches a fresh basic-auth token (`GET /v1/storage/auth`); the token
102    /// is good for ~24h server-side but cheap enough to re-fetch per sync.
103    pub async fn new(
104        service: PushService,
105        storage_key: StorageServiceKey,
106    ) -> Result<Self, ServiceError> {
107        let resp: StorageAuthResponse = service
108            .request(
109                Method::GET,
110                Endpoint::service("/v1/storage/auth"),
111                HttpAuthOverride::NoOverride,
112            )?
113            .send()
114            .await?
115            .service_error_for_status()
116            .await?
117            .json()
118            .await?;
119        let credentials = HttpAuth {
120            username: resp.username,
121            password: resp.password,
122        };
123        Ok(Self {
124            service,
125            credentials,
126            storage_key,
127        })
128    }
129
130    /// Fetch and decrypt the latest manifest.
131    pub async fn manifest(
132        &self,
133    ) -> Result<ManifestRecord, StorageServiceError> {
134        let manifest: StorageManifest = self
135            .service
136            .request(
137                Method::GET,
138                Endpoint::storage("/v1/storage/manifest"),
139                HttpAuthOverride::Identified(self.credentials.clone()),
140            )?
141            .send()
142            .await?
143            .service_error_for_status()
144            .await?
145            .protobuf()
146            .await?;
147        Self::decrypt_manifest(&self.storage_key, &manifest)
148    }
149
150    /// Fetch and decrypt the manifest only if the server's version differs
151    /// from `version`. `Ok(None)` means the server matched (HTTP 204).
152    pub async fn manifest_if_changed(
153        &self,
154        version: u64,
155    ) -> Result<Option<ManifestRecord>, StorageServiceError> {
156        let response = self
157            .service
158            .request(
159                Method::GET,
160                Endpoint::storage(format!(
161                    "/v1/storage/manifest/version/{version}"
162                )),
163                HttpAuthOverride::Identified(self.credentials.clone()),
164            )?
165            .send()
166            .await?
167            .service_error_for_status()
168            .await?;
169
170        if response.status().as_u16() == 204 {
171            return Ok(None);
172        }
173        let manifest: StorageManifest = response.protobuf().await?;
174        Ok(Some(Self::decrypt_manifest(&self.storage_key, &manifest)?))
175    }
176
177    /// Fetch and decrypt storage items by key.
178    ///
179    /// `keys` are `Identifier.raw` blobs from [`ManifestRecord::identifiers`];
180    /// `record_ikm` is [`ManifestRecord::record_ikm`] (empty on legacy
181    /// accounts, in which case the per-item key is derived from the storage
182    /// key directly). Items the server doesn't return are simply absent from
183    /// the result.
184    pub async fn read_items(
185        &self,
186        keys: Vec<Vec<u8>>,
187        record_ikm: Option<&[u8]>,
188    ) -> Result<Vec<StorageRecord>, StorageServiceError> {
189        let body = ReadOperation { read_key: keys };
190        let mut buf = Vec::with_capacity(body.encoded_len());
191        body.encode(&mut buf).expect("infallible encode into Vec");
192
193        let items: StorageItems = self
194            .service
195            .request(
196                Method::PUT,
197                Endpoint::storage("/v1/storage/read"),
198                HttpAuthOverride::Identified(self.credentials.clone()),
199            )?
200            .header("Content-Type", "application/x-protobuf")
201            .body(buf)
202            .send()
203            .await?
204            .service_error_for_status()
205            .await?
206            .protobuf()
207            .await?;
208
209        items
210            .items
211            .iter()
212            .map(|item| Self::decrypt_item(&self.storage_key, item, record_ikm))
213            .collect()
214    }
215
216    // -- crypto ------------------------------------------------------------
217
218    /// Decrypt a [`StorageManifest`] into a [`ManifestRecord`].
219    pub fn decrypt_manifest(
220        storage_key: &StorageServiceKey,
221        manifest: &StorageManifest,
222    ) -> Result<ManifestRecord, StorageServiceError> {
223        let key = Self::manifest_key(storage_key, manifest.version);
224        let plaintext = decrypt(&key, &manifest.value)?;
225        Ok(ManifestRecord::decode(&*plaintext)?)
226    }
227
228    /// Encrypt a [`ManifestRecord`] into a [`StorageManifest`] ready to PUT.
229    pub fn encrypt_manifest(
230        storage_key: &StorageServiceKey,
231        record: &ManifestRecord,
232    ) -> StorageManifest {
233        let key = Self::manifest_key(storage_key, record.version);
234        StorageManifest {
235            version: record.version,
236            value: encrypt(&key, &record.encode_to_vec()),
237        }
238    }
239
240    /// Decrypt a [`StorageItem`] into a [`StorageRecord`].
241    pub fn decrypt_item(
242        storage_key: &StorageServiceKey,
243        item: &StorageItem,
244        record_ikm: Option<&[u8]>,
245    ) -> Result<StorageRecord, StorageServiceError> {
246        let key = Self::item_key(storage_key, &item.key, record_ikm);
247        let plaintext = decrypt(&key, &item.value)?;
248        Ok(StorageRecord::decode(&*plaintext)?)
249    }
250
251    /// Encrypt a [`StorageRecord`] into a [`StorageItem`] ready to PUT.
252    ///
253    /// `raw_id` is the item's identifier; `record_ikm` should match what's
254    /// in the manifest this item will be referenced from.
255    pub fn encrypt_item(
256        storage_key: &StorageServiceKey,
257        raw_id: Vec<u8>,
258        record: &StorageRecord,
259        record_ikm: Option<&[u8]>,
260    ) -> StorageItem {
261        let key = Self::item_key(storage_key, &raw_id, record_ikm);
262        StorageItem {
263            key: raw_id,
264            value: encrypt(&key, &record.encode_to_vec()),
265        }
266    }
267
268    /// `HMAC-SHA256(storage_key, "Manifest_{version}")`.
269    fn manifest_key(storage_key: &StorageServiceKey, version: u64) -> [u8; 32] {
270        let mut mac = Hmac::<Sha256>::new_from_slice(&storage_key.inner)
271            .expect("HMAC accepts any key length");
272        mac.update(b"Manifest_");
273        mac.update(version.to_string().as_bytes());
274        mac.finalize().into_bytes().into()
275    }
276
277    /// Per-item key. Modern accounts carry a `record_ikm` in the manifest and
278    /// derive via HKDF; legacy accounts derive straight off the storage key.
279    fn item_key(
280        storage_key: &StorageServiceKey,
281        raw_id: &[u8],
282        record_ikm: Option<&[u8]>,
283    ) -> [u8; 32] {
284        match record_ikm {
285            Some(ikm) if !ikm.is_empty() => {
286                let hk = Hkdf::<Sha256>::new(None, ikm);
287                let mut okm = [0u8; 32];
288                hk.expand_multi_info(&[ITEM_KEY_INFO_PREFIX, raw_id], &mut okm)
289                    .expect("32-byte HKDF output is valid");
290                okm
291            },
292            _ => {
293                let b64 =
294                    base64::engine::general_purpose::STANDARD.encode(raw_id);
295                let mut mac =
296                    Hmac::<Sha256>::new_from_slice(&storage_key.inner)
297                        .expect("HMAC accepts any key length");
298                mac.update(b"Item_");
299                mac.update(b64.as_bytes());
300                mac.finalize().into_bytes().into()
301            },
302        }
303    }
304}
305
306/// AES-256-GCM decrypt of `iv(12) || ciphertext+tag`.
307fn decrypt(
308    key: &[u8; 32],
309    blob: &[u8],
310) -> Result<Vec<u8>, StorageServiceError> {
311    let (iv, ct) = blob
312        .split_first_chunk::<IV_LEN>()
313        .ok_or(StorageServiceError::Invalid)?;
314    Aes256Gcm::new(key.into())
315        .decrypt(iv.into(), ct)
316        .map_err(|_| StorageServiceError::Invalid)
317}
318
319/// AES-256-GCM encrypt, producing `iv(12) || ciphertext+tag` with a fresh
320/// random IV.
321fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Vec<u8> {
322    let mut iv = [0u8; IV_LEN];
323    rand::rngs::OsRng
324        .try_fill_bytes(&mut iv)
325        .expect("OS RNG available");
326
327    // Single allocation: IV + plaintext + tag
328    let mut out = Vec::with_capacity(
329        IV_LEN + plaintext.len() + <Aes256Gcm as AeadCore>::TagSize::to_usize(),
330    );
331    out.extend_from_slice(&iv);
332    out.extend_from_slice(plaintext);
333
334    // Encrypt in place - returns tag separately
335    let tag = Aes256Gcm::new(key.into())
336        .encrypt_inout_detached((&iv).into(), b"", (&mut out[IV_LEN..]).into())
337        .expect("AES-256-GCM encryption is infallible for valid keys");
338
339    // Append the tag
340    out.extend_from_slice(&tag);
341
342    out
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn manifest_round_trip() {
351        let storage_key = StorageServiceKey { inner: [7u8; 32] };
352        let record = ManifestRecord {
353            version: 42,
354            source_device: 1,
355            identifiers: vec![],
356            record_ikm: vec![],
357        };
358        let encrypted = StorageService::encrypt_manifest(&storage_key, &record);
359        assert_eq!(encrypted.version, 42);
360        let decrypted =
361            StorageService::decrypt_manifest(&storage_key, &encrypted).unwrap();
362        assert_eq!(decrypted, record);
363    }
364
365    #[test]
366    fn item_round_trip_modern_and_legacy() {
367        let storage_key = StorageServiceKey { inner: [9u8; 32] };
368        let raw_id = vec![0xABu8; 16];
369        let record = StorageRecord { record: None };
370
371        // Legacy path (no record_ikm).
372        let legacy = StorageService::encrypt_item(
373            &storage_key,
374            raw_id.clone(),
375            &record,
376            None,
377        );
378        assert_eq!(
379            StorageService::decrypt_item(&storage_key, &legacy, None).unwrap(),
380            record
381        );
382
383        // Modern path (HKDF off a record_ikm).
384        let ikm = [4u8; 32];
385        let modern = StorageService::encrypt_item(
386            &storage_key,
387            raw_id.clone(),
388            &record,
389            Some(&ikm),
390        );
391        assert_eq!(
392            StorageService::decrypt_item(&storage_key, &modern, Some(&ikm))
393                .unwrap(),
394            record
395        );
396    }
397
398    #[test]
399    fn modern_and_legacy_keys_differ() {
400        let storage_key = StorageServiceKey { inner: [3u8; 32] };
401        let raw_id = [5u8; 16];
402        let legacy = StorageService::item_key(&storage_key, &raw_id, None);
403        let modern =
404            StorageService::item_key(&storage_key, &raw_id, Some(&[4u8; 32]));
405        assert_ne!(legacy, modern);
406    }
407
408    #[test]
409    fn manifest_key_changes_with_version() {
410        let storage_key = StorageServiceKey { inner: [1u8; 32] };
411        assert_ne!(
412            StorageService::manifest_key(&storage_key, 1),
413            StorageService::manifest_key(&storage_key, 2)
414        );
415    }
416
417    #[test]
418    fn wrong_key_fails_to_decrypt() {
419        let a = StorageServiceKey { inner: [1u8; 32] };
420        let b = StorageServiceKey { inner: [2u8; 32] };
421        let record = ManifestRecord {
422            version: 1,
423            source_device: 0,
424            identifiers: vec![],
425            record_ikm: vec![],
426        };
427        let encrypted = StorageService::encrypt_manifest(&a, &record);
428        assert!(matches!(
429            StorageService::decrypt_manifest(&b, &encrypted),
430            Err(StorageServiceError::Invalid)
431        ));
432    }
433}