torrust_tracker/core/
auth.rs

1//! Tracker authentication services and structs.
2//!
3//! This module contains functions to handle tracker keys.
4//! Tracker keys are tokens used to authenticate the tracker clients when the tracker runs
5//! in `private` or `private_listed` modes.
6//!
7//! There are services to [`generate_key`]  and [`verify_key_expiration`]  authentication keys.
8//!
9//! Authentication keys are used only by [`HTTP`](crate::servers::http) trackers. All keys have an expiration time, that means
10//! they are only valid during a period of time. After that time the expiring key will no longer be valid.
11//!
12//! Keys are stored in this struct:
13//!
14//! ```rust,no_run
15//! use torrust_tracker::core::auth::Key;
16//! use torrust_tracker_primitives::DurationSinceUnixEpoch;
17//!
18//! pub struct ExpiringKey {
19//!     /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ`
20//!     pub key: Key,
21//!     /// Timestamp, the key will be no longer valid after this timestamp
22//!     pub valid_until: Option<DurationSinceUnixEpoch>,
23//! }
24//! ```
25//!
26//! You can generate a new key valid for `9999` seconds and `0` nanoseconds from the current time with the following:
27//!
28//! ```rust,no_run
29//! use torrust_tracker::core::auth;
30//! use std::time::Duration;
31//!
32//! let expiring_key = auth::generate_key(Some(Duration::new(9999, 0)));
33//!
34//! // And you can later verify it with:
35//!
36//! assert!(auth::verify_key_expiration(&expiring_key).is_ok());
37//! ```
38
39use std::panic::Location;
40use std::str::FromStr;
41use std::sync::Arc;
42use std::time::Duration;
43
44use derive_more::Display;
45use rand::distributions::Alphanumeric;
46use rand::{thread_rng, Rng};
47use serde::{Deserialize, Serialize};
48use thiserror::Error;
49use torrust_tracker_clock::clock::Time;
50use torrust_tracker_clock::conv::convert_from_timestamp_to_datetime_utc;
51use torrust_tracker_located_error::{DynError, LocatedError};
52use torrust_tracker_primitives::DurationSinceUnixEpoch;
53
54use crate::shared::bit_torrent::common::AUTH_KEY_LENGTH;
55use crate::CurrentClock;
56
57/// It generates a new permanent random key [`PeerKey`].
58#[must_use]
59pub fn generate_permanent_key() -> PeerKey {
60    generate_key(None)
61}
62
63/// It generates a new random 32-char authentication [`PeerKey`].
64///
65/// It can be an expiring or permanent key.
66///
67/// # Panics
68///
69/// It would panic if the `lifetime: Duration` + Duration is more than `Duration::MAX`.
70///
71/// # Arguments
72///
73/// * `lifetime`: if `None` the key will be permanent.
74#[must_use]
75pub fn generate_key(lifetime: Option<Duration>) -> PeerKey {
76    let random_id: String = thread_rng()
77        .sample_iter(&Alphanumeric)
78        .take(AUTH_KEY_LENGTH)
79        .map(char::from)
80        .collect();
81
82    if let Some(lifetime) = lifetime {
83        tracing::debug!("Generated key: {}, valid for: {:?} seconds", random_id, lifetime);
84
85        PeerKey {
86            key: random_id.parse::<Key>().unwrap(),
87            valid_until: Some(CurrentClock::now_add(&lifetime).unwrap()),
88        }
89    } else {
90        tracing::debug!("Generated key: {}, permanent", random_id);
91
92        PeerKey {
93            key: random_id.parse::<Key>().unwrap(),
94            valid_until: None,
95        }
96    }
97}
98
99/// It verifies an [`PeerKey`]. It checks if the expiration date has passed.
100/// Permanent keys without duration (`None`) do not expire.
101///
102/// # Errors
103///
104/// Will return:
105///
106/// - `Error::KeyExpired` if `auth_key.valid_until` is past the `current_time`.
107/// - `Error::KeyInvalid` if `auth_key.valid_until` is past the `None`.
108pub fn verify_key_expiration(auth_key: &PeerKey) -> Result<(), Error> {
109    let current_time: DurationSinceUnixEpoch = CurrentClock::now();
110
111    match auth_key.valid_until {
112        Some(valid_until) => {
113            if valid_until < current_time {
114                Err(Error::KeyExpired {
115                    location: Location::caller(),
116                })
117            } else {
118                Ok(())
119            }
120        }
121        None => Ok(()), // Permanent key
122    }
123}
124
125/// An authentication key which can potentially have an expiration time.
126/// After that time is will automatically become invalid.
127#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
128pub struct PeerKey {
129    /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ`
130    pub key: Key,
131
132    /// Timestamp, the key will be no longer valid after this timestamp.
133    /// If `None` the keys will not expire (permanent key).
134    pub valid_until: Option<DurationSinceUnixEpoch>,
135}
136
137impl std::fmt::Display for PeerKey {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        match self.expiry_time() {
140            Some(expire_time) => write!(f, "key: `{}`, valid until `{}`", self.key, expire_time),
141            None => write!(f, "key: `{}`, permanent", self.key),
142        }
143    }
144}
145
146impl PeerKey {
147    #[must_use]
148    pub fn key(&self) -> Key {
149        self.key.clone()
150    }
151
152    /// It returns the expiry time. For example, for the starting time for Unix Epoch
153    /// (timestamp 0) it will return a `DateTime` whose string representation is
154    /// `1970-01-01 00:00:00 UTC`.
155    ///
156    /// # Panics
157    ///
158    /// Will panic when the key timestamp overflows the internal i64 type.
159    /// (this will naturally happen in 292.5 billion years)
160    #[must_use]
161    pub fn expiry_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
162        self.valid_until.map(convert_from_timestamp_to_datetime_utc)
163    }
164}
165
166/// A token used for authentication.
167///
168/// - It contains only ascii alphanumeric chars: lower and uppercase letters and
169///   numbers.
170/// - It's a 32-char string.
171#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Display, Hash)]
172pub struct Key(String);
173
174impl Key {
175    /// # Errors
176    ///
177    /// Will return an error is the string represents an invalid key.
178    /// Valid keys can only contain 32 chars including 0-9, a-z and A-Z.
179    pub fn new(value: &str) -> Result<Self, ParseKeyError> {
180        if value.len() != AUTH_KEY_LENGTH {
181            return Err(ParseKeyError::InvalidKeyLength);
182        }
183
184        if !value.chars().all(|c| c.is_ascii_alphanumeric()) {
185            return Err(ParseKeyError::InvalidChars);
186        }
187
188        Ok(Self(value.to_owned()))
189    }
190
191    #[must_use]
192    pub fn value(&self) -> &str {
193        &self.0
194    }
195}
196
197/// Error returned when a key cannot be parsed from a string.
198///
199/// ```rust,no_run
200/// use torrust_tracker::core::auth::Key;
201/// use std::str::FromStr;
202///
203/// let key_string = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ";
204/// let key = Key::from_str(key_string);
205///
206/// assert!(key.is_ok());
207/// assert_eq!(key.unwrap().to_string(), key_string);
208/// ```
209///
210/// If the string does not contains a valid key, the parser function will return
211/// this error.
212#[derive(Debug, Error)]
213pub enum ParseKeyError {
214    #[error("Invalid key length. Key must be have 32 chars")]
215    InvalidKeyLength,
216    #[error("Invalid chars for key. Key can only alphanumeric chars (0-9, a-z, A-Z)")]
217    InvalidChars,
218}
219
220impl FromStr for Key {
221    type Err = ParseKeyError;
222
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        Key::new(s)?;
225        Ok(Self(s.to_string()))
226    }
227}
228
229/// Verification error. Error returned when an [`PeerKey`] cannot be
230/// verified with the (`crate::core::auth::verify_key`) function.
231#[derive(Debug, Error)]
232#[allow(dead_code)]
233pub enum Error {
234    #[error("Key could not be verified: {source}")]
235    KeyVerificationError {
236        source: LocatedError<'static, dyn std::error::Error + Send + Sync>,
237    },
238    #[error("Failed to read key: {key}, {location}")]
239    UnableToReadKey {
240        location: &'static Location<'static>,
241        key: Box<Key>,
242    },
243    #[error("Key has expired, {location}")]
244    KeyExpired { location: &'static Location<'static> },
245}
246
247impl From<r2d2_sqlite::rusqlite::Error> for Error {
248    fn from(e: r2d2_sqlite::rusqlite::Error) -> Self {
249        Error::KeyVerificationError {
250            source: (Arc::new(e) as DynError).into(),
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257
258    mod key {
259        use std::str::FromStr;
260
261        use crate::core::auth::Key;
262
263        #[test]
264        fn should_be_parsed_from_an_string() {
265            let key_string = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ";
266            let key = Key::from_str(key_string);
267
268            assert!(key.is_ok());
269            assert_eq!(key.unwrap().to_string(), key_string);
270        }
271
272        #[test]
273        fn length_should_be_32() {
274            let key = Key::new("");
275            assert!(key.is_err());
276
277            let string_longer_than_32 = "012345678901234567890123456789012"; // DevSkim: ignore  DS173237
278            let key = Key::new(string_longer_than_32);
279            assert!(key.is_err());
280        }
281
282        #[test]
283        fn should_only_include_alphanumeric_chars() {
284            let key = Key::new("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
285            assert!(key.is_err());
286        }
287    }
288
289    mod expiring_auth_key {
290        use std::str::FromStr;
291        use std::time::Duration;
292
293        use torrust_tracker_clock::clock;
294        use torrust_tracker_clock::clock::stopped::Stopped as _;
295
296        use crate::core::auth;
297
298        #[test]
299        fn should_be_parsed_from_an_string() {
300            let key_string = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ";
301            let auth_key = auth::Key::from_str(key_string);
302
303            assert!(auth_key.is_ok());
304            assert_eq!(auth_key.unwrap().to_string(), key_string);
305        }
306
307        #[test]
308        fn should_be_displayed() {
309            // Set the time to the current time.
310            clock::Stopped::local_set_to_unix_epoch();
311
312            let expiring_key = auth::generate_key(Some(Duration::from_secs(0)));
313
314            assert_eq!(
315                expiring_key.to_string(),
316                format!("key: `{}`, valid until `1970-01-01 00:00:00 UTC`", expiring_key.key) // cspell:disable-line
317            );
318        }
319
320        #[test]
321        fn should_be_generated_with_a_expiration_time() {
322            let expiring_key = auth::generate_key(Some(Duration::new(9999, 0)));
323
324            assert!(auth::verify_key_expiration(&expiring_key).is_ok());
325        }
326
327        #[test]
328        fn should_be_generate_and_verified() {
329            // Set the time to the current time.
330            clock::Stopped::local_set_to_system_time_now();
331
332            // Make key that is valid for 19 seconds.
333            let expiring_key = auth::generate_key(Some(Duration::from_secs(19)));
334
335            // Mock the time has passed 10 sec.
336            clock::Stopped::local_add(&Duration::from_secs(10)).unwrap();
337
338            assert!(auth::verify_key_expiration(&expiring_key).is_ok());
339
340            // Mock the time has passed another 10 sec.
341            clock::Stopped::local_add(&Duration::from_secs(10)).unwrap();
342
343            assert!(auth::verify_key_expiration(&expiring_key).is_err());
344        }
345    }
346}