@@ -17,6 +17,7 @@ use std::{
1717 fmt,
1818 ops:: { Deref , Not as _} ,
1919 sync:: Arc ,
20+ time:: Duration ,
2021} ;
2122
2223use ruma:: {
@@ -327,6 +328,14 @@ pub struct Account {
327328 /// needs to set this for us, depending on the count we will suggest the
328329 /// client to upload new keys.
329330 uploaded_signed_key_count : u64 ,
331+ /// The timestamp of the last time we generated a fallback key. Fallback
332+ /// keys are rotated in a time-based manner, this field records when we
333+ /// either generated our first fallback key or rotated one.
334+ ///
335+ /// Will be `None` if we never created a fallback key, or if we're migrating
336+ /// from a `AccountPickle` that didn't use time-based fallback key
337+ /// rotation.
338+ fallback_creation_timestamp : Option < MilliSecondsSinceUnixEpoch > ,
330339}
331340
332341impl Deref for Account {
@@ -358,6 +367,9 @@ pub struct PickledAccount {
358367 /// as creation time of own device
359368 #[ serde( default = "default_account_creation_time" ) ]
360369 pub creation_local_time : MilliSecondsSinceUnixEpoch ,
370+ /// The timestamp of the last time we generated a fallback key.
371+ #[ serde( default ) ]
372+ pub fallback_key_creation_timestamp : Option < MilliSecondsSinceUnixEpoch > ,
361373}
362374
363375fn default_account_creation_time ( ) -> MilliSecondsSinceUnixEpoch {
@@ -404,6 +416,7 @@ impl Account {
404416 inner : Box :: new ( account) ,
405417 shared : false ,
406418 uploaded_signed_key_count : 0 ,
419+ fallback_creation_timestamp : None ,
407420 }
408421 }
409422
@@ -496,11 +509,11 @@ impl Account {
496509 self . generate_one_time_keys_if_needed ( ) ;
497510 }
498511
499- if let Some ( unused ) = unused_fallback_keys {
500- if !unused . contains ( & DeviceKeyAlgorithm :: SignedCurve25519 ) {
501- // Generate a new fallback key if we don't have one .
502- self . generate_fallback_key_helper ( ) ;
503- }
512+ // If the server supports fallback keys or if it did so in the past, shown by
513+ // the existence of a fallback creation timestamp, generate a new one if
514+ // we don't have one, or if the current fallback key expired .
515+ if unused_fallback_keys . is_some ( ) || self . fallback_creation_timestamp . is_some ( ) {
516+ self . generate_fallback_key_if_needed ( ) ;
504517 }
505518 }
506519
@@ -543,17 +556,59 @@ impl Account {
543556 Some ( key_count as u64 )
544557 }
545558
546- pub ( crate ) fn generate_fallback_key_helper ( & mut self ) {
547- if self . inner . fallback_key ( ) . is_empty ( ) {
559+ pub ( crate ) fn generate_fallback_key_if_needed ( & mut self ) {
560+ if self . inner . fallback_key ( ) . is_empty ( ) && self . fallback_key_expired ( ) {
548561 let removed_fallback_key = self . inner . generate_fallback_key ( ) ;
562+ self . fallback_creation_timestamp = Some ( MilliSecondsSinceUnixEpoch :: now ( ) ) ;
549563
550564 debug ! (
551565 ?removed_fallback_key,
552- "No unused fallback keys were found on the server , generated a new fallback key." ,
566+ "The fallback key either expired or we didn't have one , generated a new fallback key." ,
553567 ) ;
554568 }
555569 }
556570
571+ /// Check if our most recent fallback key has expired.
572+ ///
573+ /// We consider the fallback key to be expired if it's older than a week.
574+ /// This is the lower bound for the recommended signed pre-key bundle
575+ /// rotation interval in the X3DH spec[1].
576+ ///
577+ /// [1]: https://signal.org/docs/specifications/x3dh/#publishing-keys
578+ fn fallback_key_expired ( & self ) -> bool {
579+ const FALLBACK_KEY_MAX_AGE : Duration = Duration :: from_secs ( 3600 * 24 * 7 ) ;
580+
581+ if let Some ( time) = self . fallback_creation_timestamp {
582+ // `to_system_time()` returns `None` if the the UNIX_EPOCH + `time` doesn't fit
583+ // into a i64. This will likely never happen, but let's rotate the
584+ // key in case the values are messed up for some other reason.
585+ let Some ( system_time) = time. to_system_time ( ) else {
586+ return true ;
587+ } ;
588+
589+ // `elapsed()` errors if the `system_time` is in the future, this should mean
590+ // that our clock has changed to the past, let's rotate just in case
591+ // and then we'll get to a normal time.
592+ let Ok ( elapsed) = system_time. elapsed ( ) else {
593+ return true ;
594+ } ;
595+
596+ // Alright, our times are normal and we know how much time elapsed since the
597+ // last time we created/rotated a fallback key.
598+ //
599+ // If the key is older than a week, then we rotate it.
600+ if elapsed > FALLBACK_KEY_MAX_AGE {
601+ true
602+ } else {
603+ false
604+ }
605+ } else {
606+ // We never created a fallback key, or we're migrating to the time-based
607+ // fallback key rotation, so let's generate a new fallback key.
608+ true
609+ }
610+ }
611+
557612 fn fallback_key ( & self ) -> HashMap < KeyId , Curve25519PublicKey > {
558613 self . inner . fallback_key ( )
559614 }
@@ -595,6 +650,7 @@ impl Account {
595650 shared : self . shared ( ) ,
596651 uploaded_signed_key_count : self . uploaded_key_count ( ) ,
597652 creation_local_time : self . static_data . creation_local_time ,
653+ fallback_key_creation_timestamp : self . fallback_creation_timestamp ,
598654 }
599655 }
600656
@@ -651,6 +707,7 @@ impl Account {
651707 inner : Box :: new ( account) ,
652708 shared : pickle. shared ,
653709 uploaded_signed_key_count : pickle. uploaded_signed_key_count ,
710+ fallback_creation_timestamp : pickle. fallback_key_creation_timestamp ,
654711 } )
655712 }
656713
@@ -1371,6 +1428,7 @@ mod tests {
13711428 use std:: {
13721429 collections:: { BTreeMap , BTreeSet } ,
13731430 ops:: Deref ,
1431+ time:: Duration ,
13741432 } ;
13751433
13761434 use anyhow:: Result ;
@@ -1442,30 +1500,59 @@ mod tests {
14421500 // We don't create fallback keys since we don't know if the server
14431501 // supports them, we need to receive a sync response to decide if we're
14441502 // going to create them or not.
1445- assert ! ( fallback_keys. is_empty( ) ) ;
1503+ assert ! (
1504+ fallback_keys. is_empty( ) ,
1505+ "We should not upload fallback keys until we know if the server supports them."
1506+ ) ;
14461507
14471508 let one_time_keys = BTreeMap :: from ( [ ( DeviceKeyAlgorithm :: SignedCurve25519 , 50u8 . into ( ) ) ] ) ;
14481509
14491510 // A `None` here means that the server doesn't support fallback keys, no
14501511 // fallback key gets uploaded.
14511512 account. update_key_counts ( & one_time_keys, None ) ;
14521513 let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1453- assert ! ( fallback_keys. is_empty( ) ) ;
1514+ assert ! (
1515+ fallback_keys. is_empty( ) ,
1516+ "We should not upload a fallback key if we're certain that the server doesn't support \
1517+ them."
1518+ ) ;
14541519
14551520 // The empty array means that the server supports fallback keys but
14561521 // there isn't a unused fallback key on the server. This time we upload
14571522 // a fallback key.
14581523 let unused_fallback_keys = & [ ] ;
14591524 account. update_key_counts ( & one_time_keys, Some ( unused_fallback_keys. as_ref ( ) ) ) ;
14601525 let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1461- assert ! ( !fallback_keys. is_empty( ) ) ;
1526+ assert ! (
1527+ !fallback_keys. is_empty( ) ,
1528+ "We should upload the initial fallback key if the server supports them."
1529+ ) ;
14621530 account. mark_keys_as_published ( ) ;
14631531
1464- // There's an unused fallback key on the server, nothing to do here.
1465- let unused_fallback_keys = & [ DeviceKeyAlgorithm :: SignedCurve25519 ] ;
1532+ // There's no unused fallback key on the server, but our initial fallback key
1533+ // did not yet expire.
1534+ let unused_fallback_keys = & [ ] ;
14661535 account. update_key_counts ( & one_time_keys, Some ( unused_fallback_keys. as_ref ( ) ) ) ;
14671536 let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1468- assert ! ( fallback_keys. is_empty( ) ) ;
1537+ assert ! (
1538+ fallback_keys. is_empty( ) ,
1539+ "We should not upload new fallback keys unless our current fallback key expires."
1540+ ) ;
1541+
1542+ let fallback_key_timestamp =
1543+ account. fallback_creation_timestamp . unwrap ( ) . to_system_time ( ) . unwrap ( )
1544+ - Duration :: from_secs ( 3600 * 24 * 30 ) ;
1545+
1546+ account. fallback_creation_timestamp =
1547+ Some ( MilliSecondsSinceUnixEpoch :: from_system_time ( fallback_key_timestamp) . unwrap ( ) ) ;
1548+
1549+ account. update_key_counts ( & one_time_keys, None ) ;
1550+ let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1551+ assert ! (
1552+ !fallback_keys. is_empty( ) ,
1553+ "Now that our fallback key has expired, we should try to upload a new one, even if the \
1554+ server supposedly doesn't support fallback keys anymore"
1555+ ) ;
14691556
14701557 Ok ( ( ) )
14711558 }
0 commit comments