1use chrono::Utc;
2use serde::{
3 Deserialize,
4 Serialize
5};
6
7use crate::serde_utils::{
8 serialize_signature,
9 deserialize_signature
10};
11
12#[cfg(feature = "openapi")]
21#[allow(unused_imports)]
22use serde_json::json;
23
24#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
25#[cfg_attr(feature = "openapi", schema(
26 description = "IronShield authentication token containing challenge signature and validity information"
27))]
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct IronShieldToken {
30 #[serde(
32 serialize_with = "serialize_signature",
33 deserialize_with = "deserialize_signature"
34 )]
35 #[cfg_attr(feature = "openapi", schema(example = json!([98, 41, 139, 179, 132, 76, 72, 255, 157, 174, 50, 115, 247, 136, 169, 81, 207, 103, 221, 56, 94, 132, 116, 223, 79, 98, 252, 141, 170, 30, 149, 30, 97, 132, 148, 134, 199, 198, 122, 254, 103, 224, 178, 167, 177, 23, 99, 146, 0, 107, 22, 102, 124, 10, 38, 38, 2, 227, 218, 87, 204, 135, 44, 10])))]
36 pub challenge_signature: [u8; 64],
37 #[cfg_attr(feature = "openapi", schema(example = 1755404945880i64))]
39 pub valid_for: i64,
40 #[cfg_attr(feature = "openapi", schema(example = json!([71, 15, 1, 1, 7, 64, 28, 152, 78, 88, 44, 175, 57, 103, 175, 203, 107, 65, 139, 247, 54, 246, 169, 209, 116, 166, 25, 71, 174, 193, 66, 191])))]
42 pub public_key: [u8; 32],
43 #[serde(
45 serialize_with = "serialize_signature",
46 deserialize_with = "deserialize_signature"
47 )]
48 #[cfg_attr(feature = "openapi", schema(example = json!([156, 23, 45, 67, 89, 123, 210, 98, 76, 54, 32, 187, 145, 67, 89, 210, 123, 45, 67, 89, 210, 98, 76, 54, 32, 187, 145, 67, 89, 210, 123, 45, 67, 89, 210, 98, 76, 54, 32, 187, 145, 67, 89, 210, 123, 45, 67, 89, 210, 98, 76, 54, 32, 187, 145, 67, 89, 210, 123, 45, 67, 89, 210, 98])))]
49 pub auth_signature: [u8; 64],
50}
51
52impl IronShieldToken {
53 pub fn new(
54 challenge_signature: [u8; 64],
55 valid_for: i64,
56 public_key: [u8; 32],
57 auth_signature: [u8; 64],
58 ) -> Self {
59 Self {
60 challenge_signature,
61 valid_for,
62 public_key,
63 auth_signature,
64 }
65 }
66
67 pub fn is_expired(&self) -> bool {
70 Utc::now().timestamp_millis() > self.valid_for
71 }
72
73 pub fn concat_struct(&self) -> String {
81 format!(
82 "{}|{}|{}|{}",
83 hex::encode(self.challenge_signature),
87 self.valid_for,
88 hex::encode(self.public_key),
89 hex::encode(self.auth_signature)
90 )
91 }
92
93 pub fn from_concat_struct(concat_str: &str) -> Result<Self, String> {
110 let parts: Vec<&str> = concat_str.split('|').collect();
111
112 if parts.len() != 4 {
113 return Err(format!("Expected 4 parts, got {}", parts.len()));
114 }
115
116 let challenge_signature_bytes = hex::decode(parts[0])
117 .map_err(|_| "Failed to decode challenge_signature hex string")?;
118 let challenge_signature: [u8; 64] = challenge_signature_bytes.try_into()
119 .map_err(|_| "Challenge signature must be exactly 64 bytes")?;
120
121 let valid_for = parts[1].parse::<i64>()
122 .map_err(|_| "Failed to parse valid_for as i64")?;
123
124 let public_key_bytes = hex::decode(parts[2])
125 .map_err(|_| "Failed to decode public_key hex string")?;
126 let public_key: [u8; 32] = public_key_bytes.try_into()
127 .map_err(|_| "Public key must be exactly 32 bytes")?;
128
129 let auth_signature_bytes = hex::decode(parts[3])
130 .map_err(|_| "Failed to decode authentication_signature hex string")?;
131 let authentication_signature: [u8; 64] = auth_signature_bytes.try_into()
132 .map_err(|_| "Authentication signature must be exactly 64 bytes")?;
133
134 Ok(Self {
135 challenge_signature,
136 valid_for,
137 public_key,
138 auth_signature: authentication_signature,
139 })
140 }
141
142 pub fn to_base64url_header(&self) -> String {
158 crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
159 }
160
161 pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
183 let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
185
186 Self::from_concat_struct(&concat_str)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_from_concat_struct_edge_cases() {
197 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");
202 assert_eq!(valid_64_byte_hex.len(), 128, "64-byte hex string should be exactly 128 characters");
203
204 let input = format!("{}|1000000|{}|{}",
205 valid_64_byte_hex, valid_32_byte_hex, valid_64_byte_hex);
206 let result = IronShieldToken::from_concat_struct(&input);
207
208 if result.is_err() {
209 panic!("Expected success but got error: {}", result.unwrap_err());
210 }
211
212 let parsed = result.unwrap();
213 assert_eq!(parsed.challenge_signature, [0u8; 64]);
214 assert_eq!(parsed.valid_for, 1000000);
215 assert_eq!(parsed.public_key, [0u8; 32]);
216 assert_eq!(parsed.auth_signature, [0u8; 64]);
217
218 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");
222 assert_eq!(all_f_64_hex.len(), 128, "All F's 64-byte hex string should be exactly 128 characters");
223
224 let input = format!("{}|9999999|{}|{}",
225 all_f_64_hex, all_f_32_hex, all_f_64_hex);
226 let result = IronShieldToken::from_concat_struct(&input);
227
228 if result.is_err() {
229 panic!("Expected success but got error: {}", result.unwrap_err());
230 }
231
232 let parsed = result.unwrap();
233 assert_eq!(parsed.challenge_signature, [0xffu8; 64]);
234 assert_eq!(parsed.valid_for, 9999999);
235 assert_eq!(parsed.public_key, [0xffu8; 32]);
236 assert_eq!(parsed.auth_signature, [0xffu8; 64]);
237 }
238
239 #[test]
240 fn test_concat_struct_roundtrip() {
241 let original_token = IronShieldToken::new(
243 [0xAB; 64],
244 1700000000000,
245 [0xCD; 32],
246 [0xEF; 64],
247 );
248
249 let concat_str = original_token.concat_struct();
251 let parsed_token = IronShieldToken::from_concat_struct(&concat_str).unwrap();
252
253 assert_eq!(original_token.challenge_signature, parsed_token.challenge_signature);
255 assert_eq!(original_token.valid_for, parsed_token.valid_for);
256 assert_eq!(original_token.public_key, parsed_token.public_key);
257 assert_eq!(original_token.auth_signature, parsed_token.auth_signature);
258 }
259
260 #[test]
261 fn test_empty_string_parsing() {
262 let result = IronShieldToken::from_concat_struct("");
263 assert!(result.is_err());
264 assert!(result.unwrap_err().contains("Expected 4 parts, got 1"));
265 }
266
267
268 #[test]
269 fn test_from_concat_struct_error_cases() {
270 let result = IronShieldToken::from_concat_struct("only|two|parts");
272 assert!(result.is_err());
273 assert!(result.unwrap_err().contains("Expected 4 parts, got 3"));
274
275 let result = IronShieldToken::from_concat_struct("too|many|parts|here|extra");
276 assert!(result.is_err());
277 assert!(result.unwrap_err().contains("Expected 4 parts, got 5"));
278
279 let valid_32_hex = "0".repeat(64);
281 let valid_64_hex = "0".repeat(128);
282 let invalid_hex = "invalid_hex_string";
283
284 let input = format!("{}|1000000|{}|{}", invalid_hex, valid_32_hex, valid_64_hex);
285 let result = IronShieldToken::from_concat_struct(&input);
286 assert!(result.is_err());
287 assert!(result.unwrap_err().contains("Failed to decode challenge_signature hex string"));
288
289 let input = format!("{}|1000000|{}|{}", valid_64_hex, invalid_hex, valid_64_hex);
291 let result = IronShieldToken::from_concat_struct(&input);
292 assert!(result.is_err());
293 assert!(result.unwrap_err().contains("Failed to decode public_key hex string"));
294
295 let input = format!("{}|1000000|{}|{}", valid_64_hex, valid_32_hex, invalid_hex);
297 let result = IronShieldToken::from_concat_struct(&input);
298 assert!(result.is_err());
299 assert!(result.unwrap_err().contains("Failed to decode authentication_signature hex string"));
300
301 let input = format!("{}|not_a_number|{}|{}", valid_64_hex, valid_32_hex, valid_64_hex);
303 let result = IronShieldToken::from_concat_struct(&input);
304 assert!(result.is_err());
305 assert!(result.unwrap_err().contains("Failed to parse valid_for as i64"));
306
307 let short_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", short_hex, valid_32_hex, valid_64_hex);
310 let result = IronShieldToken::from_concat_struct(&input);
311 assert!(result.is_err());
312 assert!(result.unwrap_err().contains("Challenge signature must be exactly 64 bytes"));
313
314 let short_32_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", valid_64_hex, short_32_hex, valid_64_hex);
316 let result = IronShieldToken::from_concat_struct(&input);
317 assert!(result.is_err());
318 assert!(result.unwrap_err().contains("Public key must be exactly 32 bytes"));
319 }
320}