use base64::{engine::general_purpose::STANDARD, prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::{ClobError, Result};
#[derive(Clone, Debug)]
pub struct Signer {
secret: Vec<u8>,
}
impl Signer {
pub fn new(secret: &str) -> Result<Self> {
let decoded = BASE64_URL_SAFE_NO_PAD
.decode(secret)
.or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(secret))
.or_else(|_| STANDARD.decode(secret))
.unwrap_or_else(|_| secret.as_bytes().to_vec());
Ok(Self { secret: decoded })
}
pub fn sign(&self, message: &str) -> Result<String> {
let mut mac = Hmac::<Sha256>::new_from_slice(&self.secret)
.map_err(|e| ClobError::Crypto(format!("Failed to create HMAC: {}", e)))?;
mac.update(message.as_bytes());
let result = mac.finalize();
let signature = STANDARD.encode(result.into_bytes());
let signature = signature.replace('+', "-").replace('/', "_");
Ok(signature)
}
pub fn create_message(timestamp: u64, method: &str, path: &str, body: Option<&str>) -> String {
format!("{}{}{}{}", timestamp, method, path, body.unwrap_or(""))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign() {
let secret = "c2VjcmV0"; let signer = Signer::new(secret).unwrap();
let message = Signer::create_message(1234567890, "GET", "/api/test", None);
let signature = signer.sign(&message).unwrap();
assert!(!signature.contains('+'));
assert!(!signature.contains('/'));
}
}