prefixed_api_key/
controller.rs

1use std::marker::PhantomData;
2
3use constant_time_eq::constant_time_eq;
4use digest::{Digest, FixedOutputReset};
5use rand::RngCore;
6
7use crate::controller_builder::ControllerBuilder;
8use crate::prefixed_api_key::PrefixedApiKey;
9
10#[derive(Clone, Debug)]
11pub struct PrefixedApiKeyController<R: RngCore + Clone, D: Digest + FixedOutputReset> {
12    prefix: String,
13    rng: R,
14    digest: PhantomData<D>,
15    short_token_prefix: Option<String>,
16    short_token_length: usize,
17    long_token_length: usize,
18}
19
20impl<R: RngCore + Clone, D: Digest + FixedOutputReset> PrefixedApiKeyController<R, D> {
21    pub fn new(
22        prefix: String,
23        rng: R,
24        short_token_prefix: Option<String>,
25        short_token_length: usize,
26        long_token_length: usize,
27    ) -> PrefixedApiKeyController<R, D> {
28        PrefixedApiKeyController {
29            prefix,
30            rng,
31            digest: PhantomData,
32            short_token_prefix,
33            short_token_length,
34            long_token_length,
35        }
36    }
37
38    /// Creates an instance of [ControllerBuilder] to enable building the
39    /// controller via the builder pattern
40    pub fn configure() -> ControllerBuilder<R, D> {
41        ControllerBuilder::new()
42    }
43
44    /// Generates random bytes using the configured random number generator
45    ///
46    /// Can potentially panic depending on the rng source's implementation of [fill_bytes](rand::RngCore::fill_bytes).
47    fn get_random_bytes(&self, length: usize) -> Vec<u8> {
48        let mut random_bytes = vec![0u8; length];
49        let mut rng = self.rng.clone();
50        rng.fill_bytes(&mut random_bytes);
51        random_bytes
52    }
53
54    /// Tries to generate random bytes using the configured random number generator
55    fn try_get_random_bytes(&self, length: usize) -> Result<Vec<u8>, crate::rand::Error> {
56        let mut random_bytes = vec![0u8; length];
57        let mut rng = self.rng.clone();
58        match rng.try_fill_bytes(&mut random_bytes) {
59            Ok(_) => Ok(random_bytes),
60            Err(err) => Err(err),
61        }
62    }
63
64    /// Generates a random token for part of the api key. This can be used for generating
65    /// both the secret long key, and the shorter plaintext key. The random values are
66    /// base58 encoded, which is a key feature/requirement of the library.
67    ///
68    /// Can potentially panic depending on the rng source's implementation of [fill_bytes](rand::RngCore::fill_bytes).
69    fn get_random_token(&self, length: usize) -> String {
70        let bytes = self.get_random_bytes(length);
71        bs58::encode(bytes).into_string()
72    }
73
74    /// Tries to generate a random token for part of the api key. This can be used for
75    /// generating both the secret long key, and the shorter plaintext key. The random values
76    /// are base58 encoded, which is a key feature/requirement of the library.
77    fn try_get_random_token(&self, length: usize) -> Result<String, crate::rand::Error> {
78        match self.try_get_random_bytes(length) {
79            Ok(bytes) => Ok(bs58::encode(bytes).into_string()),
80            Err(err) => Err(err),
81        }
82    }
83
84    /// Generates a new PrefiexedApiKey using the configured string prefix, short token
85    /// prefix (if configured), and random number generator. A hash of the new keys' long
86    /// token is not calculated, so you'll still need to create the hash after calling
87    /// this function.
88    ///
89    /// Can potentially panic depending on the rng source's implementation of [fill_bytes](rand::RngCore::fill_bytes).
90    pub fn generate_key(&self) -> PrefixedApiKey {
91        // generate the short token
92        let mut short_token = self.get_random_token(self.short_token_length);
93
94        // If the short token prefix is configured, concat it and the generated string and
95        // drop any characters beyond the configured short token length
96        if self.short_token_prefix.is_some() {
97            let prefix_string = self.short_token_prefix.as_ref().unwrap().to_owned();
98            short_token = (prefix_string + &short_token)
99                .chars()
100                .take(self.short_token_length)
101                .collect()
102        }
103
104        // Generate the secret long token
105        let long_token = self.get_random_token(self.long_token_length);
106
107        // Construct and return the new pak
108        PrefixedApiKey::new(self.prefix.to_owned(), short_token, long_token)
109    }
110
111    /// Tries to generate a new PrefiexedApiKey using the configured string prefix, short
112    /// token prefix (if configured), and random number generator. A hash of the new keys'
113    /// long token is not calculated, so you'll still need to create the hash after calling
114    /// this function.
115    pub fn try_generate_key(&self) -> Result<PrefixedApiKey, crate::rand::Error> {
116        // generate the short token
117        let mut short_token = self.try_get_random_token(self.short_token_length)?;
118
119        // If the short token prefix is configured, concat it and the generated string and
120        // drop any characters beyond the configured short token length
121        if self.short_token_prefix.is_some() {
122            let prefix_string = self.short_token_prefix.as_ref().unwrap().to_owned();
123            short_token = (prefix_string + &short_token)
124                .chars()
125                .take(self.short_token_length)
126                .collect()
127        }
128
129        // Generate the secret long token
130        let long_token = self.try_get_random_token(self.long_token_length)?;
131
132        // Construct and return the new pak
133        let pak = PrefixedApiKey::new(self.prefix.to_owned(), short_token, long_token);
134        Ok(pak)
135    }
136
137    /// Generates a new key using the [generate_key](PrefixedApiKeyController::generate_key) function, but also calculates and
138    /// returns the hash of the long token.
139    ///
140    /// Can potentially panic depending on the rng source's implementation of [fill_bytes](rand::RngCore::fill_bytes).
141    pub fn generate_key_and_hash(&self) -> (PrefixedApiKey, String) {
142        let pak = self.generate_key();
143        let hash = self.long_token_hashed(&pak);
144        (pak, hash)
145    }
146
147    /// Generates a new key using the [try_generate_key](PrefixedApiKeyController::try_generate_key) function, but also calculates and
148    /// returns the hash of the long token.
149    pub fn try_generate_key_and_hash(
150        &self,
151    ) -> Result<(PrefixedApiKey, String), crate::rand::Error> {
152        match self.try_generate_key() {
153            Ok(pak) => {
154                let hash = self.long_token_hashed(&pak);
155                Ok((pak, hash))
156            }
157            Err(err) => Err(err),
158        }
159    }
160
161    /// Hashes the long token of the provided PrefixedApiKey using the hashing
162    /// algorithm configured on the controller. The hashing instance gets
163    /// reused each time this is called, which is why the [FixedOutputReset](digest::FixedOutputReset)
164    /// trait is required.
165    pub fn long_token_hashed(&self, pak: &PrefixedApiKey) -> String {
166        let mut digest = D::new();
167        pak.long_token_hashed(&mut digest)
168    }
169
170    /// Secure helper for checking if a given PrefixedApiKey matches a given
171    /// long token hash. This uses the hashing algorithm configured on the controller
172    /// and uses the [constant_time_eq](constant_time_eq::constant_time_eq()) method of comparing hashes
173    /// to avoid possible timing attacks.
174    pub fn check_hash(&self, pak: &PrefixedApiKey, hash: &str) -> bool {
175        let pak_hash = self.long_token_hashed(pak);
176        constant_time_eq(pak_hash.as_bytes(), hash.as_bytes())
177    }
178}
179
180#[cfg(test)]
181mod controller_tests {
182    use rand::rngs::OsRng;
183    use sha2::Sha256;
184
185    use crate::controller::PrefixedApiKeyController;
186    use crate::PrefixedApiKey;
187
188    #[test]
189    fn configuration_works() {
190        let controller = PrefixedApiKeyController::<_, Sha256>::configure()
191            .default_lengths()
192            .prefix("mycompany".to_owned())
193            .rng(OsRng)
194            .finalize();
195        assert!(controller.is_ok())
196    }
197
198    #[test]
199    fn generator() {
200        let generator =
201            PrefixedApiKeyController::<_, Sha256>::new("mycompany".to_owned(), OsRng, None, 8, 24);
202        let token_string = generator.generate_key().to_string();
203        let pak_result = PrefixedApiKey::from_string(&token_string);
204        assert_eq!(pak_result.is_ok(), true);
205        let pak_string = pak_result.unwrap().to_string();
206        assert_eq!(token_string, pak_string);
207    }
208
209    #[test]
210    fn try_generator() {
211        let generator =
212            PrefixedApiKeyController::<_, Sha256>::new("mycompany".to_owned(), OsRng, None, 8, 24);
213        let token_res = generator.try_generate_key();
214        assert!(token_res.is_ok());
215        let token_string = token_res.unwrap().to_string();
216        let pak_result = PrefixedApiKey::from_string(&token_string);
217        assert_eq!(pak_result.is_ok(), true);
218        let pak_string = pak_result.unwrap().to_string();
219        assert_eq!(token_string, pak_string);
220    }
221
222    #[test]
223    fn generator_short_token_prefix() {
224        let short_length = 8;
225        let short_prefix = "a".repeat(short_length);
226        let generator = PrefixedApiKeyController::<_, Sha256>::new(
227            "mycompany".to_owned(),
228            OsRng,
229            Some(short_prefix.clone()),
230            short_length,
231            24,
232        );
233        let pak_short_token = generator.generate_key().short_token().to_owned();
234        assert_eq!(pak_short_token, short_prefix);
235    }
236
237    #[test]
238    fn generate_key_and_hash() {
239        let generator =
240            PrefixedApiKeyController::<_, Sha256>::new("mycompany".to_owned(), OsRng, None, 8, 24);
241        let (pak, hash) = generator.generate_key_and_hash();
242        assert!(generator.check_hash(&pak, &hash))
243    }
244
245    #[test]
246    fn check_long_token_via_generator() {
247        let pak_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
248        let hash = "0f01ab6e0833f280b73b2b618c16102d91c0b7c585d42a080d6e6603239a8bee";
249
250        let pak: PrefixedApiKey = pak_string.try_into().unwrap();
251
252        let generator =
253            PrefixedApiKeyController::<_, Sha256>::new("mycompany".to_owned(), OsRng, None, 8, 24);
254
255        assert_eq!(generator.long_token_hashed(&pak), hash);
256    }
257
258    #[test]
259    fn generator_digest_resets_after_hashing() {
260        let pak1_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
261        let pak1_hash = "0f01ab6e0833f280b73b2b618c16102d91c0b7c585d42a080d6e6603239a8bee";
262        let pak1: PrefixedApiKey = pak1_string.try_into().unwrap();
263
264        let pak2_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
265        let pak2_hash = "0f01ab6e0833f280b73b2b618c16102d91c0b7c585d42a080d6e6603239a8bee";
266        let pak2: PrefixedApiKey = pak2_string.try_into().unwrap();
267
268        let generator =
269            PrefixedApiKeyController::<_, Sha256>::new("mycompany".to_owned(), OsRng, None, 8, 24);
270
271        assert_eq!(generator.long_token_hashed(&pak1), pak1_hash);
272        assert_eq!(generator.long_token_hashed(&pak2), pak2_hash);
273    }
274
275    #[test]
276    fn generator_matches_hash() {
277        let pak_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
278        let pak_hash = "0f01ab6e0833f280b73b2b618c16102d91c0b7c585d42a080d6e6603239a8bee";
279        let pak: PrefixedApiKey = pak_string.try_into().unwrap();
280
281        let generator =
282            PrefixedApiKeyController::<_, Sha256>::new("mycompany".to_owned(), OsRng, None, 8, 24);
283
284        assert!(generator.check_hash(&pak, pak_hash));
285    }
286}