use digest::{Digest, FixedOutputReset};
use rand::{
rngs::{OsRng, StdRng, ThreadRng},
RngCore, SeedableRng,
};
use std::fmt;
use std::{error::Error, marker::PhantomData};
#[cfg(feature = "sha2")]
use sha2::{Sha224, Sha256, Sha384, Sha512, Sha512_224, Sha512_256};
use crate::controller::PrefixedApiKeyController;
#[derive(Debug, Clone)]
pub enum BuilderError {
MissingPrefix,
MissingRng,
MissingDigest,
MissingShortTokenLength,
MissingLongTokenLength,
}
impl fmt::Display for BuilderError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BuilderError::MissingPrefix => write!(f, "expected prefix to be set, but wasn't"),
BuilderError::MissingRng => write!(f, "expected rng to be set, but wasn't"),
BuilderError::MissingDigest => write!(f, "expected digest to be set, but wasn't"),
BuilderError::MissingShortTokenLength => {
write!(f, "expected short_token_length to be set, but wasn't")
}
BuilderError::MissingLongTokenLength => {
write!(f, "expected long_token_length to be set, but wasn't")
}
}
}
}
impl Error for BuilderError {}
pub struct ControllerBuilder<R: RngCore, D: Digest + FixedOutputReset> {
prefix: Option<String>,
rng: Option<R>,
digest: PhantomData<D>,
short_token_prefix: Option<String>,
short_token_length: Option<usize>,
long_token_length: Option<usize>,
}
impl<R: RngCore, D: Digest + FixedOutputReset> ControllerBuilder<R, D> {
pub fn new() -> ControllerBuilder<R, D> {
ControllerBuilder {
prefix: None,
rng: None,
digest: PhantomData,
short_token_prefix: None,
short_token_length: None,
long_token_length: None,
}
}
pub fn finalize(self) -> Result<PrefixedApiKeyController<R, D>, BuilderError> {
if self.prefix.is_none() {
return Err(BuilderError::MissingPrefix);
}
if self.rng.is_none() {
return Err(BuilderError::MissingRng);
}
if self.short_token_length.is_none() {
return Err(BuilderError::MissingShortTokenLength);
}
if self.long_token_length.is_none() {
return Err(BuilderError::MissingLongTokenLength);
}
Ok(PrefixedApiKeyController::new(
self.prefix.unwrap(),
self.rng.unwrap(),
self.short_token_prefix,
self.short_token_length.unwrap(),
self.long_token_length.unwrap(),
))
}
pub fn default_lengths(self) -> Self {
self.short_token_length(8).long_token_length(24)
}
pub fn prefix(mut self, prefix: String) -> Self {
self.prefix = Some(prefix);
self
}
pub fn rng(mut self, rng: R) -> Self {
self.rng = Some(rng);
self
}
pub fn short_token_prefix(mut self, short_token_prefix: Option<String>) -> Self {
self.short_token_prefix = short_token_prefix;
self
}
pub fn short_token_length(mut self, short_token_length: usize) -> Self {
self.short_token_length = Some(short_token_length);
self
}
pub fn long_token_length(mut self, long_token_length: usize) -> Self {
self.long_token_length = Some(long_token_length);
self
}
}
impl<D: Digest + FixedOutputReset + Clone> ControllerBuilder<OsRng, D> {
pub fn rng_osrng(self) -> Self {
self.rng(OsRng)
}
}
impl<D: Digest + FixedOutputReset + Clone> ControllerBuilder<ThreadRng, D> {
pub fn rng_threadrng(self) -> Self {
self.rng(ThreadRng::default())
}
}
impl<D: Digest + FixedOutputReset + Clone> ControllerBuilder<StdRng, D> {
pub fn rng_stdrng(self) -> Self {
self.rng(StdRng::from_entropy())
}
}
#[cfg(feature = "sha2")]
impl ControllerBuilder<OsRng, Sha256> {
pub fn seam_defaults(self) -> Self {
self.digest_sha256().rng_osrng().default_lengths()
}
}
#[cfg(feature = "sha2")]
impl<R: RngCore> ControllerBuilder<R, Sha224> {
pub fn digest_sha224(self) -> Self {
self
}
}
#[cfg(feature = "sha2")]
impl<R: RngCore> ControllerBuilder<R, Sha256> {
pub fn digest_sha256(self) -> Self {
self
}
}
#[cfg(feature = "sha2")]
impl<R: RngCore> ControllerBuilder<R, Sha384> {
pub fn digest_sha384(self) -> Self {
self
}
}
#[cfg(feature = "sha2")]
impl<R: RngCore> ControllerBuilder<R, Sha512> {
pub fn digest_sha512(self) -> Self {
self
}
}
#[cfg(feature = "sha2")]
impl<R: RngCore> ControllerBuilder<R, Sha512_224> {
pub fn digest_sha512_224(self) -> Self {
self
}
}
#[cfg(feature = "sha2")]
impl<R: RngCore> ControllerBuilder<R, Sha512_256> {
pub fn digest_sha512_256(self) -> Self {
self
}
}
impl<R: RngCore, D: Digest + FixedOutputReset + Clone> Default for ControllerBuilder<R, D> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod controller_builder_tests {
use rand::rngs::OsRng;
use sha2::Sha256;
use super::ControllerBuilder;
#[test]
fn errors_when_no_values_set() {
let controller_result = ControllerBuilder::<OsRng, Sha256>::new().finalize();
assert!(controller_result.is_err())
}
#[test]
fn ok_with_all_values_provided() {
let controller_result = ControllerBuilder::<_, Sha256>::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.short_token_prefix(None)
.short_token_length(4)
.long_token_length(500)
.finalize();
assert!(controller_result.is_ok())
}
#[test]
fn ok_with_default_short_token_prefix() {
let controller_result = ControllerBuilder::<_, Sha256>::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.short_token_length(4)
.long_token_length(500)
.finalize();
assert!(controller_result.is_ok())
}
#[test]
fn ok_with_default_lengths() {
let controller_result = ControllerBuilder::<_, Sha256>::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok())
}
#[test]
fn ok_with_rng_osrng() {
let controller_result = ControllerBuilder::<_, Sha256>::new()
.prefix("mycompany".to_owned())
.rng_osrng()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok())
}
#[test]
fn ok_with_rng_threadrng() {
let controller_result = ControllerBuilder::<_, Sha256>::new()
.prefix("mycompany".to_owned())
.rng_threadrng()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok())
}
#[test]
fn ok_with_rng_stdrng() {
let controller_result = ControllerBuilder::<_, Sha256>::new()
.prefix("mycompany".to_owned())
.rng_stdrng()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok())
}
}
#[cfg(feature = "sha2")]
#[cfg(test)]
mod controller_builder_sha2_tests {
use digest::{Digest, FixedOutputReset};
use rand::rngs::OsRng;
use rand::RngCore;
use super::{ControllerBuilder, PrefixedApiKeyController};
fn controller_generates_matching_hash<R, D>(
mut controller: PrefixedApiKeyController<R, D>,
) -> bool
where
R: RngCore,
D: Digest + FixedOutputReset,
{
let (pak, hash) = controller.generate_key_and_hash();
controller.check_hash(&pak, &hash)
}
#[test]
fn ok_with_digest_sha224() {
let controller_result = ControllerBuilder::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.digest_sha256()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok());
assert!(controller_generates_matching_hash(
controller_result.unwrap()
));
}
#[test]
fn ok_with_digest_sha256() {
let controller_result = ControllerBuilder::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.digest_sha256()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok());
assert!(controller_generates_matching_hash(
controller_result.unwrap()
));
}
#[test]
fn ok_with_digest_sha384() {
let controller_result = ControllerBuilder::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.digest_sha384()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok());
assert!(controller_generates_matching_hash(
controller_result.unwrap()
));
}
#[test]
fn ok_with_digest_sha512() {
let controller_result = ControllerBuilder::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.digest_sha512()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok());
assert!(controller_generates_matching_hash(
controller_result.unwrap()
));
}
#[test]
fn ok_with_digest_sha512_224() {
let controller_result = ControllerBuilder::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.digest_sha512_224()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok());
assert!(controller_generates_matching_hash(
controller_result.unwrap()
));
}
#[test]
fn ok_with_digest_sha512_256() {
let controller_result = ControllerBuilder::new()
.prefix("mycompany".to_owned())
.rng(OsRng)
.digest_sha512_256()
.short_token_prefix(None)
.default_lengths()
.finalize();
assert!(controller_result.is_ok());
assert!(controller_generates_matching_hash(
controller_result.unwrap()
));
}
#[test]
fn ok_with_seam_deafults() {
let controller_result = ControllerBuilder::new()
.prefix("mycompany".to_owned())
.seam_defaults()
.finalize();
assert!(controller_result.is_ok());
assert!(controller_generates_matching_hash(
controller_result.unwrap()
));
}
}