use chrono::Utc;
use crate::serde_utils::{serialize_signature, deserialize_signature};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IronShieldToken {
#[serde(
serialize_with = "serialize_signature",
deserialize_with = "deserialize_signature"
)]
pub challenge_signature: [u8; 64],
pub valid_for: i64,
pub public_key: [u8; 32],
#[serde(
serialize_with = "serialize_signature",
deserialize_with = "deserialize_signature"
)]
pub auth_signature: [u8; 64],
}
impl IronShieldToken {
pub fn new(
challenge_signature: [u8; 64],
valid_for: i64,
public_key: [u8; 32],
auth_signature: [u8; 64],
) -> Self {
Self {
challenge_signature,
valid_for,
public_key,
auth_signature,
}
}
pub fn is_expired(&self) -> bool {
Utc::now().timestamp_millis() > self.valid_for
}
pub fn concat_struct(&self) -> String {
format!(
"{}|{}|{}|{}",
hex::encode(self.challenge_signature),
self.valid_for,
hex::encode(self.public_key),
hex::encode(self.auth_signature)
)
}
pub fn from_concat_struct(concat_str: &str) -> Result<Self, String> {
let parts: Vec<&str> = concat_str.split('|').collect();
if parts.len() != 4 {
return Err(format!("Expected 4 parts, got {}", parts.len()));
}
let challenge_signature_bytes = hex::decode(parts[0])
.map_err(|_| "Failed to decode challenge_signature hex string")?;
let challenge_signature: [u8; 64] = challenge_signature_bytes.try_into()
.map_err(|_| "Challenge signature must be exactly 64 bytes")?;
let valid_for = parts[1].parse::<i64>()
.map_err(|_| "Failed to parse valid_for as i64")?;
let public_key_bytes = hex::decode(parts[2])
.map_err(|_| "Failed to decode public_key hex string")?;
let public_key: [u8; 32] = public_key_bytes.try_into()
.map_err(|_| "Public key must be exactly 32 bytes")?;
let auth_signature_bytes = hex::decode(parts[3])
.map_err(|_| "Failed to decode authentication_signature hex string")?;
let authentication_signature: [u8; 64] = auth_signature_bytes.try_into()
.map_err(|_| "Authentication signature must be exactly 64 bytes")?;
Ok(Self {
challenge_signature,
valid_for,
public_key,
auth_signature: authentication_signature,
})
}
pub fn to_base64url_header(&self) -> String {
crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
}
pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
Self::from_concat_struct(&concat_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_concat_struct_edge_cases() {
let valid_32_byte_hex = "0".repeat(64); let valid_64_byte_hex = "0".repeat(128); assert_eq!(valid_32_byte_hex.len(), 64, "32-byte hex string should be exactly 64 characters");
assert_eq!(valid_64_byte_hex.len(), 128, "64-byte hex string should be exactly 128 characters");
let input = format!("{}|1000000|{}|{}",
valid_64_byte_hex, valid_32_byte_hex, valid_64_byte_hex);
let result = IronShieldToken::from_concat_struct(&input);
if result.is_err() {
panic!("Expected success but got error: {}", result.unwrap_err());
}
let parsed = result.unwrap();
assert_eq!(parsed.challenge_signature, [0u8; 64]);
assert_eq!(parsed.valid_for, 1000000);
assert_eq!(parsed.public_key, [0u8; 32]);
assert_eq!(parsed.auth_signature, [0u8; 64]);
let all_f_32_hex = "f".repeat(64); let all_f_64_hex = "f".repeat(128); assert_eq!(all_f_32_hex.len(), 64, "All F's 32-byte hex string should be exactly 64 characters");
assert_eq!(all_f_64_hex.len(), 128, "All F's 64-byte hex string should be exactly 128 characters");
let input = format!("{}|9999999|{}|{}",
all_f_64_hex, all_f_32_hex, all_f_64_hex);
let result = IronShieldToken::from_concat_struct(&input);
if result.is_err() {
panic!("Expected success but got error: {}", result.unwrap_err());
}
let parsed = result.unwrap();
assert_eq!(parsed.challenge_signature, [0xffu8; 64]);
assert_eq!(parsed.valid_for, 9999999);
assert_eq!(parsed.public_key, [0xffu8; 32]);
assert_eq!(parsed.auth_signature, [0xffu8; 64]);
}
#[test]
fn test_concat_struct_roundtrip() {
let original_token = IronShieldToken::new(
[0xAB; 64],
1700000000000,
[0xCD; 32],
[0xEF; 64],
);
let concat_str = original_token.concat_struct();
let parsed_token = IronShieldToken::from_concat_struct(&concat_str).unwrap();
assert_eq!(original_token.challenge_signature, parsed_token.challenge_signature);
assert_eq!(original_token.valid_for, parsed_token.valid_for);
assert_eq!(original_token.public_key, parsed_token.public_key);
assert_eq!(original_token.auth_signature, parsed_token.auth_signature);
}
#[test]
fn test_empty_string_parsing() {
let result = IronShieldToken::from_concat_struct("");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 4 parts, got 1"));
}
#[test]
fn test_from_concat_struct_error_cases() {
let result = IronShieldToken::from_concat_struct("only|two|parts");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 4 parts, got 3"));
let result = IronShieldToken::from_concat_struct("too|many|parts|here|extra");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 4 parts, got 5"));
let valid_32_hex = "0".repeat(64);
let valid_64_hex = "0".repeat(128);
let invalid_hex = "invalid_hex_string";
let input = format!("{}|1000000|{}|{}", invalid_hex, valid_32_hex, valid_64_hex);
let result = IronShieldToken::from_concat_struct(&input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to decode challenge_signature hex string"));
let input = format!("{}|1000000|{}|{}", valid_64_hex, invalid_hex, valid_64_hex);
let result = IronShieldToken::from_concat_struct(&input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to decode public_key hex string"));
let input = format!("{}|1000000|{}|{}", valid_64_hex, valid_32_hex, invalid_hex);
let result = IronShieldToken::from_concat_struct(&input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to decode authentication_signature hex string"));
let input = format!("{}|not_a_number|{}|{}", valid_64_hex, valid_32_hex, valid_64_hex);
let result = IronShieldToken::from_concat_struct(&input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to parse valid_for as i64"));
let short_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", short_hex, valid_32_hex, valid_64_hex);
let result = IronShieldToken::from_concat_struct(&input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Challenge signature must be exactly 64 bytes"));
let short_32_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", valid_64_hex, short_32_hex, valid_64_hex);
let result = IronShieldToken::from_concat_struct(&input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Public key must be exactly 32 bytes"));
}
}