tor_hscrypto/
time.rs

1//! Manipulate time periods (as used in the onion service system)
2
3use std::{
4    fmt::Display,
5    time::{Duration, SystemTime},
6};
7
8use humantime::format_rfc3339_seconds;
9use tor_units::IntegerMinutes;
10
11use serde::{Deserialize, Serialize};
12
13/// A period of time, as used in the onion service system.
14///
15/// A `TimePeriod` is defined as a duration (in seconds), and the number of such
16/// durations that have elapsed since a given offset from the Unix epoch.  So
17/// for example, the interval "(86400 seconds length, 15 intervals, 12 hours
18/// offset)", covers `1970-01-16T12:00:00` up to but not including
19/// `1970-01-17T12:00:00`.
20///
21/// These time periods are used to derive a different `BlindedOnionIdKey` during
22/// each period from each `OnionIdKey`.
23#[derive(Deserialize, Serialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
24pub struct TimePeriod {
25    /// Index of the time periods that have passed since the unix epoch.
26    pub(crate) interval_num: u64,
27    /// The length of a time period, in **minutes**.
28    ///
29    /// The spec admits only periods which are a whole number of minutes.
30    pub(crate) length: IntegerMinutes<u32>,
31    /// Our offset from the epoch, in seconds.
32    ///
33    /// This is the amount of time after the Unix epoch when our epoch begins,
34    /// rounded down to the nearest second.
35    pub(crate) epoch_offset_in_sec: u32,
36}
37
38/// Two [`TimePeriod`]s are ordered with respect to one another if they have the
39/// same interval length and offset.
40impl PartialOrd for TimePeriod {
41    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
42        if self.length == other.length && self.epoch_offset_in_sec == other.epoch_offset_in_sec {
43            Some(self.interval_num.cmp(&other.interval_num))
44        } else {
45            None
46        }
47    }
48}
49
50impl Display for TimePeriod {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "#{} ", self.interval_num())?;
53        match self.range() {
54            Ok(r) => {
55                let mins = self.length().as_minutes();
56                write!(
57                    f,
58                    "{}..+{}:{:02}",
59                    format_rfc3339_seconds(r.start),
60                    mins / 60,
61                    mins % 60
62                )
63            }
64            Err(_) => write!(f, "overflow! {self:?}"),
65        }
66    }
67}
68
69impl TimePeriod {
70    /// Construct a time period of a given `length` that contains `when`.
71    ///
72    /// The `length` value is rounded down to the nearest second,
73    /// and must then be a whole number of minutes.
74    ///
75    /// The `epoch_offset` value is the amount of time after the Unix epoch when
76    /// our epoch begins.  It is also rounded down to the nearest second.
77    ///
78    /// Return None if the Duration is too large or too small, or if `when`
79    /// cannot be represented as a time period.
80    pub fn new(
81        length: Duration,
82        when: SystemTime,
83        epoch_offset: Duration,
84    ) -> Result<Self, TimePeriodError> {
85        // The algorithm here is specified in rend-spec-v3 section 2.2.1
86        let length_in_sec =
87            u32::try_from(length.as_secs()).map_err(|_| TimePeriodError::IntervalInvalid)?;
88        if length_in_sec % 60 != 0 || length.subsec_nanos() != 0 {
89            return Err(TimePeriodError::IntervalInvalid);
90        }
91        let length_in_minutes = length_in_sec / 60;
92        let length = IntegerMinutes::new(length_in_minutes);
93        let epoch_offset_in_sec =
94            u32::try_from(epoch_offset.as_secs()).map_err(|_| TimePeriodError::OffsetInvalid)?;
95        let interval_num = when
96            .duration_since(SystemTime::UNIX_EPOCH + epoch_offset)
97            .map_err(|_| TimePeriodError::OutOfRange)?
98            .as_secs()
99            / u64::from(length_in_sec);
100        Ok(TimePeriod {
101            interval_num,
102            length,
103            epoch_offset_in_sec,
104        })
105    }
106
107    /// Compute the `TimePeriod`, given its length (in **minutes**), index (the number of time
108    /// periods that have passed since the unix epoch), and offset from the epoch (in seconds).
109    ///
110    /// The `epoch_offset_in_sec` value is the number of seconds after the Unix epoch when our
111    /// epoch begins, rounded down to the nearest second.
112    /// Note that this is *not* the time_t at which this *Time Period* begins.
113    ///
114    /// The returned TP begins at the time_t `interval_num * length * 60 + epoch_offset_in_sec`
115    /// and ends `length * 60` seconds later.
116    pub fn from_parts(length: u32, interval_num: u64, epoch_offset_in_sec: u32) -> Self {
117        let length_in_sec = length * 60;
118
119        Self {
120            interval_num,
121            length: length.into(),
122            epoch_offset_in_sec,
123        }
124    }
125
126    /// Return the time period after this one.
127    ///
128    /// Return None if this is the last representable time period.
129    pub fn next(&self) -> Option<Self> {
130        Some(TimePeriod {
131            interval_num: self.interval_num.checked_add(1)?,
132            ..*self
133        })
134    }
135    /// Return the time period before this one.
136    ///
137    /// Return None if this is the first representable time period.
138    pub fn prev(&self) -> Option<Self> {
139        Some(TimePeriod {
140            interval_num: self.interval_num.checked_sub(1)?,
141            ..*self
142        })
143    }
144    /// Return true if this time period contains `when`.
145    ///
146    /// # Limitations
147    ///
148    /// This function always returns false if the time period contains any times
149    /// that cannot be represented as a `SystemTime`.
150    pub fn contains(&self, when: SystemTime) -> bool {
151        match self.range() {
152            Ok(r) => r.contains(&when),
153            Err(_) => false,
154        }
155    }
156    /// Return a range representing the [`SystemTime`] values contained within
157    /// this time period.
158    ///
159    /// Return None if this time period contains any times that can be
160    /// represented as a `SystemTime`.
161    pub fn range(&self) -> Result<std::ops::Range<SystemTime>, TimePeriodError> {
162        (|| {
163            let length_in_sec = u64::from(self.length.as_minutes()) * 60;
164            let start_sec = length_in_sec.checked_mul(self.interval_num)?;
165            let end_sec = start_sec.checked_add(length_in_sec)?;
166            let epoch_offset = Duration::new(self.epoch_offset_in_sec.into(), 0);
167            let start = (SystemTime::UNIX_EPOCH + epoch_offset)
168                .checked_add(Duration::from_secs(start_sec))?;
169            let end = (SystemTime::UNIX_EPOCH + epoch_offset)
170                .checked_add(Duration::from_secs(end_sec))?;
171            Some(start..end)
172        })()
173        .ok_or(TimePeriodError::OutOfRange)
174    }
175
176    /// Return the numeric index of this time period.
177    ///
178    /// This function should only be used when encoding the time period for
179    /// cryptographic purposes.
180    pub fn interval_num(&self) -> u64 {
181        self.interval_num
182    }
183
184    /// Return the length of this time period as a number of seconds.
185    ///
186    /// This function should only be used when encoding the time period for
187    /// cryptographic purposes.
188    pub fn length(&self) -> IntegerMinutes<u32> {
189        self.length
190    }
191
192    /// Return our offset from the epoch, in seconds.
193    ///
194    /// Note that this is *not* the start of the TP.
195    /// See `TimePeriod::from_parts`.
196    pub fn epoch_offset_in_sec(&self) -> u32 {
197        self.epoch_offset_in_sec
198    }
199}
200
201/// An error that occurs when creating or manipulating a [`TimePeriod`]
202#[derive(Clone, Debug, thiserror::Error)]
203#[non_exhaustive]
204pub enum TimePeriodError {
205    /// We couldn't represent the time period in the way we were trying to
206    /// represent it, since it outside of the range supported by the data type.
207    #[error("Time period out was out of range")]
208    OutOfRange,
209
210    /// The time period couldn't be constructed because its interval was
211    /// invalid.
212    ///
213    /// (We require that intervals are a multiple of 60 seconds, and that they
214    /// can be represented in a `u32`.)
215    #[error("Invalid time period interval")]
216    IntervalInvalid,
217
218    /// The time period couldn't be constructed because its offset was invalid.
219    ///
220    /// (We require that offsets can be represented in a `u32`.)
221    #[error("Invalid time period offset")]
222    OffsetInvalid,
223}
224
225#[cfg(test)]
226mod test {
227    // @@ begin test lint list maintained by maint/add_warning @@
228    #![allow(clippy::bool_assert_comparison)]
229    #![allow(clippy::clone_on_copy)]
230    #![allow(clippy::dbg_macro)]
231    #![allow(clippy::mixed_attributes_style)]
232    #![allow(clippy::print_stderr)]
233    #![allow(clippy::print_stdout)]
234    #![allow(clippy::single_char_pattern)]
235    #![allow(clippy::unwrap_used)]
236    #![allow(clippy::unchecked_duration_subtraction)]
237    #![allow(clippy::useless_vec)]
238    #![allow(clippy::needless_pass_by_value)]
239    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
240
241    use super::*;
242    use humantime::{parse_duration, parse_rfc3339};
243
244    /// Check reconstructing `period` from parts produces an identical `TimePeriod`.
245    fn assert_eq_from_parts(period: TimePeriod) {
246        assert_eq!(
247            period,
248            TimePeriod::from_parts(
249                period.length().as_minutes(),
250                period.interval_num(),
251                period.epoch_offset_in_sec()
252            )
253        );
254    }
255
256    #[test]
257    fn check_testvec() {
258        // Test case from C tor, taken from rend-spec.
259        let offset = Duration::new(12 * 60 * 60, 0);
260        let time = parse_rfc3339("2016-04-13T11:00:00Z").unwrap();
261        let one_day = parse_duration("1day").unwrap();
262        let period = TimePeriod::new(one_day, time, offset).unwrap();
263        assert_eq!(period.interval_num, 16903);
264        assert!(period.contains(time));
265        assert_eq_from_parts(period);
266
267        let time = parse_rfc3339("2016-04-13T11:59:59Z").unwrap();
268        let period = TimePeriod::new(one_day, time, offset).unwrap();
269        assert_eq!(period.interval_num, 16903); // still the same.
270        assert!(period.contains(time));
271        assert_eq_from_parts(period);
272
273        assert_eq!(period.prev().unwrap().interval_num, 16902);
274        assert_eq!(period.next().unwrap().interval_num, 16904);
275
276        let time2 = parse_rfc3339("2016-04-13T12:00:00Z").unwrap();
277        let period2 = TimePeriod::new(one_day, time2, offset).unwrap();
278        assert_eq!(period2.interval_num, 16904);
279        assert!(period < period2);
280        assert!(period2 > period);
281        assert_eq!(period.next().unwrap(), period2);
282        assert_eq!(period2.prev().unwrap(), period);
283        assert!(period2.contains(time2));
284        assert!(!period2.contains(time));
285        assert!(!period.contains(time2));
286
287        assert_eq!(
288            period.range().unwrap(),
289            parse_rfc3339("2016-04-12T12:00:00Z").unwrap()
290                ..parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
291        );
292        assert_eq!(
293            period2.range().unwrap(),
294            parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
295                ..parse_rfc3339("2016-04-14T12:00:00Z").unwrap()
296        );
297        assert_eq_from_parts(period2);
298    }
299}