@@ -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,61 @@ 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+ /// Generate a new fallback key iff a unpublished one isn't already inside
560+ /// of vodozemac and if the currently active one expired.
561+ ///
562+ /// The former is checked using [`Account::fallback_key().is_empty()`],
563+ /// which is a hashmap that gets cleared by the
564+ /// [`Account::mark_keys_as_published()`] call.
565+ pub ( crate ) fn generate_fallback_key_if_needed ( & mut self ) {
566+ if self . inner . fallback_key ( ) . is_empty ( ) && self . fallback_key_expired ( ) {
548567 let removed_fallback_key = self . inner . generate_fallback_key ( ) ;
568+ self . fallback_creation_timestamp = Some ( MilliSecondsSinceUnixEpoch :: now ( ) ) ;
549569
550570 debug ! (
551571 ?removed_fallback_key,
552- "No unused fallback keys were found on the server, generated a new fallback key." ,
572+ "The fallback key either expired or we didn't have one: generated a new fallback key." ,
553573 ) ;
554574 }
555575 }
556576
577+ /// Check if our most recent fallback key has expired.
578+ ///
579+ /// We consider the fallback key to be expired if it's older than a week.
580+ /// This is the lower bound for the recommended signed pre-key bundle
581+ /// rotation interval in the X3DH spec[1].
582+ ///
583+ /// [1]: https://signal.org/docs/specifications/x3dh/#publishing-keys
584+ fn fallback_key_expired ( & self ) -> bool {
585+ const FALLBACK_KEY_MAX_AGE : Duration = Duration :: from_secs ( 3600 * 24 * 7 ) ;
586+
587+ if let Some ( time) = self . fallback_creation_timestamp {
588+ // `to_system_time()` returns `None` if the the UNIX_EPOCH + `time` doesn't fit
589+ // into a i64. This will likely never happen, but let's rotate the
590+ // key in case the values are messed up for some other reason.
591+ let Some ( system_time) = time. to_system_time ( ) else {
592+ return true ;
593+ } ;
594+
595+ // `elapsed()` errors if the `system_time` is in the future, this should mean
596+ // that our clock has changed to the past, let's rotate just in case
597+ // and then we'll get to a normal time.
598+ let Ok ( elapsed) = system_time. elapsed ( ) else {
599+ return true ;
600+ } ;
601+
602+ // Alright, our times are normal and we know how much time elapsed since the
603+ // last time we created/rotated a fallback key.
604+ //
605+ // If the key is older than a week, then we rotate it.
606+ elapsed > FALLBACK_KEY_MAX_AGE
607+ } else {
608+ // We never created a fallback key, or we're migrating to the time-based
609+ // fallback key rotation, so let's generate a new fallback key.
610+ true
611+ }
612+ }
613+
557614 fn fallback_key ( & self ) -> HashMap < KeyId , Curve25519PublicKey > {
558615 self . inner . fallback_key ( )
559616 }
@@ -595,6 +652,7 @@ impl Account {
595652 shared : self . shared ( ) ,
596653 uploaded_signed_key_count : self . uploaded_key_count ( ) ,
597654 creation_local_time : self . static_data . creation_local_time ,
655+ fallback_key_creation_timestamp : self . fallback_creation_timestamp ,
598656 }
599657 }
600658
@@ -651,6 +709,7 @@ impl Account {
651709 inner : Box :: new ( account) ,
652710 shared : pickle. shared ,
653711 uploaded_signed_key_count : pickle. uploaded_signed_key_count ,
712+ fallback_creation_timestamp : pickle. fallback_key_creation_timestamp ,
654713 } )
655714 }
656715
@@ -1372,6 +1431,7 @@ mod tests {
13721431 use std:: {
13731432 collections:: { BTreeMap , BTreeSet } ,
13741433 ops:: Deref ,
1434+ time:: Duration ,
13751435 } ;
13761436
13771437 use anyhow:: Result ;
@@ -1443,30 +1503,59 @@ mod tests {
14431503 // We don't create fallback keys since we don't know if the server
14441504 // supports them, we need to receive a sync response to decide if we're
14451505 // going to create them or not.
1446- assert ! ( fallback_keys. is_empty( ) ) ;
1506+ assert ! (
1507+ fallback_keys. is_empty( ) ,
1508+ "We should not upload fallback keys until we know if the server supports them."
1509+ ) ;
14471510
14481511 let one_time_keys = BTreeMap :: from ( [ ( DeviceKeyAlgorithm :: SignedCurve25519 , 50u8 . into ( ) ) ] ) ;
14491512
14501513 // A `None` here means that the server doesn't support fallback keys, no
14511514 // fallback key gets uploaded.
14521515 account. update_key_counts ( & one_time_keys, None ) ;
14531516 let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1454- assert ! ( fallback_keys. is_empty( ) ) ;
1517+ assert ! (
1518+ fallback_keys. is_empty( ) ,
1519+ "We should not upload a fallback key if we're certain that the server doesn't support \
1520+ them."
1521+ ) ;
14551522
14561523 // The empty array means that the server supports fallback keys but
14571524 // there isn't a unused fallback key on the server. This time we upload
14581525 // a fallback key.
14591526 let unused_fallback_keys = & [ ] ;
14601527 account. update_key_counts ( & one_time_keys, Some ( unused_fallback_keys. as_ref ( ) ) ) ;
14611528 let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1462- assert ! ( !fallback_keys. is_empty( ) ) ;
1529+ assert ! (
1530+ !fallback_keys. is_empty( ) ,
1531+ "We should upload the initial fallback key if the server supports them."
1532+ ) ;
14631533 account. mark_keys_as_published ( ) ;
14641534
1465- // There's an unused fallback key on the server, nothing to do here.
1466- let unused_fallback_keys = & [ DeviceKeyAlgorithm :: SignedCurve25519 ] ;
1535+ // There's no unused fallback key on the server, but our initial fallback key
1536+ // did not yet expire.
1537+ let unused_fallback_keys = & [ ] ;
14671538 account. update_key_counts ( & one_time_keys, Some ( unused_fallback_keys. as_ref ( ) ) ) ;
14681539 let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1469- assert ! ( fallback_keys. is_empty( ) ) ;
1540+ assert ! (
1541+ fallback_keys. is_empty( ) ,
1542+ "We should not upload new fallback keys unless our current fallback key expires."
1543+ ) ;
1544+
1545+ let fallback_key_timestamp =
1546+ account. fallback_creation_timestamp . unwrap ( ) . to_system_time ( ) . unwrap ( )
1547+ - Duration :: from_secs ( 3600 * 24 * 30 ) ;
1548+
1549+ account. fallback_creation_timestamp =
1550+ Some ( MilliSecondsSinceUnixEpoch :: from_system_time ( fallback_key_timestamp) . unwrap ( ) ) ;
1551+
1552+ account. update_key_counts ( & one_time_keys, None ) ;
1553+ let ( _, _, fallback_keys) = account. keys_for_upload ( ) ;
1554+ assert ! (
1555+ !fallback_keys. is_empty( ) ,
1556+ "Now that our fallback key has expired, we should try to upload a new one, even if the \
1557+ server supposedly doesn't support fallback keys anymore"
1558+ ) ;
14701559
14711560 Ok ( ( ) )
14721561 }
0 commit comments