prefixed_api_key/
prefixed_api_key.rs

1use digest::{Digest, FixedOutputReset};
2use std::error::Error;
3use std::fmt;
4use std::fmt::Debug;
5
6#[derive(Debug, PartialEq, Eq)]
7pub enum PrefixedApiKeyError {
8    WrongNumberOfParts(usize),
9}
10
11impl Error for PrefixedApiKeyError {}
12
13impl fmt::Display for PrefixedApiKeyError {
14    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
15        // TODO: Display should use something other than debug display
16        write!(f, "{:?}", self)
17    }
18}
19
20/// A struct representing the api token generated for, and provided to,
21/// the user. An instance of this struct can be instantiated from a string
22/// provided by the user for further validation, or it can be instantiated
23/// via the `new` method while generating a new key to be given to the user.
24pub struct PrefixedApiKey {
25    prefix: String,
26    short_token: String,
27    long_token: String,
28}
29
30impl PrefixedApiKey {
31    /// Constructs a new instance of the struct. This is just a wrapper around
32    /// directly instantiating the struct, and makes no assertions or assumptions
33    /// about the values provided.
34    pub fn new(prefix: String, short_token: String, long_token: String) -> PrefixedApiKey {
35        PrefixedApiKey {
36            prefix,
37            short_token,
38            long_token,
39        }
40    }
41
42    /// Getter method for accessing the key's prefix
43    pub fn prefix(&self) -> &str {
44        &self.prefix
45    }
46
47    /// Getter method for accessing the key's short token
48    pub fn short_token(&self) -> &str {
49        &self.short_token
50    }
51
52    /// Getter method for accessing the key's secret long token
53    pub fn long_token(&self) -> &str {
54        &self.long_token
55    }
56
57    /// Gets the hashed form of the keys secret long token, using the hashing
58    /// algorithm provided as `digest`. This resets the digest instance while
59    /// finalizing so it may be reused afterward.
60    pub fn long_token_hashed<D: Digest + FixedOutputReset>(&self, digest: &mut D) -> String {
61        Digest::update(digest, self.long_token.clone());
62        hex::encode(digest.finalize_reset())
63    }
64
65    /// Instantiates the struct from the string form of the api token. This
66    /// validates that the string has the expected number of parts (deliniated by `"_"`),
67    /// but otherwise makes no assertions or assumptions about the values.
68    pub fn from_string(pak_string: &str) -> Result<PrefixedApiKey, PrefixedApiKeyError> {
69        let parts: Vec<&str> = pak_string.split('_').collect();
70
71        if parts.len() != 3 {
72            // Incorrect number of parts
73            return Err(PrefixedApiKeyError::WrongNumberOfParts(parts.len()));
74        }
75
76        Ok(PrefixedApiKey::new(
77            parts[0].to_owned(),
78            parts[1].to_owned(),
79            parts[2].to_owned(),
80        ))
81    }
82}
83
84/// A custom implementation of Debug that masks the secret long token that way
85/// the struct can be debug printed without leaking sensitive info into logs
86impl Debug for PrefixedApiKey {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.debug_struct("PrefixedApiKey")
89            .field("prefix", &self.prefix)
90            .field("short_token", &self.short_token)
91            .field("long_token", &"***")
92            .finish()
93    }
94}
95
96/// A manual implementation of `ToString` which does not mask the secret long token.
97/// The `Display` trait is explicitely not implemented to avoid accidentally leaking
98/// the long token in logs.
99#[allow(clippy::to_string_trait_impl)]
100impl ToString for PrefixedApiKey {
101    fn to_string(&self) -> String {
102        format!("{}_{}_{}", self.prefix, self.short_token, self.long_token)
103    }
104}
105
106impl TryInto<PrefixedApiKey> for &str {
107    type Error = PrefixedApiKeyError;
108
109    fn try_into(self) -> Result<PrefixedApiKey, Self::Error> {
110        PrefixedApiKey::from_string(self)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use sha2::{Digest, Sha256};
117
118    use crate::prefixed_api_key::{PrefixedApiKey, PrefixedApiKeyError};
119
120    #[test]
121    fn to_string_is_expected() {
122        let prefix = "mycompany".to_owned();
123        let short = "abcdefg".to_owned();
124        let long = "bacdegadsa".to_owned();
125        let expected_token = format!("{}_{}_{}", prefix, short, long);
126        let pak = PrefixedApiKey::new(prefix, short, long);
127        assert_eq!(pak.to_string(), expected_token)
128    }
129
130    #[test]
131    fn self_from_string_works() {
132        let pak_string = "mycompany_abcdefg_bacdegadsa";
133        let pak_result = PrefixedApiKey::from_string(pak_string);
134        assert_eq!(pak_result.is_ok(), true);
135        assert_eq!(pak_result.unwrap().to_string(), pak_string);
136    }
137
138    #[test]
139    fn str_into_pak() {
140        let pak_string = "mycompany_abcdefg_bacdegadsa";
141        let pak_result: Result<PrefixedApiKey, _> = pak_string.try_into();
142        assert_eq!(pak_result.is_ok(), true);
143        assert_eq!(pak_result.unwrap().to_string(), pak_string);
144    }
145
146    #[test]
147    fn string_into_pak_via_as_ref() {
148        let pak_string = "mycompany_abcdefg_bacdegadsa".to_owned();
149        let pak_result: Result<PrefixedApiKey, _> = pak_string.as_str().try_into();
150        assert_eq!(pak_result.is_ok(), true);
151        assert_eq!(pak_result.unwrap().to_string(), pak_string);
152    }
153
154    #[test]
155    fn str_into_pak_with_extra_parts() {
156        let pak_string = "mycompany_abcd_efg_bacdegadsa";
157        let pak_result: Result<PrefixedApiKey, _> = pak_string.try_into();
158        assert_eq!(pak_result.is_err(), true);
159        assert_eq!(
160            pak_result.unwrap_err(),
161            PrefixedApiKeyError::WrongNumberOfParts(4)
162        );
163    }
164
165    #[test]
166    fn check_long_token() {
167        let pak_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
168        let hash = "0f01ab6e0833f280b73b2b618c16102d91c0b7c585d42a080d6e6603239a8bee";
169
170        let pak: PrefixedApiKey = pak_string.try_into().unwrap();
171        let mut digest = Sha256::new();
172        assert_eq!(pak.long_token_hashed(&mut digest), hash);
173    }
174
175    #[test]
176    fn check_debug_display_hides_secret_token() {
177        let pak_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
178
179        let pak: PrefixedApiKey = pak_string.try_into().unwrap();
180        let debug_string = format!("{:?}", pak);
181        assert_eq!(debug_string, "PrefixedApiKey { prefix: \"mycompany\", short_token: \"CEUsS4psCmc\", long_token: \"***\" }");
182    }
183}