#[macro_use]
extern crate log;
use chrono::{DateTime, Utc};
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use crypto::sha3::Sha3;
use rand::distributions::{Alphanumeric, Distribution};
use rand::thread_rng;
use std::convert::TryFrom;
use std::fmt;
simpl::err!(HcError,
{Int@std::num::ParseIntError;}
);
fn _hash<T: Digest>(hasher: &mut T, challenge: &str, bits: u32) -> String {
let mut counter = 0;
let hex_digits = ((bits as f32) / 4.).ceil() as usize;
let zeros = String::from_utf8(vec![b'0'; hex_digits]).unwrap();
loop {
hasher.input_str(&format!("{}:{:x}", challenge, counter));
if hasher.result_str()[..hex_digits] == zeros {
debug!("{}", hasher.result_str());
return format!("{:x}", counter);
};
hasher.reset();
counter += 1
}
}
fn _mint(challenge: &str, bits: u32) -> String {
if cfg!(feature = "sha1") {
let mut hasher = Sha1::new();
_hash(&mut hasher, challenge, bits)
} else {
let mut hasher = Sha3::sha3_256();
_hash(&mut hasher, challenge, bits)
}
}
pub fn check_with_params(
stamp: &str,
resource: Option<&str>,
bits: Option<u32>,
expiration: Option<DateTime<Utc>>,
) -> Result<bool, HcError> {
let stamp = Stamp::try_from(stamp)?;
if !stamp.check_version() {
return Err(HcError::from(
format!(
"Can only check version 1 stamp, got version {}",
stamp.version
)
.as_str(),
));
}
if !stamp.check_resource(resource) {
return Ok(false);
}
if !stamp.check_bits(bits) {
return Ok(false);
}
if !stamp.check_expiration(expiration) {
return Ok(false);
}
Ok(stamp.check())
}
pub fn check(stamp: &str) -> Result<bool> {
check_with_params(stamp, None, None, None)
}
#[derive(Debug)]
pub struct Stamp {
version: String,
claim: u32,
ts: String,
resource: String,
ext: String,
rand: String,
counter: String,
}
impl Stamp {
fn check_version(&self) -> bool {
self.version == "1"
}
fn check_resource(&self, resource: Option<&str>) -> bool {
if let Some(resource) = resource {
self.resource == resource
} else {
true
}
}
fn check_bits(&self, bits: Option<u32>) -> bool {
if let Some(bits) = bits {
bits <= self.claim
} else {
true
}
}
fn check_expiration(&self, expiration: Option<DateTime<Utc>>) -> bool {
if let Some(expiration) = expiration {
Utc::now() < expiration
} else {
true
}
}
fn hex_digits(&self) -> usize {
((self.claim as f32) / 4.).floor() as usize
}
fn zeroes(&self) -> String {
String::from_utf8(vec![b'0'; self.hex_digits()]).unwrap()
}
fn _check<T: Digest>(&self, hasher: &mut T) -> bool {
debug!("{}", self.to_string());
hasher.input_str(&self.to_string());
debug!("{}", hasher.result_str());
hasher.result_str()[..self.hex_digits()] == self.zeroes()
}
fn check(&self) -> bool {
if cfg!(feature = "sha1") {
let mut hasher = Sha1::new();
self._check(&mut hasher)
} else {
let mut hasher = Sha3::sha3_256();
self._check(&mut hasher)
}
}
fn format(&self) -> String {
format!(
"{}:{}:{}:{}:{}:{}:{}",
self.version, self.claim, self.ts, self.resource, self.ext, self.rand, self.counter
)
}
pub fn mint(
resource: Option<&str>,
bits: Option<u32>,
now: Option<DateTime<Utc>>,
ext: Option<&str>,
saltchars: Option<usize>,
stamp_seconds: bool,
) -> Result<Self> {
let version = "1";
let now = now.unwrap_or_else(Utc::now);
let ts = if stamp_seconds {
now.format("%Y%M%d%H%M%S")
} else {
now.format("%Y%M%d")
};
let bits = bits.unwrap_or(20);
let ext = ext.unwrap_or("");
let saltchars = saltchars.unwrap_or(8);
let rand = Alphanumeric
.sample_iter(thread_rng())
.take(saltchars)
.collect();
let resource = resource.unwrap_or("");
let challenge = format!("{}:{}:{}:{}:{}:{}", version, bits, ts, resource, ext, rand);
Ok(Stamp {
version: version.to_string(),
claim: bits,
ts: ts.to_string(),
resource: resource.to_string(),
ext: ext.to_string(),
rand,
counter: _mint(&challenge, bits),
})
}
pub fn with_secs() -> Result<Self> {
Self::mint(None, None, None, None, None, true)
}
pub fn with_resource(resource: &str, stamp_seconds: bool) -> Result<Self> {
Self::mint(Some(resource), None, None, None, None, stamp_seconds)
}
pub fn with_bits(bits: u32, stamp_seconds: bool) -> Result<Self> {
Self::mint(None, Some(bits), None, None, None, stamp_seconds)
}
pub fn with_resource_and_bits(resource: &str, bits: u32, stamp_seconds: bool) -> Result<Self> {
Self::mint(Some(resource), Some(bits), None, None, None, stamp_seconds)
}
}
impl TryFrom<&str> for Stamp {
type Error = HcError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let stamp_vec = value.split(':').collect::<Vec<&str>>();
if stamp_vec.len() != 7 {
return Err(HcError::from(
format!("Malformed stamp, expected 6 parts, got {}", stamp_vec.len()).as_str(),
));
}
Ok(Stamp {
version: stamp_vec[0].to_string(),
claim: stamp_vec[1].parse()?,
ts: stamp_vec[2].to_string(),
resource: stamp_vec[3].to_string(),
ext: stamp_vec[4].to_string(),
rand: stamp_vec[5].to_string(),
counter: stamp_vec[6].to_string(),
})
}
}
impl fmt::Display for Stamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format())
}
}
impl Default for Stamp {
fn default() -> Self {
Self::mint(None, None, None, None, None, false).unwrap()
}
}
mod test {
use crate::Stamp;
use crate::check;
#[test]
fn test_default() {
let stamp = Stamp::default();
let result = check(&stamp.to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_with_secs() {
let stamp = Stamp::with_secs();
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_with_resource() {
let stamp = Stamp::with_resource("test", false);
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_with_resource_and_seconds() {
let stamp = Stamp::with_resource("test", true);
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_with_bits() {
let stamp = Stamp::with_bits(16, false);
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_with_bits_and_seconds() {
let stamp = Stamp::with_bits(16, true);
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_with_resource_and_bits() {
let stamp = Stamp::with_resource_and_bits("test", 16, false);
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_with_resource_and_bits_and_seconds() {
let stamp = Stamp::with_resource_and_bits("test", 16, true);
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_mint() {
let stamp = Stamp::mint(Some("test"), Some(15), None, Some("name1=2"), Some(12), false);
assert!(stamp.is_ok());
let result = check(&stamp.unwrap().to_string());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_check() {
assert!(check("1:20:20202116:test::Z4p8WaiO:31c14").unwrap());
assert!(!check("1:20:20202116:test1::Z4p8WaiO:31c14").unwrap());
assert!(!check("1:20:20202116:test::z4p8WaiO:31c14").unwrap());
assert!(!check("1:20:20202116:test::Z4p8WaiO:31C14").unwrap());
assert!(check("0:20:20202116:test::Z4p8WaiO:31c14").is_err());
assert!(!check("1:19:20202116:test::Z4p8WaiO:31c14").unwrap());
assert!(!check("1:20:20202115:test::Z4p8WaiO:31c14").unwrap());
}
}