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()
        );
    }
}