whatsapp_rust/features/
rotate_key.rs1use crate::client::{Client, SignalMaintenanceError};
9use crate::request::IqError;
10use wacore::iq::prekeys::RotateSignedPreKeySpec;
11use wacore::libsignal::protocol::{KeyPair, PrivateKey, PublicKey, SignalProtocolError};
12use wacore::libsignal::store::record_helpers::new_signed_pre_key_record;
13use wacore::store::commands::DeviceCommand;
14
15pub(crate) const SIGNED_PRE_KEY_ROTATION_INTERVAL_MS: i64 = 7 * 24 * 60 * 60 * 1000; pub(crate) const SIGNED_PRE_KEY_RETENTION: usize = 3;
24
25const MAX_SIGNED_PRE_KEY_ID: u32 = 16_777_215;
28
29fn storage_err<E>(
32 context: impl std::fmt::Display + Send + Sync + 'static,
33) -> impl FnOnce(E) -> SignalMaintenanceError
34where
35 E: std::error::Error + Send + Sync + 'static,
36{
37 move |e| SignalMaintenanceError::Storage(anyhow::Error::new(e).context(context))
38}
39
40pub(crate) fn should_rotate_signed_pre_key(last_rotation_ms: i64, now_ms: i64) -> bool {
43 last_rotation_ms != 0
44 && now_ms.saturating_sub(last_rotation_ms) >= SIGNED_PRE_KEY_ROTATION_INTERVAL_MS
45}
46
47pub(crate) fn next_signed_pre_key_id(current: u32) -> u32 {
49 if current >= MAX_SIGNED_PRE_KEY_ID {
50 1
51 } else {
52 current + 1
53 }
54}
55
56impl Client {
57 pub(crate) async fn maybe_rotate_signed_pre_key(&self) -> Result<(), SignalMaintenanceError> {
60 let Some(_guard) = self.signed_pre_key_rotation_lock.try_lock() else {
64 return Ok(());
65 };
66
67 let last = self
68 .persistence_manager
69 .get_device_snapshot()
70 .last_signed_pre_key_rotation_ms;
71 let now = wacore::time::now_millis();
72
73 if last == 0 {
74 self.persistence_manager
75 .process_command(DeviceCommand::SetSignedPreKeyRotationBaseline(now))
76 .await;
77 self.persistence_manager
78 .flush()
79 .await
80 .map_err(storage_err("failed to flush rotation baseline"))?;
81 return Ok(());
82 }
83
84 if should_rotate_signed_pre_key(last, now) {
85 self.rotate_signed_pre_key_inner().await?;
86 }
87 Ok(())
88 }
89
90 pub async fn rotate_signed_pre_key(&self) -> Result<(), SignalMaintenanceError> {
104 let _guard = self.signed_pre_key_rotation_lock.lock().await;
105 self.rotate_signed_pre_key_inner().await
106 }
107
108 async fn rotate_signed_pre_key_inner(&self) -> Result<(), SignalMaintenanceError> {
109 let snapshot = self.persistence_manager.get_device_snapshot();
110 let now = wacore::time::now_millis();
111 let backend = self.persistence_manager.backend();
112
113 let old_id = snapshot.signed_pre_key_id;
114 let new_id = next_signed_pre_key_id(old_id);
115
116 let (new_kp, signature) = match backend
121 .load_signed_prekey(new_id)
122 .await
123 .map_err(storage_err("failed to load staged signed pre-key"))?
124 {
125 Some(bytes) => {
126 let s = waproto::codec::signed_pre_key_record_decode(&bytes).map_err(|e| {
127 SignalMaintenanceError::CorruptKey(format!("staged record decode: {e}"))
128 })?;
129 let public = PublicKey::from_djb_public_key_bytes(s.public_key.as_deref().ok_or(
130 SignalMaintenanceError::CorruptKey("staged record missing public".to_string()),
131 )?)
132 .map_err(|e| {
133 SignalMaintenanceError::CorruptKey(format!("staged record public key: {e}"))
134 })?;
135 let private = PrivateKey::deserialize(s.private_key.as_deref().ok_or(
136 SignalMaintenanceError::CorruptKey("staged record missing private".to_string()),
137 )?)
138 .map_err(|e| {
139 SignalMaintenanceError::CorruptKey(format!("staged record private key: {e}"))
140 })?;
141 let signature: [u8; 64] = s
142 .signature
143 .ok_or(SignalMaintenanceError::CorruptKey(
144 "staged record missing signature".to_string(),
145 ))?
146 .try_into()
147 .map_err(|_| {
148 SignalMaintenanceError::CorruptKey(
149 "staged signature must be 64 bytes".to_string(),
150 )
151 })?;
152 (KeyPair::new(public, private), signature)
153 }
154 None => {
155 let mut rng = rand::make_rng::<rand::rngs::StdRng>();
156 let kp = KeyPair::generate(&mut rng);
157 let signature: [u8; 64] = snapshot
160 .identity_key
161 .private_key
162 .calculate_signature(&kp.public_key.serialize(), &mut rng)
163 .map_err(|e| SignalMaintenanceError::Signal(e.into()))?
164 .as_ref()
165 .try_into()
166 .map_err(|_| {
169 SignalMaintenanceError::Signal(SignalProtocolError::InvalidState(
170 "rotate_signed_pre_key",
171 "Ed25519 signature must be 64 bytes".to_string(),
172 ))
173 })?;
174 let record =
175 new_signed_pre_key_record(new_id, &kp, signature, wacore::time::now_utc());
176 backend
177 .store_signed_prekey(
178 new_id,
179 &waproto::codec::signed_pre_key_record_to_vec(&record),
180 )
181 .await
182 .map_err(storage_err("failed to stage new signed pre-key"))?;
183 (kp, signature)
184 }
185 };
186
187 let old_record = new_signed_pre_key_record(
192 old_id,
193 &snapshot.signed_pre_key,
194 snapshot.signed_pre_key_signature,
195 wacore::time::now_utc(),
196 );
197 backend
198 .store_signed_prekey(
199 old_id,
200 &waproto::codec::signed_pre_key_record_to_vec(&old_record),
201 )
202 .await
203 .map_err(storage_err("failed to retain old signed pre-key"))?;
204
205 let upload_result = self
210 .execute(RotateSignedPreKeySpec::new(
211 new_id,
212 new_kp.public_key,
213 signature.to_vec(),
214 ))
215 .await;
216 match upload_result {
217 Ok(()) => {}
218 Err(error) => {
219 if let IqError::ServerError { code, text, .. } = &error {
220 if *code == 406 || *code == 409 {
229 backend
230 .remove_signed_prekey(new_id)
231 .await
232 .map_err(storage_err(format!(
233 "failed to drop rejected staged signed pre-key {new_id}"
234 )))?;
235 log::warn!(
236 "signed pre-key rotation rejected (code={code}, text='{text}'); \
237 discarded the rejected key, will remint on a later connect"
238 );
239 } else {
240 log::warn!(
241 "signed pre-key rotation upload rejected (code={code}, text='{text}'); \
242 keeping the staged key, will retry on a later connect"
243 );
244 }
245 } else {
246 log::warn!(
249 "signed pre-key rotation upload failed: {error:?}; \
250 keeping the staged key, will retry on a later connect"
251 );
252 }
253 return Err(error.into());
254 }
255 }
256
257 self.persistence_manager
260 .process_command(DeviceCommand::SetSignedPreKey {
261 key_pair: new_kp,
262 id: new_id,
263 signature,
264 rotation_ms: now,
265 })
266 .await;
267 self.persistence_manager
268 .flush()
269 .await
270 .map_err(storage_err("failed to flush rotated signed pre-key"))?;
271
272 if let Err(e) = backend.remove_signed_prekey(new_id).await {
277 log::warn!("failed to drop staged signed pre-key {new_id}: {e}");
278 }
279 let mut retained = backend
280 .load_all_signed_prekeys()
281 .await
282 .map_err(storage_err("failed to load retained signed pre-keys"))?;
283 retained.sort_unstable_by_key(|(id, _)| std::cmp::Reverse(*id));
284 for (id, _) in retained
285 .into_iter()
286 .skip(SIGNED_PRE_KEY_RETENTION.saturating_sub(1))
287 {
288 if let Err(e) = backend.remove_signed_prekey(id).await {
289 log::warn!("failed to prune retained signed pre-key {id}: {e}");
290 }
291 }
292
293 Ok(())
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn should_rotate_truth_table() {
303 assert!(!should_rotate_signed_pre_key(0, i64::MAX));
305
306 let last = 1_000_000_000_000;
307 assert!(!should_rotate_signed_pre_key(
309 last,
310 last + SIGNED_PRE_KEY_ROTATION_INTERVAL_MS - 1
311 ));
312 assert!(should_rotate_signed_pre_key(
314 last,
315 last + SIGNED_PRE_KEY_ROTATION_INTERVAL_MS
316 ));
317 assert!(should_rotate_signed_pre_key(
319 last,
320 last + SIGNED_PRE_KEY_ROTATION_INTERVAL_MS * 3
321 ));
322 assert!(!should_rotate_signed_pre_key(last, last - 1));
324 }
325
326 #[test]
327 fn next_id_increments_and_wraps() {
328 assert_eq!(next_signed_pre_key_id(1), 2);
329 assert_eq!(next_signed_pre_key_id(41), 42);
330 assert_eq!(
331 next_signed_pre_key_id(MAX_SIGNED_PRE_KEY_ID - 1),
332 MAX_SIGNED_PRE_KEY_ID
333 );
334 assert_eq!(next_signed_pre_key_id(MAX_SIGNED_PRE_KEY_ID), 1);
336 }
337
338 #[tokio::test]
339 async fn public_rotation_uses_the_single_flight_lock() {
340 let client = crate::test_utils::create_test_client().await;
341 let guard = client.signed_pre_key_rotation_lock.lock().await;
342
343 assert!(
344 tokio::time::timeout(
345 std::time::Duration::from_millis(100),
346 client.rotate_signed_pre_key(),
347 )
348 .await
349 .is_err(),
350 "manual rotation must serialize with an active rotation"
351 );
352 drop(guard);
353 }
354
355 #[tokio::test]
356 async fn due_automatic_rotation_does_not_relock_its_single_flight_guard() {
357 let client = crate::test_utils::create_test_client().await;
358 let snapshot = client.persistence_manager.get_device_snapshot();
359 let staged_id = next_signed_pre_key_id(snapshot.signed_pre_key_id);
360 let due_baseline =
361 wacore::time::now_millis().saturating_sub(SIGNED_PRE_KEY_ROTATION_INTERVAL_MS);
362 client
363 .persistence_manager
364 .process_command(DeviceCommand::SetSignedPreKeyRotationBaseline(due_baseline))
365 .await;
366 client
367 .persistence_manager
368 .flush()
369 .await
370 .expect("persist due rotation baseline");
371
372 let error = tokio::time::timeout(
373 std::time::Duration::from_secs(5),
374 client.maybe_rotate_signed_pre_key(),
375 )
376 .await
377 .expect("automatic rotation must not recursively acquire its held lock")
378 .expect_err("the disconnected test client must fail at upload");
379 assert!(matches!(
380 error,
381 SignalMaintenanceError::Iq(IqError::NotConnected)
382 ));
383 assert!(
384 client
385 .persistence_manager
386 .backend()
387 .load_signed_prekey(staged_id)
388 .await
389 .expect("load staged signed pre-key")
390 .is_some(),
391 "the due path must reach the inner rotation flow before upload"
392 );
393 }
394
395 #[tokio::test]
396 async fn public_rotation_reports_upload_failure() {
397 let client = crate::test_utils::create_test_client().await;
398
399 let error = client
400 .rotate_signed_pre_key()
401 .await
402 .expect_err("manual rotation must report a failed upload");
403 assert!(matches!(
404 error,
405 SignalMaintenanceError::Iq(IqError::NotConnected)
406 ));
407 }
408
409 #[tokio::test]
410 async fn rotation_reports_an_unusable_staged_key_as_corrupt() {
411 let client = crate::test_utils::create_test_client().await;
412 let snapshot = client.persistence_manager.get_device_snapshot();
413 let staged_id = next_signed_pre_key_id(snapshot.signed_pre_key_id);
414 client
417 .persistence_manager
418 .backend()
419 .store_signed_prekey(staged_id, &[])
420 .await
421 .expect("stage an empty signed pre-key record");
422
423 let error = client
424 .rotate_signed_pre_key()
425 .await
426 .expect_err("an unusable staged key must abort the rotation");
427 assert!(matches!(error, SignalMaintenanceError::CorruptKey(_)));
428 }
429}