Skip to main content

qcode/provider/
key.rs

1//! A provider's key, which is written once and never shown again.
2//!
3//! The key is a value of its own rather than a `String` so that nothing can print it by
4//! accident: it has no `Display`, its `Debug` writes only the marker and the last four
5//! characters, and the one method that hands the secret out
6//! ([`expose`](Key::expose)) is visible only inside this crate, where the single caller is the
7//! transport that puts it in a request header. A key never travels in a URL, so nothing that
8//! records or reports an address can record it either.
9
10/// How many characters of a key are ever shown.
11const SHOWN: usize = 4;
12
13/// The shortest key whose last four characters can be shown without showing most of it. A key
14/// of six characters would be two thirds revealed by its last four.
15const SHOWABLE: usize = 12;
16
17/// A provider's key.
18#[derive(Clone, PartialEq, Eq)]
19pub struct Key(String);
20
21impl Key {
22    /// The key the person pasted, with the spaces around it dropped, or `None` when they pasted
23    /// nothing. A pasted key often carries a trailing newline from the clipboard, and a key with
24    /// a newline in it makes a header no server accepts.
25    #[must_use]
26    pub fn new(text: &str) -> Option<Self> {
27        let trimmed = text.trim();
28        // A key that is not one line is not a key: a header holds no control characters, and
29        // something pasted from a file may bring a whole paragraph with it.
30        if trimmed.is_empty() || trimmed.chars().any(char::is_control) {
31            return None;
32        }
33        Some(Self(trimmed.to_owned()))
34    }
35
36    /// The last four characters, which is all of a key that is ever shown, or `None` for a key
37    /// so short that four characters would give most of it away.
38    #[must_use]
39    pub fn last_four(&self) -> Option<String> {
40        let characters: Vec<char> = self.0.chars().collect();
41        (characters.len() >= SHOWABLE).then(|| characters[characters.len() - SHOWN..].iter().collect())
42    }
43
44    /// The secret itself, for the one place that has to send it.
45    pub(crate) fn expose(&self) -> &str {
46        &self.0
47    }
48}
49
50impl std::fmt::Debug for Key {
51    /// Writes what the page writes and nothing more, so a key in a structure that is printed
52    /// while something is being looked into does not end up in a log.
53    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self.last_four() {
55            Some(tail) => write!(formatter, "Key(…{tail})"),
56            None => formatter.write_str("Key(…)"),
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    /// Not a key of anyone's: the characters spell what it is.
66    const MADE_UP: &str = "not-a-real-key-0000-wxyz";
67
68    #[test]
69    fn only_the_last_four_characters_are_ever_shown() {
70        let key = Key::new(MADE_UP).expect("a key");
71        assert_eq!(key.last_four().as_deref(), Some("wxyz"));
72        assert_eq!(format!("{key:?}"), "Key(…wxyz)");
73        assert!(!format!("{key:?}").contains("not-a-real"), "the front of the key is not printed");
74    }
75
76    #[test]
77    fn a_short_key_shows_nothing_at_all_rather_than_most_of_itself() {
78        let key = Key::new("abcdefgh").expect("a key");
79        assert_eq!(key.last_four(), None, "four of eight characters is most of the key");
80        assert_eq!(format!("{key:?}"), "Key(…)");
81        assert!(!format!("{key:?}").contains("efgh"));
82    }
83
84    #[test]
85    fn what_the_clipboard_brings_along_is_dropped_and_a_paragraph_is_refused() {
86        let key = Key::new(&format!("  {MADE_UP}\n")).expect("a key");
87        assert_eq!(key.expose(), MADE_UP, "the key itself is what a header gets");
88        assert_eq!(Key::new(""), None);
89        assert_eq!(Key::new("   \n  "), None);
90        assert_eq!(Key::new("first line\nsecond line"), None, "no header holds two lines");
91        assert_eq!(Key::new("has\ttab"), None);
92    }
93}