ironshield_types/
token.rs1use chrono::Utc;
2use serde::{
3 Deserialize,
4 Serialize
5};
6
7use crate::serde_utils::{
8 serialize_signature,
9 deserialize_signature
10};
11
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct IronShieldToken {
23 #[serde(
24 serialize_with = "serialize_signature",
25 deserialize_with = "deserialize_signature"
26 )]
27 pub challenge_signature: [u8; 64],
28 pub valid_for: i64,
29 pub public_key: [u8; 32],
30 #[serde(
31 serialize_with = "serialize_signature",
32 deserialize_with = "deserialize_signature"
33 )]
34 pub auth_signature: [u8; 64],
35}
36
37impl IronShieldToken {
38 pub fn new(
39 challenge_signature: [u8; 64],
40 valid_for: i64,
41 public_key: [u8; 32],
42 auth_signature: [u8; 64],
43 ) -> Self {
44 Self {
45 challenge_signature,
46 valid_for,
47 public_key,
48 auth_signature,
49 }
50 }
51
52 pub fn is_expired(&self) -> bool {
55 Utc::now().timestamp_millis() > self.valid_for
56 }
57
58 pub fn concat_struct(&self) -> String {
66 format!(
67 "{}|{}|{}|{}",
68 hex::encode(self.challenge_signature),
72 self.valid_for,
73 hex::encode(self.public_key),
74 hex::encode(self.auth_signature)
75 )
76 }
77
78 pub fn from_concat_struct(concat_str: &str) -> Result<Self, String> {
95 let parts: Vec<&str> = concat_str.split('|').collect();
96
97 if parts.len() != 4 {
98 return Err(format!("Expected 4 parts, got {}", parts.len()));
99 }
100
101 let challenge_signature_bytes = hex::decode(parts[0])
102 .map_err(|_| "Failed to decode challenge_signature hex string")?;
103 let challenge_signature: [u8; 64] = challenge_signature_bytes.try_into()
104 .map_err(|_| "Challenge signature must be exactly 64 bytes")?;
105
106 let valid_for = parts[1].parse::<i64>()
107 .map_err(|_| "Failed to parse valid_for as i64")?;
108
109 let public_key_bytes = hex::decode(parts[2])
110 .map_err(|_| "Failed to decode public_key hex string")?;
111 let public_key: [u8; 32] = public_key_bytes.try_into()
112 .map_err(|_| "Public key must be exactly 32 bytes")?;
113
114 let auth_signature_bytes = hex::decode(parts[3])
115 .map_err(|_| "Failed to decode authentication_signature hex string")?;
116 let authentication_signature: [u8; 64] = auth_signature_bytes.try_into()
117 .map_err(|_| "Authentication signature must be exactly 64 bytes")?;
118
119 Ok(Self {
120 challenge_signature,
121 valid_for,
122 public_key,
123 auth_signature: authentication_signature,
124 })
125 }
126
127 pub fn to_base64url_header(&self) -> String {
143 crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
144 }
145
146 pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
168 let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
170
171 Self::from_concat_struct(&concat_str)
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_from_concat_struct_edge_cases() {
182 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");
187 assert_eq!(valid_64_byte_hex.len(), 128, "64-byte hex string should be exactly 128 characters");
188
189 let input = format!("{}|1000000|{}|{}",
190 valid_64_byte_hex, valid_32_byte_hex, valid_64_byte_hex);
191 let result = IronShieldToken::from_concat_struct(&input);
192
193 if result.is_err() {
194 panic!("Expected success but got error: {}", result.unwrap_err());
195 }
196
197 let parsed = result.unwrap();
198 assert_eq!(parsed.challenge_signature, [0u8; 64]);
199 assert_eq!(parsed.valid_for, 1000000);
200 assert_eq!(parsed.public_key, [0u8; 32]);
201 assert_eq!(parsed.auth_signature, [0u8; 64]);
202
203 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");
207 assert_eq!(all_f_64_hex.len(), 128, "All F's 64-byte hex string should be exactly 128 characters");
208
209 let input = format!("{}|9999999|{}|{}",
210 all_f_64_hex, all_f_32_hex, all_f_64_hex);
211 let result = IronShieldToken::from_concat_struct(&input);
212
213 if result.is_err() {
214 panic!("Expected success but got error: {}", result.unwrap_err());
215 }
216
217 let parsed = result.unwrap();
218 assert_eq!(parsed.challenge_signature, [0xffu8; 64]);
219 assert_eq!(parsed.valid_for, 9999999);
220 assert_eq!(parsed.public_key, [0xffu8; 32]);
221 assert_eq!(parsed.auth_signature, [0xffu8; 64]);
222 }
223
224 #[test]
225 fn test_concat_struct_roundtrip() {
226 let original_token = IronShieldToken::new(
228 [0xAB; 64],
229 1700000000000,
230 [0xCD; 32],
231 [0xEF; 64],
232 );
233
234 let concat_str = original_token.concat_struct();
236 let parsed_token = IronShieldToken::from_concat_struct(&concat_str).unwrap();
237
238 assert_eq!(original_token.challenge_signature, parsed_token.challenge_signature);
240 assert_eq!(original_token.valid_for, parsed_token.valid_for);
241 assert_eq!(original_token.public_key, parsed_token.public_key);
242 assert_eq!(original_token.auth_signature, parsed_token.auth_signature);
243 }
244
245 #[test]
246 fn test_empty_string_parsing() {
247 let result = IronShieldToken::from_concat_struct("");
248 assert!(result.is_err());
249 assert!(result.unwrap_err().contains("Expected 4 parts, got 1"));
250 }
251
252
253 #[test]
254 fn test_from_concat_struct_error_cases() {
255 let result = IronShieldToken::from_concat_struct("only|two|parts");
257 assert!(result.is_err());
258 assert!(result.unwrap_err().contains("Expected 4 parts, got 3"));
259
260 let result = IronShieldToken::from_concat_struct("too|many|parts|here|extra");
261 assert!(result.is_err());
262 assert!(result.unwrap_err().contains("Expected 4 parts, got 5"));
263
264 let valid_32_hex = "0".repeat(64);
266 let valid_64_hex = "0".repeat(128);
267 let invalid_hex = "invalid_hex_string";
268
269 let input = format!("{}|1000000|{}|{}", invalid_hex, valid_32_hex, valid_64_hex);
270 let result = IronShieldToken::from_concat_struct(&input);
271 assert!(result.is_err());
272 assert!(result.unwrap_err().contains("Failed to decode challenge_signature hex string"));
273
274 let input = format!("{}|1000000|{}|{}", valid_64_hex, invalid_hex, valid_64_hex);
276 let result = IronShieldToken::from_concat_struct(&input);
277 assert!(result.is_err());
278 assert!(result.unwrap_err().contains("Failed to decode public_key hex string"));
279
280 let input = format!("{}|1000000|{}|{}", valid_64_hex, valid_32_hex, invalid_hex);
282 let result = IronShieldToken::from_concat_struct(&input);
283 assert!(result.is_err());
284 assert!(result.unwrap_err().contains("Failed to decode authentication_signature hex string"));
285
286 let input = format!("{}|not_a_number|{}|{}", valid_64_hex, valid_32_hex, valid_64_hex);
288 let result = IronShieldToken::from_concat_struct(&input);
289 assert!(result.is_err());
290 assert!(result.unwrap_err().contains("Failed to parse valid_for as i64"));
291
292 let short_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", short_hex, valid_32_hex, valid_64_hex);
295 let result = IronShieldToken::from_concat_struct(&input);
296 assert!(result.is_err());
297 assert!(result.unwrap_err().contains("Challenge signature must be exactly 64 bytes"));
298
299 let short_32_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", valid_64_hex, short_32_hex, valid_64_hex);
301 let result = IronShieldToken::from_concat_struct(&input);
302 assert!(result.is_err());
303 assert!(result.unwrap_err().contains("Public key must be exactly 32 bytes"));
304 }
305}