ironshield_types/
token.rs1use chrono::Utc;
2use crate::serde_utils::{serialize_signature, deserialize_signature};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct IronShieldToken {
15 #[serde(
16 serialize_with = "serialize_signature",
17 deserialize_with = "deserialize_signature"
18 )]
19 pub challenge_signature: [u8; 64],
20 pub valid_for: i64,
21 pub public_key: [u8; 32],
22 #[serde(
23 serialize_with = "serialize_signature",
24 deserialize_with = "deserialize_signature"
25 )]
26 pub auth_signature: [u8; 64],
27}
28
29impl IronShieldToken {
30 pub fn new(
31 challenge_signature: [u8; 64],
32 valid_for: i64,
33 public_key: [u8; 32],
34 auth_signature: [u8; 64],
35 ) -> Self {
36 Self {
37 challenge_signature,
38 valid_for,
39 public_key,
40 auth_signature,
41 }
42 }
43
44 pub fn is_expired(&self) -> bool {
47 Utc::now().timestamp_millis() > self.valid_for
48 }
49
50 pub fn concat_struct(&self) -> String {
58 format!(
59 "{}|{}|{}|{}",
60 hex::encode(self.challenge_signature),
64 self.valid_for,
65 hex::encode(self.public_key),
66 hex::encode(self.auth_signature)
67 )
68 }
69
70 pub fn from_concat_struct(concat_str: &str) -> Result<Self, String> {
87 let parts: Vec<&str> = concat_str.split('|').collect();
88
89 if parts.len() != 4 {
90 return Err(format!("Expected 4 parts, got {}", parts.len()));
91 }
92
93 let challenge_signature_bytes = hex::decode(parts[0])
94 .map_err(|_| "Failed to decode challenge_signature hex string")?;
95 let challenge_signature: [u8; 64] = challenge_signature_bytes.try_into()
96 .map_err(|_| "Challenge signature must be exactly 64 bytes")?;
97
98 let valid_for = parts[1].parse::<i64>()
99 .map_err(|_| "Failed to parse valid_for as i64")?;
100
101 let public_key_bytes = hex::decode(parts[2])
102 .map_err(|_| "Failed to decode public_key hex string")?;
103 let public_key: [u8; 32] = public_key_bytes.try_into()
104 .map_err(|_| "Public key must be exactly 32 bytes")?;
105
106 let auth_signature_bytes = hex::decode(parts[3])
107 .map_err(|_| "Failed to decode authentication_signature hex string")?;
108 let authentication_signature: [u8; 64] = auth_signature_bytes.try_into()
109 .map_err(|_| "Authentication signature must be exactly 64 bytes")?;
110
111 Ok(Self {
112 challenge_signature,
113 valid_for,
114 public_key,
115 auth_signature: authentication_signature,
116 })
117 }
118
119 pub fn to_base64url_header(&self) -> String {
135 crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
136 }
137
138 pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
160 let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
162
163 Self::from_concat_struct(&concat_str)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_from_concat_struct_edge_cases() {
174 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");
179 assert_eq!(valid_64_byte_hex.len(), 128, "64-byte hex string should be exactly 128 characters");
180
181 let input = format!("{}|1000000|{}|{}",
182 valid_64_byte_hex, valid_32_byte_hex, valid_64_byte_hex);
183 let result = IronShieldToken::from_concat_struct(&input);
184
185 if result.is_err() {
186 panic!("Expected success but got error: {}", result.unwrap_err());
187 }
188
189 let parsed = result.unwrap();
190 assert_eq!(parsed.challenge_signature, [0u8; 64]);
191 assert_eq!(parsed.valid_for, 1000000);
192 assert_eq!(parsed.public_key, [0u8; 32]);
193 assert_eq!(parsed.auth_signature, [0u8; 64]);
194
195 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");
199 assert_eq!(all_f_64_hex.len(), 128, "All F's 64-byte hex string should be exactly 128 characters");
200
201 let input = format!("{}|9999999|{}|{}",
202 all_f_64_hex, all_f_32_hex, all_f_64_hex);
203 let result = IronShieldToken::from_concat_struct(&input);
204
205 if result.is_err() {
206 panic!("Expected success but got error: {}", result.unwrap_err());
207 }
208
209 let parsed = result.unwrap();
210 assert_eq!(parsed.challenge_signature, [0xffu8; 64]);
211 assert_eq!(parsed.valid_for, 9999999);
212 assert_eq!(parsed.public_key, [0xffu8; 32]);
213 assert_eq!(parsed.auth_signature, [0xffu8; 64]);
214 }
215
216 #[test]
217 fn test_concat_struct_roundtrip() {
218 let original_token = IronShieldToken::new(
220 [0xAB; 64],
221 1700000000000,
222 [0xCD; 32],
223 [0xEF; 64],
224 );
225
226 let concat_str = original_token.concat_struct();
228 let parsed_token = IronShieldToken::from_concat_struct(&concat_str).unwrap();
229
230 assert_eq!(original_token.challenge_signature, parsed_token.challenge_signature);
232 assert_eq!(original_token.valid_for, parsed_token.valid_for);
233 assert_eq!(original_token.public_key, parsed_token.public_key);
234 assert_eq!(original_token.auth_signature, parsed_token.auth_signature);
235 }
236
237 #[test]
238 fn test_empty_string_parsing() {
239 let result = IronShieldToken::from_concat_struct("");
240 assert!(result.is_err());
241 assert!(result.unwrap_err().contains("Expected 4 parts, got 1"));
242 }
243
244
245 #[test]
246 fn test_from_concat_struct_error_cases() {
247 let result = IronShieldToken::from_concat_struct("only|two|parts");
249 assert!(result.is_err());
250 assert!(result.unwrap_err().contains("Expected 4 parts, got 3"));
251
252 let result = IronShieldToken::from_concat_struct("too|many|parts|here|extra");
253 assert!(result.is_err());
254 assert!(result.unwrap_err().contains("Expected 4 parts, got 5"));
255
256 let valid_32_hex = "0".repeat(64);
258 let valid_64_hex = "0".repeat(128);
259 let invalid_hex = "invalid_hex_string";
260
261 let input = format!("{}|1000000|{}|{}", invalid_hex, valid_32_hex, valid_64_hex);
262 let result = IronShieldToken::from_concat_struct(&input);
263 assert!(result.is_err());
264 assert!(result.unwrap_err().contains("Failed to decode challenge_signature hex string"));
265
266 let input = format!("{}|1000000|{}|{}", valid_64_hex, invalid_hex, valid_64_hex);
268 let result = IronShieldToken::from_concat_struct(&input);
269 assert!(result.is_err());
270 assert!(result.unwrap_err().contains("Failed to decode public_key hex string"));
271
272 let input = format!("{}|1000000|{}|{}", valid_64_hex, valid_32_hex, invalid_hex);
274 let result = IronShieldToken::from_concat_struct(&input);
275 assert!(result.is_err());
276 assert!(result.unwrap_err().contains("Failed to decode authentication_signature hex string"));
277
278 let input = format!("{}|not_a_number|{}|{}", valid_64_hex, valid_32_hex, valid_64_hex);
280 let result = IronShieldToken::from_concat_struct(&input);
281 assert!(result.is_err());
282 assert!(result.unwrap_err().contains("Failed to parse valid_for as i64"));
283
284 let short_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", short_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("Challenge signature must be exactly 64 bytes"));
290
291 let short_32_hex = "0".repeat(32); let input = format!("{}|1000000|{}|{}", valid_64_hex, short_32_hex, valid_64_hex);
293 let result = IronShieldToken::from_concat_struct(&input);
294 assert!(result.is_err());
295 assert!(result.unwrap_err().contains("Public key must be exactly 32 bytes"));
296 }
297}