use crate::types::{Error, Result};
use base64::{engine::general_purpose, Engine as _};
use hmac::{Hmac, Mac};
use rand::Rng;
use sha2::{Digest, Sha256, Sha512};
type HmacSha256 = Hmac<Sha256>;
type HmacSha512 = Hmac<Sha512>;
#[must_use]
pub fn sha256(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
hex::encode(result)
}
#[must_use]
pub fn sha512(data: &[u8]) -> String {
let mut hasher = Sha512::new();
hasher.update(data);
let result = hasher.finalize();
hex::encode(result)
}
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<String> {
let mut mac = HmacSha256::new_from_slice(key)
.map_err(|e| Error::generic(format!("Invalid key length: {e}")))?;
mac.update(data);
let result = mac.finalize();
Ok(hex::encode(result.into_bytes()))
}
pub fn hmac_sha512(key: &[u8], data: &[u8]) -> Result<String> {
let mut mac = HmacSha512::new_from_slice(key)
.map_err(|e| Error::generic(format!("Invalid key length: {e}")))?;
mac.update(data);
let result = mac.finalize();
Ok(hex::encode(result.into_bytes()))
}
pub fn verify_hmac_sha256(key: &[u8], data: &[u8], signature: &str) -> Result<bool> {
let expected = hmac_sha256(key, data)?;
Ok(constant_time_compare(&expected, signature))
}
pub fn verify_hmac_sha512(key: &[u8], data: &[u8], signature: &str) -> Result<bool> {
let expected = hmac_sha512(key, data)?;
Ok(constant_time_compare(&expected, signature))
}
#[must_use]
pub fn constant_time_compare(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let mut result: u8 = 0;
for (byte_a, byte_b) in a.bytes().zip(b.bytes()) {
result |= byte_a ^ byte_b;
}
result == 0
}
#[must_use]
pub fn base64_encode(data: &[u8]) -> String {
general_purpose::STANDARD.encode(data)
}
pub fn base64_decode(encoded: &str) -> Result<Vec<u8>> {
general_purpose::STANDARD
.decode(encoded)
.map_err(|e| Error::generic(format!("Base64 decode error: {e}")))
}
#[must_use]
pub fn base64_url_encode(data: &[u8]) -> String {
general_purpose::URL_SAFE_NO_PAD.encode(data)
}
pub fn base64_url_decode(encoded: &str) -> Result<Vec<u8>> {
general_purpose::URL_SAFE_NO_PAD
.decode(encoded)
.map_err(|e| Error::generic(format!("Base64 URL decode error: {e}")))
}
#[must_use]
pub fn generate_random_bytes(length: usize) -> Vec<u8> {
let mut rng = rand::thread_rng();
(0..length).map(|_| rng.gen::<u8>()).collect()
}
#[must_use]
pub fn generate_secure_token(byte_length: usize) -> String {
let bytes = generate_random_bytes(byte_length);
hex::encode(bytes)
}
#[must_use]
pub fn generate_secure_token_base64(byte_length: usize) -> String {
let bytes = generate_random_bytes(byte_length);
base64_encode(&bytes)
}
#[must_use]
pub fn calculate_cookie_checksum(
cookie_name: &str,
cookie_value: &str,
secret: Option<&str>,
) -> String {
let data = match secret {
Some(s) => format!("{cookie_name}:{cookie_value}:{s}"),
None => format!("{cookie_name}:{cookie_value}"),
};
sha256(data.as_bytes())
}
#[must_use]
pub fn verify_cookie_checksum(
cookie_name: &str,
cookie_value: &str,
expected_checksum: &str,
secret: Option<&str>,
) -> bool {
let calculated = calculate_cookie_checksum(cookie_name, cookie_value, secret);
constant_time_compare(&calculated, expected_checksum)
}
#[must_use]
pub fn detect_cookie_signatures(cookie_value: &str) -> Vec<String> {
let mut signatures = Vec::new();
if cookie_value.split('.').count() == 3 {
let parts: Vec<&str> = cookie_value.split('.').collect();
if parts.iter().all(|p| is_base64_like(p)) {
signatures.push("JWT".to_string());
}
}
if (cookie_value.len() == 64 || cookie_value.len() == 128)
&& cookie_value.chars().all(|c| c.is_ascii_hexdigit())
{
if cookie_value.len() == 64 {
signatures.push("HMAC-SHA256-HEX".to_string());
} else {
signatures.push("HMAC-SHA512-HEX".to_string());
}
}
if (32..=128).contains(&cookie_value.len()) && is_base64_like(cookie_value) {
signatures.push("BASE64-SIGNATURE".to_string());
}
if cookie_value.len() >= 32
&& cookie_value
.chars()
.all(|c| c.is_alphanumeric() || c == '-')
{
signatures.push("SESSION-TOKEN".to_string());
}
signatures
}
fn is_base64_like(s: &str) -> bool {
s.chars()
.all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '=' || c == '-' || c == '_')
}
#[must_use]
pub fn calculate_entropy(data: &str) -> f64 {
use std::collections::HashMap;
if data.is_empty() {
return 0.0;
}
let mut frequencies: HashMap<char, usize> = HashMap::new();
for c in data.chars() {
*frequencies.entry(c).or_insert(0) += 1;
}
#[allow(clippy::cast_precision_loss)]
let len = data.len() as f64;
let mut entropy = 0.0;
for count in frequencies.values() {
#[allow(clippy::cast_precision_loss)]
let probability = *count as f64 / len;
entropy -= probability * probability.log2();
}
entropy
}
#[must_use]
pub fn assess_token_strength(token: &str) -> &'static str {
let entropy = calculate_entropy(token);
#[allow(clippy::cast_precision_loss)]
let total_entropy = entropy * token.len() as f64;
match total_entropy {
e if e < 28.0 => "WEAK", e if e < 36.0 => "MODERATE", e if e < 60.0 => "STRONG", _ => "VERY_STRONG", }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sha256() {
let hash = sha256(b"test");
assert_eq!(hash.len(), 64); assert_eq!(
hash,
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
);
}
#[test]
fn test_hmac_sha256() {
let hmac = hmac_sha256(b"secret", b"message").unwrap();
assert_eq!(hmac.len(), 64);
assert!(verify_hmac_sha256(b"secret", b"message", &hmac).unwrap());
assert!(!verify_hmac_sha256(b"wrong", b"message", &hmac).unwrap());
}
#[test]
fn test_constant_time_compare() {
assert!(constant_time_compare("hello", "hello"));
assert!(!constant_time_compare("hello", "world"));
assert!(!constant_time_compare("hello", "hell"));
}
#[test]
fn test_base64() {
let data = b"Hello, World!";
let encoded = base64_encode(data);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(data.to_vec(), decoded);
}
#[test]
fn test_base64_url() {
let data = b"URL safe encoding test!";
let encoded = base64_url_encode(data);
assert!(!encoded.contains('='));
let decoded = base64_url_decode(&encoded).unwrap();
assert_eq!(data.to_vec(), decoded);
}
#[test]
fn test_random_generation() {
let bytes1 = generate_random_bytes(32);
let bytes2 = generate_random_bytes(32);
assert_eq!(bytes1.len(), 32);
assert_eq!(bytes2.len(), 32);
assert_ne!(bytes1, bytes2); }
#[test]
fn test_secure_token() {
let token = generate_secure_token(16);
assert_eq!(token.len(), 32); }
#[test]
fn test_cookie_checksum() {
let checksum = calculate_cookie_checksum("session", "value123", Some("secret"));
assert!(verify_cookie_checksum(
"session",
"value123",
&checksum,
Some("secret")
));
assert!(!verify_cookie_checksum(
"session",
"value123",
&checksum,
Some("wrong")
));
}
#[test]
fn test_detect_jwt() {
let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
let sigs = detect_cookie_signatures(jwt);
assert!(sigs.contains(&"JWT".to_string()));
}
#[test]
fn test_detect_hmac_hex() {
let hmac = "a".repeat(64); let sigs = detect_cookie_signatures(&hmac);
assert!(sigs.contains(&"HMAC-SHA256-HEX".to_string()));
}
#[test]
fn test_entropy() {
let low = calculate_entropy("aaaaaaaaaa");
assert!(low < 0.1);
let high = calculate_entropy("x9K2mP4rL8nQ3vZ");
assert!(high > 3.0);
}
#[test]
fn test_token_strength() {
assert_eq!(assess_token_strength("weak"), "WEAK");
assert_eq!(assess_token_strength("token12345"), "MODERATE");
assert_eq!(assess_token_strength("random_tok12"), "STRONG");
let very_strong = generate_secure_token(32);
assert_eq!(assess_token_strength(&very_strong), "VERY_STRONG");
}
#[test]
fn test_sha512() {
let hash = sha512(b"test");
assert_eq!(hash.len(), 128); }
#[test]
fn test_hmac_sha512() {
let hmac = hmac_sha512(b"secret", b"message").unwrap();
assert_eq!(hmac.len(), 128);
assert!(verify_hmac_sha512(b"secret", b"message", &hmac).unwrap());
}
}