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