use base64::Engine;
use hmac::Mac;
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub(crate) struct Header {
pub alg: String,
pub typ: String,
}
impl Header {
pub fn default() -> Self {
Self {
alg: "HS256".to_string(),
typ: "JWT".to_string(),
}
}
}
#[derive(Debug)]
pub(crate) struct Validation;
impl Validation {
pub fn default() -> Self {
Self
}
}
#[derive(Debug)]
pub(crate) struct EncodingKey(pub Vec<u8>);
impl EncodingKey {
pub fn from_secret(secret: &[u8]) -> Self {
Self(secret.to_vec())
}
}
#[derive(Debug)]
pub(crate) struct DecodingKey(pub Vec<u8>);
impl DecodingKey {
pub fn from_secret(secret: &[u8]) -> Self {
Self(secret.to_vec())
}
}
#[derive(Debug)]
pub(crate) struct TokenData<T> {
#[allow(dead_code)]
pub header: Header,
pub claims: T,
}
pub(crate) fn encode<T: serde::Serialize>(
_header: &Header,
claims: &T,
key: &EncodingKey,
) -> Result<String, String> {
let header = r#"{"alg":"HS256","typ":"JWT"}"#;
let header_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header);
let claims_json =
serde_json::to_string(claims).map_err(|e| format!("JWT encode failed: {}", e))?;
let claims_b64 =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims_json.as_bytes());
let signing_input = format!("{}.{}", header_b64, claims_b64);
let signature = sign_hmac_sha256(&signing_input, &key.0)?;
Ok(format!("{}.{}.{}", header_b64, claims_b64, signature))
}
pub(crate) fn decode<T: serde::de::DeserializeOwned>(
token: &str,
key: &DecodingKey,
_validation: &Validation,
) -> Result<TokenData<T>, String> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err("Invalid JWT format".to_string());
}
let (header_b64, claims_b64, signature_b64) = (parts[0], parts[1], parts[2]);
let header_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(header_b64)
.map_err(|_| "Invalid JWT header".to_string())?;
let header_str =
String::from_utf8(header_bytes).map_err(|_| "Invalid JWT header encoding".to_string())?;
let header_json: serde_json::Value =
serde_json::from_str(&header_str).map_err(|_| "Invalid JWT header encoding".to_string())?;
if header_json.get("alg").and_then(|v| v.as_str()) != Some("HS256")
|| header_json.get("typ").and_then(|v| v.as_str()) != Some("JWT")
{
return Err("Unsupported JWT algorithm or type".to_string());
}
let signing_input = format!("{}.{}", header_b64, claims_b64);
let expected_signature = sign_hmac_sha256(&signing_input, &key.0)?;
use subtle::ConstantTimeEq;
if expected_signature
.as_bytes()
.ct_eq(signature_b64.as_bytes())
.unwrap_u8()
!= 1
{
return Err("Invalid JWT signature".to_string());
}
let claims_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(claims_b64)
.map_err(|_| "Invalid JWT claims".to_string())?;
let claims: T = serde_json::from_slice(&claims_bytes)
.map_err(|_| "Invalid JWT claims format".to_string())?;
Ok(TokenData {
header: Header::default(),
claims,
})
}
pub(crate) fn sign_hmac_sha256(data: &str, secret: &[u8]) -> Result<String, String> {
use hmac::Hmac;
use sha2::Sha256;
let mut mac =
Hmac::<Sha256>::new_from_slice(secret).map_err(|e| format!("HMAC init failed: {}", e))?;
mac.update(data.as_bytes());
let result = mac.finalize();
Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(result.into_bytes()))
}