1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
//! Manipulate time periods (as used in the onion service system)
use std::time::{Duration, SystemTime};
/// A period of time, as used in the onion service system.
///
/// A `TimePeriod` is defined as a duration (in seconds), and the number of such
/// durations that have elapsed since a given offset from the Unix epoch. So
/// for example, the interval "(86400 seconds length, 15 intervals, 12 hours
/// offset)", covers `1970-01-16T12:00:00` up to but not including
/// `1970-01-17T12:00:00`.
///
/// These time periods are used to derive a different `BlindedOnionIdKey` during
/// each period from each `OnionIdKey`.
///
/// # Compatibility Note
///
/// Although `rend-spec-v3.txt` says that the offset is a constant "12 hours", C
/// Tor doesn't behave that way. Instead, the offset is set to twelve voting
/// intervals. Since this module doesn't (and shouldn't!) have access to the
/// voting interval, we store the offset as part of the TimePeriod.
///
/// TODO hs: remove or revise this note once the spec is updated; see prop342.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct TimePeriod {
/// Index of the time periods that have passed since the unix epoch.
pub(crate) interval_num: u64,
/// The length of a time period, in seconds.
pub(crate) length_in_sec: u32,
/// Our offset from the epoch, in seconds.
pub(crate) offset_in_sec: u32,
}
/// Two [`TimePeriod`]s are ordered with respect to one another if they have the
/// same interval length and offset.
impl PartialOrd for TimePeriod {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.length_in_sec == other.length_in_sec && self.offset_in_sec == other.offset_in_sec {
Some(self.interval_num.cmp(&other.interval_num))
} else {
None
}
}
}
impl TimePeriod {
/// Construct a time period of a given `length` that contains `when`.
///
/// The `length` value is rounded down to the nearest second.
///
/// The `epoch_offset` value is the amount of time after the Unix epoch when
/// our epoch begins. It is also rounded down to the nearest second.
///
/// Return None if the Duration is too large or too small, or if `when`
/// cannot be represented as a time period.
//
// TODO hs: Make this, and other functions in this module, return a Result
// instead of an Option. (I'll do that after we merge the pending code in
// !987, since otherwise the change would break that code. -nickm)
//
// TODO hs: perhaps we should take an IntegerSeconds or such rathe than a
// duration, since these values are restricted. Or perhaps we should give an
// error if the Duration doesn't divide evenly by seconds as
// appropriate.
//
// TODO hs: conceivably this should take a voting interval instead of an
// epoch offset.
pub fn new(length: Duration, when: SystemTime, epoch_offset: Duration) -> Option<Self> {
// The algorithm here is specified in rend-spec-v3 section 2.2.1
let length_in_sec = u32::try_from(length.as_secs()).ok()?;
let offset_in_sec = u32::try_from(epoch_offset.as_secs()).ok()?;
let interval_num = when
.duration_since(SystemTime::UNIX_EPOCH + epoch_offset)
.ok()?
.as_secs()
/ u64::from(length_in_sec);
Some(TimePeriod {
interval_num,
length_in_sec,
offset_in_sec,
})
}
/// Return the time period after this one.
///
/// Return None if this is the last representable time period.
pub fn next(&self) -> Option<Self> {
Some(TimePeriod {
interval_num: self.interval_num.checked_add(1)?,
..*self
})
}
/// Return the time period after this one.
///
/// Return None if this is the first representable time period.
pub fn prev(&self) -> Option<Self> {
Some(TimePeriod {
interval_num: self.interval_num.checked_sub(1)?,
..*self
})
}
/// Return true if this time period contains `when`.
///
/// # Limitations
///
/// This function always returns false if the time period contains any times
/// that cannot be represented as a `SystemTime`.
pub fn contains(&self, when: SystemTime) -> bool {
match self.range() {
Some(r) => r.contains(&when),
None => false,
}
}
/// Return a range representing the [`SystemTime`] values contained within
/// this time period.
///
/// Return None if this time period contains any times that can be
/// represented as a `SystemTime`.
pub fn range(&self) -> Option<std::ops::Range<SystemTime>> {
let start_sec = u64::from(self.length_in_sec).checked_mul(self.interval_num)?;
let end_sec = start_sec.checked_add(self.length_in_sec.into())?;
let epoch_offset = Duration::new(self.offset_in_sec.into(), 0);
let start =
(SystemTime::UNIX_EPOCH + epoch_offset).checked_add(Duration::from_secs(start_sec))?;
let end =
(SystemTime::UNIX_EPOCH + epoch_offset).checked_add(Duration::from_secs(end_sec))?;
Some(start..end)
}
/// Return the numeric index of this time period.
///
/// This function should only be used when encoding the time period for
/// cryptographic purposes.
pub fn interval_num(&self) -> u64 {
self.interval_num
}
/// Return the length of this time period as a number of seconds.
///
/// This function should only be used when encoding the time period for
/// cryptographic purposes.
pub fn length_in_sec(&self) -> u64 {
self.length_in_sec.into()
}
}
#[cfg(test)]
mod test {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
use humantime::{parse_duration, parse_rfc3339};
#[test]
fn check_testvec() {
// Test case from C tor, taken from rend-spec.
let offset = Duration::new(12 * 60 * 60, 0);
let time = parse_rfc3339("2016-04-13T11:00:00Z").unwrap();
let one_day = parse_duration("1day").unwrap();
let period = TimePeriod::new(one_day, time, offset).unwrap();
assert_eq!(period.interval_num, 16903);
assert!(period.contains(time));
let time = parse_rfc3339("2016-04-13T11:59:59Z").unwrap();
let period = TimePeriod::new(one_day, time, offset).unwrap();
assert_eq!(period.interval_num, 16903); // still the same.
assert!(period.contains(time));
assert_eq!(period.prev().unwrap().interval_num, 16902);
assert_eq!(period.next().unwrap().interval_num, 16904);
let time2 = parse_rfc3339("2016-04-13T12:00:00Z").unwrap();
let period2 = TimePeriod::new(one_day, time2, offset).unwrap();
assert_eq!(period2.interval_num, 16904);
assert!(period < period2);
assert!(period2 > period);
assert_eq!(period.next().unwrap(), period2);
assert_eq!(period2.prev().unwrap(), period);
assert!(period2.contains(time2));
assert!(!period2.contains(time));
assert!(!period.contains(time2));
assert_eq!(
period.range().unwrap(),
parse_rfc3339("2016-04-12T12:00:00Z").unwrap()
..parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
);
assert_eq!(
period2.range().unwrap(),
parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
..parse_rfc3339("2016-04-14T12:00:00Z").unwrap()
);
}
}