prefixed_api_key/
controller_builder.rs

1use digest::{Digest, FixedOutputReset};
2use rand::{
3    rngs::{OsRng, StdRng, ThreadRng},
4    RngCore, SeedableRng,
5};
6use std::fmt;
7use std::{error::Error, marker::PhantomData};
8
9#[cfg(feature = "sha2")]
10use sha2::{Sha224, Sha256, Sha384, Sha512, Sha512_224, Sha512_256};
11
12use crate::controller::PrefixedApiKeyController;
13
14#[derive(Debug, Clone)]
15pub enum BuilderError {
16    MissingPrefix,
17    MissingRng,
18    MissingDigest,
19    MissingShortTokenLength,
20    MissingLongTokenLength,
21}
22
23impl fmt::Display for BuilderError {
24    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25        match self {
26            BuilderError::MissingPrefix => write!(f, "expected prefix to be set, but wasn't"),
27            BuilderError::MissingRng => write!(f, "expected rng to be set, but wasn't"),
28            BuilderError::MissingDigest => write!(f, "expected digest to be set, but wasn't"),
29            BuilderError::MissingShortTokenLength => {
30                write!(f, "expected short_token_length to be set, but wasn't")
31            }
32            BuilderError::MissingLongTokenLength => {
33                write!(f, "expected long_token_length to be set, but wasn't")
34            }
35        }
36    }
37}
38
39impl Error for BuilderError {}
40
41pub struct ControllerBuilder<R: RngCore + Clone, D: Digest + FixedOutputReset> {
42    prefix: Option<String>,
43    rng: Option<R>,
44    digest: PhantomData<D>,
45    short_token_prefix: Option<String>,
46    short_token_length: Option<usize>,
47    long_token_length: Option<usize>,
48}
49
50impl<R: RngCore + Clone, D: Digest + FixedOutputReset> ControllerBuilder<R, D> {
51    pub fn new() -> ControllerBuilder<R, D> {
52        ControllerBuilder {
53            prefix: None,
54            rng: None,
55            digest: PhantomData,
56            short_token_prefix: None,
57            short_token_length: None,
58            long_token_length: None,
59        }
60    }
61
62    /// Finishes building the controller, returning Err if any necessary configs are
63    /// missing.
64    pub fn finalize(self) -> Result<PrefixedApiKeyController<R, D>, BuilderError> {
65        if self.prefix.is_none() {
66            return Err(BuilderError::MissingPrefix);
67        }
68
69        if self.rng.is_none() {
70            return Err(BuilderError::MissingRng);
71        }
72
73        if self.short_token_length.is_none() {
74            return Err(BuilderError::MissingShortTokenLength);
75        }
76
77        if self.long_token_length.is_none() {
78            return Err(BuilderError::MissingLongTokenLength);
79        }
80
81        Ok(PrefixedApiKeyController::new(
82            self.prefix.unwrap(),
83            self.rng.unwrap(),
84            self.short_token_prefix,
85            self.short_token_length.unwrap(),
86            self.long_token_length.unwrap(),
87        ))
88    }
89
90    /// Helper for setting the default short and long token length based on the
91    /// defaults set in the [typescript version Prefixed API Key module](https://github.com/seamapi/prefixed-api-key/blob/main/src/index.ts#L19-L20).
92    pub fn default_lengths(self) -> Self {
93        self.short_token_length(8).long_token_length(24)
94    }
95
96    /// Sets the token prefix. This should be the name of your company or organization.
97    pub fn prefix(mut self, prefix: String) -> Self {
98        self.prefix = Some(prefix);
99        self
100    }
101
102    /// Sets an rng source that implements [RngCore](rand::RngCore), which will be used for
103    /// generating bytes used in the short and long tokens of the key.
104    pub fn rng(mut self, rng: R) -> Self {
105        self.rng = Some(rng);
106        self
107    }
108
109    /// An optional prefix for the short tokens. The length of this value should
110    /// be less than the value you set for the `short_token_length`, and should
111    /// leave enough space to avoid collisions with other short tokens.
112    ///
113    /// Default: None
114    pub fn short_token_prefix(mut self, short_token_prefix: Option<String>) -> Self {
115        self.short_token_prefix = short_token_prefix;
116        self
117    }
118
119    /// The length of the short token
120    pub fn short_token_length(mut self, short_token_length: usize) -> Self {
121        self.short_token_length = Some(short_token_length);
122        self
123    }
124
125    /// The length of the secret long token
126    pub fn long_token_length(mut self, long_token_length: usize) -> Self {
127        self.long_token_length = Some(long_token_length);
128        self
129    }
130}
131
132impl<D: Digest + FixedOutputReset + Clone> ControllerBuilder<OsRng, D> {
133    /// Helper function for configuring the Controller with an instance of [OsRng](rand::rngs::OsRng).
134    ///
135    /// <p style="background:rgba(255,181,77,0.16);padding:0.75em;">
136    /// <strong>Warning:</strong>
137    /// The RNG you pick is an important decision. Please familiarize yourself with the
138    /// <a href="https://docs.rs/rand/latest/rand/rngs/index.html#background-random-number-generators-rngs">types of RNGs</a>,
139    /// and then read the descriptions of each of
140    /// <a href="https://docs.rs/rand/latest/rand/rngs/index.html#our-generators">the RNGs provided in the rand crate</a>
141    /// to determine the most appropriate RNG for your use case.
142    /// </p>
143    pub fn rng_osrng(self) -> Self {
144        self.rng(OsRng)
145    }
146}
147
148impl<D: Digest + FixedOutputReset + Clone> ControllerBuilder<ThreadRng, D> {
149    /// Helper function for configuring the Controller with an instance of [ThreadRng](rand::rngs::ThreadRng) created
150    /// by calling [default](rand::rngs::ThreadRng::default).
151    ///
152    /// <p style="background:rgba(255,181,77,0.16);padding:0.75em;">
153    /// <strong>Warning:</strong>
154    /// The RNG you pick is an important decision. Please familiarize yourself with the
155    /// <a href="https://docs.rs/rand/latest/rand/rngs/index.html#background-random-number-generators-rngs">types of RNGs</a>,
156    /// and then read the descriptions of each of
157    /// <a href="https://docs.rs/rand/latest/rand/rngs/index.html#our-generators">the RNGs provided in the rand crate</a>
158    /// to determine the most appropriate RNG for your use case.
159    /// </p>
160    pub fn rng_threadrng(self) -> Self {
161        self.rng(ThreadRng::default())
162    }
163}
164
165impl<D: Digest + FixedOutputReset + Clone> ControllerBuilder<StdRng, D> {
166    /// Helper function for configuring the Controller with an instance of [StdRng](rand::rngs::StdRng) created
167    /// by calling [from_entropy](rand::rngs::StdRng::from_entropy).
168    ///
169    /// <p style="background:rgba(255,181,77,0.16);padding:0.75em;">
170    /// <strong>Warning:</strong>
171    /// The RNG you pick is an important decision. Please familiarize yourself with the
172    /// <a href="https://docs.rs/rand/latest/rand/rngs/index.html#background-random-number-generators-rngs">types of RNGs</a>,
173    /// and then read the descriptions of each of
174    /// <a href="https://docs.rs/rand/latest/rand/rngs/index.html#our-generators">the RNGs provided in the rand crate</a>
175    /// to determine the most appropriate RNG for your use case.
176    /// </p>
177    pub fn rng_stdrng(self) -> Self {
178        self.rng(StdRng::from_entropy())
179    }
180}
181
182#[cfg(feature = "sha2")]
183impl ControllerBuilder<OsRng, Sha256> {
184    /// Helper function for configuring the Controller with a new [Sha256](sha2::Sha256) instance
185    ///
186    /// Requires the "sha2" feature
187    pub fn seam_defaults(self) -> Self {
188        self.digest_sha256().rng_osrng().default_lengths()
189    }
190}
191
192#[cfg(feature = "sha2")]
193impl<R: RngCore + Clone> ControllerBuilder<R, Sha224> {
194    /// Helper function for configuring the Controller with a new [Sha224](sha2::Sha224) instance
195    ///
196    /// Requires the "sha2" feature
197    pub fn digest_sha224(self) -> Self {
198        self
199    }
200}
201
202#[cfg(feature = "sha2")]
203impl<R: RngCore + Clone> ControllerBuilder<R, Sha256> {
204    /// Helper function for configuring the Controller with a new [Sha256](sha2::Sha256) instance
205    ///
206    /// Requires the "sha2" feature
207    pub fn digest_sha256(self) -> Self {
208        self
209    }
210}
211
212#[cfg(feature = "sha2")]
213impl<R: RngCore + Clone> ControllerBuilder<R, Sha384> {
214    /// Helper function for configuring the Controller with a new [Sha384](sha2::Sha384) instance
215    ///
216    /// Requires the "sha2" feature
217    pub fn digest_sha384(self) -> Self {
218        self
219    }
220}
221
222#[cfg(feature = "sha2")]
223impl<R: RngCore + Clone> ControllerBuilder<R, Sha512> {
224    /// Helper function for configuring the Controller with a new [Sha512](sha2::Sha512) instance
225    ///
226    /// Requires the "sha2" feature
227    pub fn digest_sha512(self) -> Self {
228        self
229    }
230}
231
232#[cfg(feature = "sha2")]
233impl<R: RngCore + Clone> ControllerBuilder<R, Sha512_224> {
234    /// Helper function for configuring the Controller with a new [Sha512_224](sha2::Sha512_224) instance
235    ///
236    /// Requires the "sha2" feature
237    pub fn digest_sha512_224(self) -> Self {
238        self
239    }
240}
241
242#[cfg(feature = "sha2")]
243impl<R: RngCore + Clone> ControllerBuilder<R, Sha512_256> {
244    /// Helper function for configuring the Controller with a new [Sha512_256](sha2::Sha512_256) instance
245    ///
246    /// Requires the "sha2" feature
247    pub fn digest_sha512_256(self) -> Self {
248        self
249    }
250}
251
252impl<R: RngCore + Clone, D: Digest + FixedOutputReset + Clone> Default for ControllerBuilder<R, D> {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258#[cfg(test)]
259mod controller_builder_tests {
260    use rand::rngs::OsRng;
261    use sha2::Sha256;
262
263    use super::ControllerBuilder;
264
265    #[test]
266    fn errors_when_no_values_set() {
267        let controller_result = ControllerBuilder::<OsRng, Sha256>::new().finalize();
268        assert!(controller_result.is_err())
269    }
270
271    #[test]
272    fn ok_with_all_values_provided() {
273        let controller_result = ControllerBuilder::<_, Sha256>::new()
274            .prefix("mycompany".to_owned())
275            .rng(OsRng)
276            .short_token_prefix(None)
277            .short_token_length(4)
278            .long_token_length(500)
279            .finalize();
280        assert!(controller_result.is_ok())
281    }
282
283    #[test]
284    fn ok_with_default_short_token_prefix() {
285        // We just omit setting the short_token_prefix to use the default None value
286        let controller_result = ControllerBuilder::<_, Sha256>::new()
287            .prefix("mycompany".to_owned())
288            .rng(OsRng)
289            .short_token_length(4)
290            .long_token_length(500)
291            .finalize();
292        assert!(controller_result.is_ok())
293    }
294
295    #[test]
296    fn ok_with_default_lengths() {
297        let controller_result = ControllerBuilder::<_, Sha256>::new()
298            .prefix("mycompany".to_owned())
299            .rng(OsRng)
300            .short_token_prefix(None)
301            .default_lengths()
302            .finalize();
303        assert!(controller_result.is_ok())
304    }
305
306    #[test]
307    fn ok_with_rng_osrng() {
308        let controller_result = ControllerBuilder::<_, Sha256>::new()
309            .prefix("mycompany".to_owned())
310            .rng_osrng()
311            .short_token_prefix(None)
312            .default_lengths()
313            .finalize();
314        assert!(controller_result.is_ok())
315    }
316
317    #[test]
318    fn ok_with_rng_threadrng() {
319        let controller_result = ControllerBuilder::<_, Sha256>::new()
320            .prefix("mycompany".to_owned())
321            .rng_threadrng()
322            .short_token_prefix(None)
323            .default_lengths()
324            .finalize();
325        assert!(controller_result.is_ok())
326    }
327
328    #[test]
329    fn ok_with_rng_stdrng() {
330        let controller_result = ControllerBuilder::<_, Sha256>::new()
331            .prefix("mycompany".to_owned())
332            .rng_stdrng()
333            .short_token_prefix(None)
334            .default_lengths()
335            .finalize();
336        assert!(controller_result.is_ok())
337    }
338}
339
340#[cfg(feature = "sha2")]
341#[cfg(test)]
342mod controller_builder_sha2_tests {
343    use digest::{Digest, FixedOutputReset};
344
345    use crate::{
346        rand::rngs::OsRng, rand::rngs::StdRng, rand::rngs::ThreadRng, rand::RngCore,
347        rand::SeedableRng, BuilderError, PakControllerOsSha224, PakControllerOsSha256,
348        PakControllerOsSha384, PakControllerOsSha512, PakControllerOsSha512_224,
349        PakControllerOsSha512_256, PakControllerStdSha256, PakControllerThreadSha256,
350    };
351
352    use super::{ControllerBuilder, PrefixedApiKeyController};
353
354    fn controller_generates_matching_hash<R, D>(controller: PrefixedApiKeyController<R, D>) -> bool
355    where
356        R: RngCore + Clone,
357        D: Digest + FixedOutputReset,
358    {
359        let (pak, hash) = controller.generate_key_and_hash();
360        controller.check_hash(&pak, &hash)
361    }
362
363    #[test]
364    fn ok_with_digest_sha224() {
365        let controller_result: Result<PakControllerOsSha224, BuilderError> =
366            ControllerBuilder::new()
367                .prefix("mycompany".to_owned())
368                .rng(OsRng)
369                .digest_sha224()
370                .short_token_prefix(None)
371                .default_lengths()
372                .finalize();
373        assert!(controller_result.is_ok());
374        assert!(controller_generates_matching_hash(
375            controller_result.unwrap()
376        ));
377    }
378
379    #[test]
380    fn ok_with_digest_sha256() {
381        let controller_result: Result<PakControllerOsSha256, BuilderError> =
382            ControllerBuilder::new()
383                .prefix("mycompany".to_owned())
384                .rng(OsRng)
385                .digest_sha256()
386                .short_token_prefix(None)
387                .default_lengths()
388                .finalize();
389        assert!(controller_result.is_ok());
390        assert!(controller_generates_matching_hash(
391            controller_result.unwrap()
392        ));
393    }
394
395    #[test]
396    fn ok_with_digest_sha384() {
397        let controller_result: Result<PakControllerOsSha384, BuilderError> =
398            ControllerBuilder::new()
399                .prefix("mycompany".to_owned())
400                .rng(OsRng)
401                .digest_sha384()
402                .short_token_prefix(None)
403                .default_lengths()
404                .finalize();
405        assert!(controller_result.is_ok());
406        assert!(controller_generates_matching_hash(
407            controller_result.unwrap()
408        ));
409    }
410
411    #[test]
412    fn ok_with_digest_sha512() {
413        let controller_result: Result<PakControllerOsSha512, BuilderError> =
414            ControllerBuilder::new()
415                .prefix("mycompany".to_owned())
416                .rng(OsRng)
417                .digest_sha512()
418                .short_token_prefix(None)
419                .default_lengths()
420                .finalize();
421        assert!(controller_result.is_ok());
422        assert!(controller_generates_matching_hash(
423            controller_result.unwrap()
424        ));
425    }
426
427    #[test]
428    fn ok_with_digest_sha512_224() {
429        let controller_result: Result<PakControllerOsSha512_224, BuilderError> =
430            ControllerBuilder::new()
431                .prefix("mycompany".to_owned())
432                .rng(OsRng)
433                .digest_sha512_224()
434                .short_token_prefix(None)
435                .default_lengths()
436                .finalize();
437        assert!(controller_result.is_ok());
438        assert!(controller_generates_matching_hash(
439            controller_result.unwrap()
440        ));
441    }
442
443    #[test]
444    fn ok_with_digest_sha512_256() {
445        let controller_result: Result<PakControllerOsSha512_256, BuilderError> =
446            ControllerBuilder::new()
447                .prefix("mycompany".to_owned())
448                .rng(OsRng)
449                .digest_sha512_256()
450                .short_token_prefix(None)
451                .default_lengths()
452                .finalize();
453        assert!(controller_result.is_ok());
454        assert!(controller_generates_matching_hash(
455            controller_result.unwrap()
456        ));
457    }
458
459    #[test]
460    fn ok_with_rng_std() {
461        let controller_result: Result<PakControllerStdSha256, BuilderError> =
462            ControllerBuilder::new()
463                .prefix("mycompany".to_owned())
464                .rng_stdrng()
465                .digest_sha256()
466                .short_token_prefix(None)
467                .default_lengths()
468                .finalize();
469        assert!(controller_result.is_ok());
470        assert!(controller_generates_matching_hash(
471            controller_result.unwrap()
472        ));
473    }
474
475    #[test]
476    fn ok_with_rng_thread() {
477        let controller_result: Result<PakControllerThreadSha256, BuilderError> =
478            ControllerBuilder::new()
479                .prefix("mycompany".to_owned())
480                .rng_threadrng()
481                .digest_sha256()
482                .short_token_prefix(None)
483                .default_lengths()
484                .finalize();
485        assert!(controller_result.is_ok());
486        assert!(controller_generates_matching_hash(
487            controller_result.unwrap()
488        ));
489    }
490
491    #[test]
492    fn ok_with_seam_deafults() {
493        let controller_result: Result<PakControllerOsSha256, BuilderError> =
494            ControllerBuilder::new()
495                .prefix("mycompany".to_owned())
496                .seam_defaults()
497                .finalize();
498        assert!(controller_result.is_ok());
499        assert!(controller_generates_matching_hash(
500            controller_result.unwrap()
501        ));
502    }
503}