use crate::NonceCredential;
use crate::nonce::error::NonceError;
use crate::nonce::time_utils::current_timestamp;
use base64::Engine;
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
pub type NonceGeneratorFn = Box<dyn Fn() -> String + Send + Sync>;
pub type TimeProviderFn = Box<dyn Fn() -> Result<u64, NonceError> + Send + Sync>;
pub struct CredentialBuilder {
secret: Vec<u8>,
nonce_generator: NonceGeneratorFn,
time_provider: TimeProviderFn,
}
impl CredentialBuilder {
pub fn new(secret: &[u8]) -> Self {
Self {
secret: secret.to_vec(),
nonce_generator: Box::new(|| uuid::Uuid::new_v4().to_string()),
time_provider: Box::new(|| Ok(current_timestamp()? as u64)),
}
}
pub fn with_nonce_generator<F>(mut self, generator: F) -> Self
where
F: Fn() -> String + Send + Sync + 'static,
{
self.nonce_generator = Box::new(generator);
self
}
pub fn with_time_provider<F>(mut self, provider: F) -> Self
where
F: Fn() -> Result<u64, NonceError> + Send + Sync + 'static,
{
self.time_provider = Box::new(provider);
self
}
pub fn sign(self, payload: &[u8]) -> Result<NonceCredential, NonceError> {
let timestamp = (self.time_provider)()?;
let nonce = (self.nonce_generator)();
let signature = self.create_signature(&self.secret, timestamp, &nonce, payload)?;
Ok(NonceCredential {
timestamp,
nonce,
signature,
})
}
pub fn sign_structured(self, components: &[&[u8]]) -> Result<NonceCredential, NonceError> {
let timestamp = (self.time_provider)()?;
let nonce = (self.nonce_generator)();
let signature =
self.create_structured_signature(&self.secret, timestamp, &nonce, components)?;
Ok(NonceCredential {
timestamp,
nonce,
signature,
})
}
pub fn sign_with<F>(self, mac_fn: F) -> Result<NonceCredential, NonceError>
where
F: FnOnce(&mut HmacSha256, &str, &str),
{
let timestamp = (self.time_provider)()?;
let nonce = (self.nonce_generator)();
let signature = self.create_custom_signature(&self.secret, timestamp, &nonce, mac_fn)?;
Ok(NonceCredential {
timestamp,
nonce,
signature,
})
}
fn create_signature(
&self,
secret: &[u8],
timestamp: u64,
nonce: &str,
payload: &[u8],
) -> Result<String, NonceError> {
let mut mac = HmacSha256::new_from_slice(secret)
.map_err(|e| NonceError::CryptoError(format!("Invalid secret key: {e}")))?;
mac.update(timestamp.to_string().as_bytes());
mac.update(nonce.as_bytes());
mac.update(payload);
let result = mac.finalize();
Ok(base64::engine::general_purpose::STANDARD.encode(result.into_bytes()))
}
fn create_structured_signature(
&self,
secret: &[u8],
timestamp: u64,
nonce: &str,
components: &[&[u8]],
) -> Result<String, NonceError> {
let mut mac = HmacSha256::new_from_slice(secret)
.map_err(|e| NonceError::CryptoError(format!("Invalid secret key: {e}")))?;
mac.update(timestamp.to_string().as_bytes());
mac.update(nonce.as_bytes());
for component in components {
mac.update(component);
}
let result = mac.finalize();
Ok(base64::engine::general_purpose::STANDARD.encode(result.into_bytes()))
}
fn create_custom_signature<F>(
&self,
secret: &[u8],
timestamp: u64,
nonce: &str,
mac_fn: F,
) -> Result<String, NonceError>
where
F: FnOnce(&mut HmacSha256, &str, &str),
{
let mut mac = HmacSha256::new_from_slice(secret)
.map_err(|e| NonceError::CryptoError(format!("Invalid secret key: {e}")))?;
mac_fn(&mut mac, ×tamp.to_string(), nonce);
let result = mac.finalize();
Ok(base64::engine::general_purpose::STANDARD.encode(result.into_bytes()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[test]
fn test_credential_builder_new() {
let builder = CredentialBuilder::new(b"test_secret");
assert_eq!(builder.secret, b"test_secret".to_vec());
}
#[test]
fn test_basic_signing() {
let credential = CredentialBuilder::new(b"secret").sign(b"payload").unwrap();
assert!(!credential.nonce.is_empty());
assert!(credential.timestamp > 0);
assert!(!credential.signature.is_empty());
}
#[test]
fn test_structured_signing() {
let credential = CredentialBuilder::new(b"secret")
.sign_structured(&[b"part1", b"part2", b"part3"])
.unwrap();
assert!(!credential.nonce.is_empty());
assert!(credential.timestamp > 0);
assert!(!credential.signature.is_empty());
}
#[test]
fn test_custom_nonce_generator() {
let counter = Arc::new(AtomicU64::new(0));
let counter_clone = counter.clone();
let credential = CredentialBuilder::new(b"secret")
.with_nonce_generator(move || {
let id = counter_clone.fetch_add(1, Ordering::SeqCst);
format!("custom-{id:010}")
})
.sign(b"payload")
.unwrap();
assert_eq!(credential.nonce, "custom-0000000000");
}
#[test]
fn test_custom_time_provider() {
let fixed_time = 1234567890u64;
let credential = CredentialBuilder::new(b"secret")
.with_time_provider(move || Ok(fixed_time))
.sign(b"payload")
.unwrap();
assert_eq!(credential.timestamp, fixed_time);
}
#[test]
fn test_time_provider_error() {
let result = CredentialBuilder::new(b"secret")
.with_time_provider(|| Err(NonceError::CryptoError("Time error".to_string())))
.sign(b"payload");
assert!(matches!(result, Err(NonceError::CryptoError(_))));
}
#[test]
fn test_sign_with_custom_mac() {
let credential = CredentialBuilder::new(b"secret")
.sign_with(|mac, timestamp, nonce| {
mac.update(b"prefix:");
mac.update(timestamp.as_bytes());
mac.update(b":nonce:");
mac.update(nonce.as_bytes());
mac.update(b":custom");
})
.unwrap();
assert!(!credential.nonce.is_empty());
assert!(credential.timestamp > 0);
assert!(!credential.signature.is_empty());
}
#[test]
fn test_multiple_credentials_different_nonces() {
let builder = || CredentialBuilder::new(b"secret");
let cred1 = builder().sign(b"payload").unwrap();
let cred2 = builder().sign(b"payload").unwrap();
assert_ne!(cred1.nonce, cred2.nonce);
assert_ne!(cred1.signature, cred2.signature);
}
#[test]
fn test_structured_vs_regular_signing() {
let secret = b"secret";
let mut combined = Vec::new();
combined.extend_from_slice(b"part1");
combined.extend_from_slice(b"part2");
let cred1 = CredentialBuilder::new(secret)
.with_nonce_generator(|| "fixed_nonce".to_string())
.with_time_provider(|| Ok(1234567890))
.sign(&combined)
.unwrap();
let cred2 = CredentialBuilder::new(secret)
.with_nonce_generator(|| "fixed_nonce".to_string())
.with_time_provider(|| Ok(1234567890))
.sign_structured(&[b"part1", b"part2"])
.unwrap();
assert_eq!(cred1.signature, cred2.signature);
}
#[test]
fn test_builder_method_chaining() {
let secret = b"test_secret";
let payload = b"test_payload";
let cred1 = CredentialBuilder::new(secret)
.with_nonce_generator(|| "custom".to_string())
.with_time_provider(|| Ok(1234567890))
.sign(payload)
.unwrap();
let cred2 = CredentialBuilder::new(secret)
.with_time_provider(|| Ok(1234567890))
.with_nonce_generator(|| "custom".to_string())
.sign(payload)
.unwrap();
assert_eq!(cred1.nonce, "custom");
assert_eq!(cred1.timestamp, 1234567890);
assert_eq!(cred1.signature, cred2.signature);
}
}