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