ironshield_types/
response.rs1use serde::{
2 Deserialize,
3 Serialize
4};
5
6use crate::IronShieldChallenge;
7
8#[cfg(feature = "openapi")]
13#[allow(unused_imports)]
14use serde_json::json;
15
16#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
17#[cfg_attr(feature = "openapi", schema(
18 description = "IronShield challenge response containing the solved challenge and proof-of-work solution"
19))]
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct IronShieldChallengeResponse {
22 pub solved_challenge: IronShieldChallenge,
24 #[cfg_attr(feature = "openapi", schema(example = 187453i64))]
26 pub solution: i64,
27}
28
29impl IronShieldChallengeResponse {
30 pub fn new(
40 solved_challenge: IronShieldChallenge,
41 solution: i64
42 ) -> Self {
43 Self {
44 solved_challenge,
45 solution,
46 }
47 }
48
49 pub fn concat_struct(&self) -> String {
55 format!(
56 "{}|{}",
57 self.solved_challenge.concat_struct(),
58 self.solution
59 )
60 }
61
62 pub fn from_concat_struct(concat_string: &str) -> Result<Self, String> {
77 let last_pipe_pos = concat_string.rfind('|')
79 .ok_or("Expected at least one '|' separator")?;
80
81 let challenge_part = &concat_string[..last_pipe_pos];
82 let solution_part = &concat_string[last_pipe_pos + 1..];
83
84 let solved_challenge = IronShieldChallenge::from_concat_struct(challenge_part)?;
85 let solution = solution_part.parse::<i64>()
86 .map_err(|_| "Failed to parse solution as i64")?;
87
88 Ok(Self {
89 solved_challenge,
90 solution,
91 })
92 }
93
94 pub fn to_base64url_header(&self) -> String {
112 crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
113 }
114
115 pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
139 let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
141
142 Self::from_concat_struct(&concat_str)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::SigningKey;
151
152 #[test]
153 fn test_response_base64url_header_encoding_roundtrip() {
154 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
156 let challenge = IronShieldChallenge::new(
157 "test_website".to_string(),
158 100_000,
159 dummy_key,
160 [0x34; 32],
161 );
162 let response: IronShieldChallengeResponse = IronShieldChallengeResponse::new(challenge, 12345);
163
164 let encoded: String = response.to_base64url_header();
166 let decoded: IronShieldChallengeResponse = IronShieldChallengeResponse::from_base64url_header(&encoded).unwrap();
167
168 assert_eq!(response.solved_challenge.random_nonce, decoded.solved_challenge.random_nonce);
170 assert_eq!(response.solved_challenge.website_id, decoded.solved_challenge.website_id);
171 assert_eq!(response.solved_challenge.challenge_param, decoded.solved_challenge.challenge_param);
172 assert_eq!(response.solved_challenge.public_key, decoded.solved_challenge.public_key);
173 assert_eq!(response.solved_challenge.challenge_signature, decoded.solved_challenge.challenge_signature);
174 assert_eq!(response.solution, decoded.solution);
175 }
176
177 #[test]
178 fn test_response_base64url_header_invalid_data() {
179 let result: Result<IronShieldChallengeResponse, String> = IronShieldChallengeResponse::from_base64url_header("invalid-base64!");
181 assert!(result.is_err());
182 assert!(result.unwrap_err().contains("Base64 decode error"));
183
184 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
186 let invalid_format: String = URL_SAFE_NO_PAD.encode(b"only_one_part");
187 let result: Result<IronShieldChallengeResponse, String> = IronShieldChallengeResponse::from_base64url_header(&invalid_format);
188 assert!(result.is_err());
189 assert!(result.unwrap_err().contains("Expected at least one '|' separator"));
190 }
191
192 #[test]
193 fn test_concat_struct() {
194 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
195 let challenge = IronShieldChallenge::new(
196 "test_website".to_string(),
197 100_000,
198 dummy_key,
199 [0x34; 32],
200 );
201 let response = IronShieldChallengeResponse::new(challenge.clone(), 42);
202 let concat = response.concat_struct();
203 let expected = format!("{}|{}", challenge.concat_struct(), 42);
204 assert_eq!(concat, expected);
205 }
206
207 #[test]
208 fn test_from_concat_struct() {
209 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
210 let challenge = IronShieldChallenge::new(
211 "test_website".to_string(),
212 100_000,
213 dummy_key,
214 [0x34; 32],
215 );
216 let concat = format!("{}|{}", challenge.concat_struct(), 42);
217 let response = IronShieldChallengeResponse::from_concat_struct(&concat).unwrap();
218 assert_eq!(response.solved_challenge.website_id, challenge.website_id);
219 assert_eq!(response.solved_challenge.challenge_param, challenge.challenge_param);
220 assert_eq!(response.solution, 42);
221 }
222
223 #[test]
224 fn test_from_concat_struct_edge_cases() {
225 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
226 let challenge = IronShieldChallenge::new(
227 "test_website".to_string(),
228 1,
229 dummy_key,
230 [0x00; 32],
231 );
232
233 let concat = format!("{}|{}", challenge.concat_struct(), -1);
235 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
236 assert!(result.is_ok());
237 let parsed = result.unwrap();
238 assert_eq!(parsed.solution, -1);
239
240 let concat = format!("{}|{}", challenge.concat_struct(), 0);
242 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
243 assert!(result.is_ok());
244 let parsed = result.unwrap();
245 assert_eq!(parsed.solution, 0);
246
247 let concat = format!("{}|{}", challenge.concat_struct(), i64::MAX);
249 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
250 assert!(result.is_ok());
251 let parsed = result.unwrap();
252 assert_eq!(parsed.solution, i64::MAX);
253 }
254
255 #[test]
256 fn test_from_concat_struct_error_cases() {
257 let result = IronShieldChallengeResponse::from_concat_struct("no_pipe_separator");
259 assert!(result.is_err());
260 assert!(result.unwrap_err().contains("Expected at least one '|' separator"));
261
262 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
264 let challenge = IronShieldChallenge::new(
265 "test_website".to_string(),
266 1,
267 dummy_key,
268 [0x00; 32],
269 );
270 let concat = format!("{}|{}", challenge.concat_struct(), "not_a_number");
271 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
272 assert!(result.is_err());
273 assert!(result.unwrap_err().contains("Failed to parse solution as i64"));
274 }
275}