1use crate::serde_utils::{
2 deserialize_32_bytes,
3 deserialize_signature,
4 serialize_32_bytes,
5 serialize_signature
6};
7
8use chrono::Utc;
9use ed25519_dalek::SigningKey;
10use hex;
11use rand;
12use serde::{
13 Deserialize,
14 Serialize
15};
16
17pub const CHALLENGE_DIFFICULTY: u64 = 200_000_000u64;
18
19const HASH_BITS: usize = 256;
20const ARRAY_SIZE: usize = 32;
21const BITS_PER_BYTE: usize = 8;
22const BITS_PER_BYTE_MASK: usize = 7;
23const MAX_BYTE_VALUE: u8 = 0xFF;
24const MAX_BIT_POSITION: usize = 255;
25const LSB_INDEX: usize = ARRAY_SIZE - 1;
26const LSB_VALUE: u8 = 1;
27
28#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct IronShieldChallenge {
41 pub random_nonce: String,
42 pub created_time: i64,
43 pub expiration_time: i64,
44 pub website_id: String,
45 #[serde(
46 serialize_with = "serialize_32_bytes",
47 deserialize_with = "deserialize_32_bytes"
48 )]
49 pub challenge_param: [u8; 32],
50 pub recommended_attempts: u64,
51 #[serde(
52 serialize_with = "serialize_32_bytes",
53 deserialize_with = "deserialize_32_bytes"
54 )]
55 pub public_key: [u8; 32],
56 #[serde(
57 serialize_with = "serialize_signature",
58 deserialize_with = "deserialize_signature"
59 )]
60 pub challenge_signature: [u8; 64],
61}
62
63impl IronShieldChallenge {
64 pub fn new(
79 website_id: String,
80 difficulty: u64,
81 private_key: SigningKey,
82 public_key: [u8; 32],
83 ) -> Self {
84 let random_nonce: String = Self::generate_random_nonce();
85 let created_time: i64 = Self::generate_created_time();
86 let expiration_time: i64 = created_time + 30_000; let challenge_param: [u8; 32] = Self::difficulty_to_challenge_param(difficulty);
88
89 let signing_message = crate::crypto::create_signing_message(
91 &random_nonce,
92 created_time,
93 expiration_time,
94 &website_id,
95 &challenge_param,
96 &public_key
97 );
98
99 let challenge_signature: [u8; 64] = crate::crypto::generate_signature(&private_key, &signing_message)
101 .unwrap_or([0u8; 64]);
102
103 Self {
104 random_nonce,
105 created_time,
106 website_id,
107 expiration_time,
108 challenge_param,
109 recommended_attempts: Self::recommended_attempts(difficulty),
110 public_key,
111 challenge_signature,
112 }
113 }
114
115 pub fn difficulty_to_challenge_param(difficulty: u64) -> [u8; 32] {
140 if difficulty == 0 {
141 panic!("Difficulty cannot be zero.")
142 }
143
144 if difficulty == 1 {
145 return [MAX_BYTE_VALUE; ARRAY_SIZE];
146 }
147
148 let log2_difficulty: f64 = (difficulty as f64).log2();
152 let target_exponent: f64 = HASH_BITS as f64 - log2_difficulty;
153
154 if target_exponent <= 0.0 { return Self::create_minimal_challenge_param()
156 }
157
158 if target_exponent >= HASH_BITS as f64 {
159 return [MAX_BYTE_VALUE; ARRAY_SIZE];
160 }
161
162 let bit_position: usize = target_exponent.round() as usize;
164
165 if bit_position >= HASH_BITS {
166 return [MAX_BYTE_VALUE; ARRAY_SIZE];
167 }
168
169 Self::create_challenge_param_with_bit_set(bit_position)
170 }
171
172 fn create_minimal_challenge_param() -> [u8; 32] {
179 let mut result: [u8; 32] = [0u8; ARRAY_SIZE];
180 result[LSB_INDEX] = LSB_VALUE;
181 result
182 }
183
184 fn create_challenge_param_with_bit_set(
197 bit_position: usize
198 ) -> [u8; 32] {
199 let mut result: [u8; 32] = [0u8; ARRAY_SIZE];
200
201 let byte_index: usize = (MAX_BIT_POSITION - bit_position) / BITS_PER_BYTE;
203 let bit_index: usize = BITS_PER_BYTE_MASK - ((MAX_BIT_POSITION - bit_position) % BITS_PER_BYTE);
204
205 if byte_index < ARRAY_SIZE {
206 result[byte_index] = 1u8 << bit_index;
207 } else { return Self::create_minimal_challenge_param()
209 }
210
211 result
212 }
213
214 pub fn is_expired(&self) -> bool {
218 Utc::now().timestamp_millis() > self.expiration_time
219 }
220
221 pub fn time_until_expiration(&self) -> i64 {
224 self.expiration_time - Utc::now().timestamp_millis()
225 }
226
227 pub fn generate_created_time() -> i64 {
230 Utc::now().timestamp_millis()
231 }
232
233 pub fn generate_random_nonce() -> String {
236 hex::encode(&rand::random::<[u8; 16]>())
237 }
238
239 pub fn recommended_attempts(difficulty: u64) -> u64 {
255 difficulty.saturating_mul(2)
256 }
257
258 pub fn concat_struct(&self) -> String {
268 format!(
269 "{}|{}|{}|{}|{}|{}|{}|{}",
270 self.random_nonce,
271 self.created_time,
272 self.expiration_time,
273 self.website_id,
274 hex::encode(self.challenge_param),
276 self.recommended_attempts,
277 hex::encode(self.public_key),
278 hex::encode(self.challenge_signature)
279 )
280 }
281
282 pub fn from_concat_struct(concat_str: &str) -> Result<Self, String> {
300 let parts: Vec<&str> = concat_str.split('|').collect();
301
302 if parts.len() != 8 {
303 return Err(format!("Expected 8 parts, got {}", parts.len()));
304 }
305
306 let random_nonce: String = parts[0].to_string();
307
308 let created_time: i64 = parts[1].parse::<i64>()
309 .map_err(|_| "Failed to parse created_time as i64")?;
310
311 let expiration_time: i64 = parts[2].parse::<i64>()
312 .map_err(|_| "Failed to parse expiration_time as i64")?;
313
314 let website_id: String = parts[3].to_string();
315
316 let challenge_param_bytes: Vec<u8> = hex::decode(parts[4])
317 .map_err(|_| "Failed to decode challenge_params hex string")?;
318 let challenge_param: [u8; 32] = challenge_param_bytes
319 .try_into()
320 .map_err(|_| "Challenge params must be exactly 32 bytes")?;
321
322 let recommended_attempts: u64 = parts[5].parse::<u64>()
323 .map_err(|_| "Failed to parse recommended_attempts as u64")?;
324
325 let public_key_bytes: Vec<u8> = hex::decode(parts[6])
326 .map_err(|_| "Failed to decode public_key hex string")?;
327 let public_key: [u8; 32] = public_key_bytes.try_into()
328 .map_err(|_| "Public key must be exactly 32 bytes")?;
329
330 let signature_bytes: Vec<u8> = hex::decode(parts[7])
331 .map_err(|_| "Failed to decode challenge_signature hex string")?;
332 let challenge_signature: [u8; 64] = signature_bytes
333 .try_into()
334 .map_err(|_| "Signature must be exactly 64 bytes")?;
335
336 Ok(Self {
337 random_nonce,
338 created_time,
339 expiration_time,
340 website_id,
341 challenge_param,
342 recommended_attempts,
343 public_key,
344 challenge_signature,
345 })
346 }
347
348 pub fn to_base64url_header(&self) -> String {
371 crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
372 }
373
374 pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
403 let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
405
406 Self::from_concat_struct(&concat_str)
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_difficulty_to_challenge_param_basic_cases() {
417 let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(1);
419 assert_eq!(challenge_param, [0xFF; 32]);
420
421 let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(2);
423 let expected: [u8; 32] = {
424 let mut arr: [u8; 32] = [0x00; 32];
425 arr[0] = 0x80; arr
427 };
428 assert_eq!(challenge_param, expected);
429
430 let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(4);
431 let expected: [u8; 32] = {
432 let mut arr: [u8; 32] = [0x00; 32];
433 arr[0] = 0x40; arr
435 };
436 assert_eq!(challenge_param, expected);
437
438 let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(256);
439 let expected: [u8; 32] = {
440 let mut arr: [u8; 32] = [0x00; 32];
441 arr[0] = 0x01; arr
443 };
444 assert_eq!(challenge_param, expected);
445 }
446
447 #[test]
448 fn test_difficulty_to_challenge_param_realistic_range() {
449 let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(10_000);
453 assert_eq!(challenge_param[0], 0x00);
455 assert_eq!(challenge_param[1], 0x08); let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(50_000);
459 assert_eq!(challenge_param[0], 0x00);
460 assert_eq!(challenge_param[1], 0x01); let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(100_000);
464 assert_eq!(challenge_param[0], 0x00);
465 assert_eq!(challenge_param[1], 0x00);
466 assert_eq!(challenge_param[2], 0x80); let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(1_000_000);
470 assert_eq!(challenge_param[0], 0x00);
471 assert_eq!(challenge_param[1], 0x00);
472 assert_eq!(challenge_param[2], 0x10); let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(10_000_000);
476 assert_eq!(challenge_param[0], 0x00);
477 assert_eq!(challenge_param[1], 0x00);
478 assert_eq!(challenge_param[2], 0x02); }
480
481 #[test]
482 fn test_difficulty_to_challenge_param_ordering() {
483 let difficulties: [u64; 9] = [1000, 5000, 10_000, 50_000, 100_000, 500_000, 1_000_000, 5_000_000, 10_000_000];
485 let mut challenge_params = Vec::new();
486
487 for &difficulty in &difficulties {
488 challenge_params.push(IronShieldChallenge::difficulty_to_challenge_param(difficulty));
489 }
490
491 for i in 1..challenge_params.len() {
493 assert!(
494 challenge_params[i-1] > challenge_params[i],
495 "Challenge param for difficulty {} should be larger than for difficulty {}",
496 difficulties[i-1], difficulties[i]
497 );
498 }
499 }
500
501 #[test]
502 fn test_difficulty_to_challenge_param_precision() {
503 let base_difficulty: u64 = 100_000;
505 let base_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(base_difficulty);
506
507 let similar_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(100_001);
509
510 assert!(base_param >= similar_param); let much_different_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(200_000);
516 assert!(base_param > much_different_param);
517
518 let big_different_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(400_000);
520 assert!(much_different_param > big_different_param);
521 }
522
523 #[test]
524 fn test_difficulty_to_challenge_param_powers_of_10() {
525 let difficulties: [u64; 6] = [10, 100, 1_000, 10_000, 100_000, 1_000_000];
527
528 for &difficulty in &difficulties {
529 let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(difficulty);
530
531 assert_ne!(challenge_param, [0x00; 32]);
533 assert_ne!(challenge_param, [0xFF; 32]);
534
535 let leading_zero_bytes: usize = challenge_param.iter().take_while(|&&b| b == 0).count();
537 assert!(leading_zero_bytes < 32, "Too many leading zero bytes for difficulty {}", difficulty);
538
539 assert!(leading_zero_bytes < 28, "Challenge param too small for difficulty {}", difficulty);
541 }
542 }
543
544 #[test]
545 fn test_difficulty_to_challenge_param_mathematical_properties() {
546 let d1: u64 = 50_000;
551 let d2: u64 = 100_000; let param1: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(d1);
554 let param2: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(d2);
555
556 let val1: u128 = u128::from_be_bytes(param1[0..16].try_into().unwrap());
558 let val2: u128 = u128::from_be_bytes(param2[0..16].try_into().unwrap());
559
560 let ratio: f64 = val1 as f64 / val2 as f64;
562 assert!(ratio > 1.8 && ratio < 2.2, "Ratio should be close to 2.0, got {}", ratio);
563 }
564
565 #[test]
566 fn test_difficulty_to_challenge_param_edge_cases() {
567 let result = std::panic::catch_unwind(|| {
569 IronShieldChallenge::difficulty_to_challenge_param(0);
570 });
571 assert!(result.is_err());
572
573 let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(u64::MAX);
575 assert_ne!(challenge_param, [0xFF; 32]);
576 assert_ne!(challenge_param, [0; 32]);
577
578 let high_difficulty: u64 = 1u64 << 40; let challenge_param: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(high_difficulty);
581 assert_ne!(challenge_param, [0; 32]);
582 assert_ne!(challenge_param, [0xFF; 32]);
583 }
584
585 #[test]
586 fn test_difficulty_to_challenge_param_consistency() {
587 let test_difficulties: [u64; 13] = [
589 10_000, 25_000, 50_000, 75_000, 100_000,
590 250_000, 500_000, 750_000, 1_000_000,
591 2_500_000, 5_000_000, 7_500_000, 10_000_000
592 ];
593
594 for &difficulty in &test_difficulties {
595 let param1: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(difficulty);
596 let param2: [u8; 32] = IronShieldChallenge::difficulty_to_challenge_param(difficulty);
597 assert_eq!(param1, param2, "Function should be deterministic for difficulty {}", difficulty);
598
599 assert_ne!(param1, [0x00; 32]);
601 assert_ne!(param1, [0xFF; 32]);
602 }
603 }
604
605 #[test]
606 fn test_recommended_attempts() {
607 assert_eq!(IronShieldChallenge::recommended_attempts(1000), 2000);
609 assert_eq!(IronShieldChallenge::recommended_attempts(50000), 100000);
610 assert_eq!(IronShieldChallenge::recommended_attempts(0), 0);
611
612 assert_eq!(IronShieldChallenge::recommended_attempts(u64::MAX), u64::MAX);
614
615 assert_eq!(IronShieldChallenge::recommended_attempts(10_000), 20_000);
617 assert_eq!(IronShieldChallenge::recommended_attempts(1_000_000), 2_000_000);
618 }
619
620 #[test]
621 fn test_base64url_header_encoding_roundtrip() {
622 let private_key = SigningKey::from_bytes(&[0; 32]);
624 let public_key = private_key.verifying_key().to_bytes();
625 let original_challenge = IronShieldChallenge::new(
626 "test-site".to_string(),
627 100_000,
628 private_key,
629 public_key,
630 );
631
632 let encoded = original_challenge.to_base64url_header();
634 let decoded_challenge = IronShieldChallenge::from_base64url_header(&encoded)
635 .expect("Failed to decode header");
636
637 assert_eq!(original_challenge.random_nonce, decoded_challenge.random_nonce);
639 assert_eq!(original_challenge.created_time, decoded_challenge.created_time);
640 assert_eq!(original_challenge.expiration_time, decoded_challenge.expiration_time);
641 assert_eq!(original_challenge.website_id, decoded_challenge.website_id);
642 assert_eq!(original_challenge.challenge_param, decoded_challenge.challenge_param);
643 assert_eq!(original_challenge.public_key, decoded_challenge.public_key);
644 assert_eq!(original_challenge.challenge_signature, decoded_challenge.challenge_signature);
645 }
646
647 #[test]
648 fn test_base64url_header_invalid_data() {
649 let result: Result<IronShieldChallenge, String> = IronShieldChallenge::from_base64url_header("invalid-base64!");
651 assert!(result.is_err());
652 assert!(result.unwrap_err().contains("Base64 decode error"));
653
654 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
656 let invalid_format: String = URL_SAFE_NO_PAD.encode(b"not_enough_parts");
657 let result: Result<IronShieldChallenge, String> = IronShieldChallenge::from_base64url_header(&invalid_format);
658 assert!(result.is_err());
659 assert!(result.unwrap_err().contains("Expected 8 parts"));
660 }
661
662 #[test]
663 fn test_difficulty_range_boundaries() {
664 let min_difficulty = 10_000;
666 let max_difficulty = 10_000_000;
667
668 let min_param = IronShieldChallenge::difficulty_to_challenge_param(min_difficulty);
669 let max_param = IronShieldChallenge::difficulty_to_challenge_param(max_difficulty);
670
671 assert!(min_param > max_param);
673
674 assert_ne!(min_param, [0x00; 32]);
676 assert_ne!(min_param, [0xFF; 32]);
677 assert_ne!(max_param, [0x00; 32]);
678 assert_ne!(max_param, [0xFF; 32]);
679
680 let below_min = IronShieldChallenge::difficulty_to_challenge_param(9_999);
682 let above_max = IronShieldChallenge::difficulty_to_challenge_param(10_000_001);
683
684 assert!(below_min >= min_param); assert!(above_max <= max_param); }
688
689 #[test]
690 fn test_from_concat_struct_edge_cases() {
691 let valid_32_byte_hex = "0000000000000000000000000000000000000000000000000000000000000000";
693 assert_eq!(valid_32_byte_hex.len(), 64, "32-byte hex string should be exactly 64 characters");
694 let valid_64_byte_hex = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
695 assert_eq!(valid_64_byte_hex.len(), 128, "64-byte hex string should be exactly 128 characters");
696
697 let input = format!("test_nonce|1000000|1030000|test_website|{}|0|{}|{}",
698 valid_32_byte_hex, valid_32_byte_hex, valid_64_byte_hex);
699 let result = IronShieldChallenge::from_concat_struct(&input);
700
701 assert!(result.is_ok(), "Should parse valid zero-value data");
702 let parsed = result.unwrap();
703 assert_eq!(parsed.random_nonce, "test_nonce");
704 assert_eq!(parsed.created_time, 1000000);
705 assert_eq!(parsed.expiration_time, 1030000);
706 assert_eq!(parsed.website_id, "test_website");
707 assert_eq!(parsed.challenge_param, [0u8; 32]);
708 assert_eq!(parsed.recommended_attempts, 0);
709 assert_eq!(parsed.public_key, [0u8; 32]);
710 assert_eq!(parsed.challenge_signature, [0u8; 64]);
711
712 let all_f_32_hex = "f".repeat(64);
714 assert_eq!(all_f_32_hex.len(), 64, "All F's 32-byte hex string should be exactly 64 characters");
715 let all_f_64_hex = "f".repeat(128);
716 assert_eq!(all_f_64_hex.len(), 128, "All F's 64-byte hex string should be exactly 128 characters");
717
718 let input = format!("max_nonce|{}|{}|max_website|{}|{}|{}|{}",
719 i64::MAX, i64::MAX, all_f_32_hex, u64::MAX, all_f_32_hex, all_f_64_hex);
720 let result = IronShieldChallenge::from_concat_struct(&input);
721
722 assert!(result.is_ok(), "Should parse valid max-value data");
723 let parsed = result.unwrap();
724 assert_eq!(parsed.random_nonce, "max_nonce");
725 assert_eq!(parsed.created_time, i64::MAX);
726 assert_eq!(parsed.expiration_time, i64::MAX);
727 assert_eq!(parsed.website_id, "max_website");
728 assert_eq!(parsed.challenge_param, [0xffu8; 32]);
729 assert_eq!(parsed.recommended_attempts, u64::MAX);
730 assert_eq!(parsed.public_key, [0xffu8; 32]);
731 assert_eq!(parsed.challenge_signature, [0xffu8; 64]);
732 }
733}