1use crate::anti_tamper::{ClockStatus, HardwareFingerprint, LicenseState};
39use crate::crypto::CryptoRegistry;
40use crate::error::{LicenseError, Result};
41use crate::state_manager::StateManager;
42use chrono::{DateTime, Duration, Utc};
43use hmac::{Hmac, Mac};
44use serde::{Deserialize, Serialize};
45use sha2::{Digest, Sha256};
46use std::path::{Path, PathBuf};
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
54pub enum UnlockType {
55 #[default]
57 ClockReset,
58 ActivationReset,
60 FullReset,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct UnlockChallenge {
67 pub challenge_code: String,
69
70 pub fingerprint_hash: String,
72
73 pub fingerprint: HardwareFingerprint,
75
76 pub timestamp: DateTime<Utc>,
78
79 pub nonce: String,
81
82 pub unlock_type: UnlockType,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct UnlockResult {
89 pub success: bool,
91
92 pub unlock_type: UnlockType,
94
95 pub message: String,
97
98 pub files_reset: Vec<String>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct LockoutStatus {
105 pub is_locked: bool,
107
108 pub lock_reason: Option<String>,
110
111 pub locked_at: Option<DateTime<Utc>>,
113
114 pub clock_status: ClockStatus,
116
117 pub last_validated: Option<DateTime<Utc>>,
119
120 pub validation_count: u64,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
129struct PendingChallenge {
130 nonce: String,
132 timestamp: DateTime<Utc>,
134 unlock_type: UnlockType,
136 fingerprint_hash: String,
138}
139
140type HmacSha256 = Hmac<Sha256>;
141
142impl PendingChallenge {
143 fn save(&self, state_dir: &Path, state_integrity_key: &[u8; 32]) -> Result<()> {
145 let json = serde_json::to_vec(self)
146 .map_err(|e| LicenseError::SerializationError(e.to_string()))?;
147
148 let mut mac = HmacSha256::new_from_slice(state_integrity_key)
149 .map_err(|e| LicenseError::SigningFailed(format!("HMAC init: {}", e)))?;
150 mac.update(&json);
151 let tag = mac.finalize().into_bytes();
152
153 let mut data = Vec::with_capacity(32 + json.len());
155 data.extend_from_slice(&tag);
156 data.extend_from_slice(&json);
157
158 let path = Self::challenge_path(state_dir);
159 std::fs::create_dir_all(state_dir)?;
160 std::fs::write(&path, &data)?;
161
162 Ok(())
163 }
164
165 fn load_and_delete(state_dir: &Path, state_integrity_key: &[u8; 32]) -> Result<Self> {
167 let path = Self::challenge_path(state_dir);
168
169 let data = std::fs::read(&path).map_err(|_| {
170 LicenseError::InvalidResponseCode(
171 "No pending challenge found. Generate a challenge first.".to_string(),
172 )
173 })?;
174
175 let _ = std::fs::remove_file(&path);
177
178 if data.len() < 33 {
179 return Err(LicenseError::InvalidResponseCode(
180 "Corrupted challenge file".to_string(),
181 ));
182 }
183
184 let (tag_bytes, json) = data.split_at(32);
185
186 let mut mac = HmacSha256::new_from_slice(state_integrity_key)
188 .map_err(|e| LicenseError::VerificationFailed(format!("HMAC init: {}", e)))?;
189 mac.update(json);
190 mac.verify_slice(tag_bytes).map_err(|_| {
191 LicenseError::VerificationFailed("Challenge file integrity check failed".to_string())
192 })?;
193
194 serde_json::from_slice(json)
195 .map_err(|e| LicenseError::InvalidResponseCode(format!("Corrupt challenge: {}", e)))
196 }
197
198 fn challenge_path(state_dir: &Path) -> PathBuf {
199 state_dir.join("pending.challenge")
200 }
201}
202
203pub fn generate_challenge_from_state(
222 state_dir: Option<&Path>,
223 _license_id: Option<&str>,
224 unlock_type: UnlockType,
225 state_integrity_key: &[u8; 32],
226) -> Result<UnlockChallenge> {
227 let fingerprint = HardwareFingerprint::generate();
229
230 let nonce = generate_nonce();
232
233 let timestamp = Utc::now();
235
236 let challenge_code = generate_challenge_code(&fingerprint, timestamp, &nonce);
238
239 let pending = PendingChallenge {
241 nonce: nonce.clone(),
242 timestamp,
243 unlock_type,
244 fingerprint_hash: fingerprint.combined_hash.clone(),
245 };
246
247 let dir = state_dir.unwrap_or_else(|| Path::new("."));
248 pending.save(dir, state_integrity_key)?;
249
250 Ok(UnlockChallenge {
251 challenge_code,
252 fingerprint_hash: fingerprint.combined_hash.clone(),
253 fingerprint,
254 timestamp,
255 nonce,
256 unlock_type,
257 })
258}
259
260fn generate_challenge_code(
264 fingerprint: &HardwareFingerprint,
265 timestamp: DateTime<Utc>,
266 nonce: &str,
267) -> String {
268 const ALPHABET: &[u8] = b"23456789ABCDEFGHJKMNPQRSTUVWXYZ";
271
272 let mut hasher = Sha256::new();
274 hasher.update(fingerprint.combined_hash.as_bytes());
275 hasher.update(timestamp.timestamp().to_le_bytes());
276 hasher.update(nonce.as_bytes());
277 let hash = hasher.finalize();
278
279 let mut code = String::with_capacity(12);
281 for i in 0..12 {
282 let idx = (hash[i] as usize) % ALPHABET.len();
283 code.push(ALPHABET[idx] as char);
284 }
285
286 format!(
288 "{}-{}-{}-{}",
289 &code[0..3],
290 &code[3..6],
291 &code[6..9],
292 &code[9..12]
293 )
294}
295
296fn generate_nonce() -> String {
298 use rand::Rng;
299 let bytes: [u8; 32] = rand::thread_rng().gen();
300 hex::encode(bytes)
301}
302
303pub fn validate_response_code(
328 response_bytes: &[u8],
329 state_dir: Option<&Path>,
330 license_id: &str,
331 state_integrity_key: &[u8; 32],
332 public_key_pem: &str,
333 algorithm_id: &str,
334) -> Result<UnlockResult> {
335 if response_bytes.len() < 10 {
337 return Err(LicenseError::InvalidResponseCode(
338 "Response too short".to_string(),
339 ));
340 }
341
342 let timestamp_bytes: [u8; 8] = response_bytes[0..8]
344 .try_into()
345 .map_err(|_| LicenseError::InvalidResponseCode("Invalid timestamp".to_string()))?;
346 let timestamp = i64::from_le_bytes(timestamp_bytes);
347 let response_time = DateTime::from_timestamp(timestamp, 0)
348 .ok_or_else(|| LicenseError::InvalidResponseCode("Invalid timestamp value".to_string()))?;
349
350 let now = Utc::now();
352 let age = now - response_time;
353 if age > Duration::hours(24) {
354 return Ok(UnlockResult {
355 success: false,
356 unlock_type: UnlockType::ClockReset,
357 message: "Response code has expired (older than 24 hours)".to_string(),
358 files_reset: vec![],
359 });
360 }
361
362 let unlock_type = match response_bytes[8] {
364 1 => UnlockType::ClockReset,
365 2 => UnlockType::ActivationReset,
366 3 => UnlockType::FullReset,
367 _ => {
368 return Err(LicenseError::InvalidResponseCode(
369 "Invalid unlock type".to_string(),
370 ))
371 }
372 };
373
374 let signature = &response_bytes[9..];
376
377 let dir = state_dir.unwrap_or_else(|| Path::new("."));
379 let pending = PendingChallenge::load_and_delete(dir, state_integrity_key)?;
380
381 let challenge_age = now - pending.timestamp;
383 if challenge_age > Duration::hours(24) {
384 return Ok(UnlockResult {
385 success: false,
386 unlock_type,
387 message: "Challenge has expired (older than 24 hours). Generate a new one.".to_string(),
388 files_reset: vec![],
389 });
390 }
391
392 let unlock_type_byte = response_bytes[8];
394 let mut hasher = Sha256::new();
395 hasher.update(pending.nonce.as_bytes());
396 hasher.update(timestamp_bytes);
397 hasher.update([unlock_type_byte]);
398 hasher.update(pending.fingerprint_hash.as_bytes());
399 let message = hasher.finalize();
400
401 let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_id)?;
403 algorithm
404 .verify(&message, signature, public_key_pem)
405 .map_err(|_| {
406 LicenseError::InvalidResponseCode("Invalid signature on response code".to_string())
407 })?;
408
409 let files_reset = apply_unlock(state_dir, unlock_type, license_id, state_integrity_key)?;
411
412 Ok(UnlockResult {
413 success: true,
414 unlock_type,
415 message: "Machine successfully unlocked".to_string(),
416 files_reset,
417 })
418}
419
420fn apply_unlock(
422 state_dir: Option<&Path>,
423 unlock_type: UnlockType,
424 license_id: &str,
425 state_integrity_key: &[u8; 32],
426) -> Result<Vec<String>> {
427 let mut files_reset = Vec::new();
428
429 let paths = unlock_state_paths(state_dir, license_id, state_integrity_key);
430
431 match unlock_type {
432 UnlockType::ClockReset => {
433 for path in &paths {
434 if path.exists() {
435 if let Ok(Some(mut state)) =
436 LicenseState::load(path, license_id, state_integrity_key)
437 {
438 state.last_system_time = Utc::now();
439 if state.save(path, state_integrity_key).is_ok() {
440 files_reset.push(path.display().to_string());
441 }
442 }
443 }
444 }
445 }
446 UnlockType::ActivationReset => {
447 for path in &paths {
448 if path.exists() {
449 if let Ok(Some(mut state)) =
450 LicenseState::load(path, license_id, state_integrity_key)
451 {
452 state.last_validated = Utc::now();
453 state.last_system_time = Utc::now();
454 if state.save(path, state_integrity_key).is_ok() {
455 files_reset.push(path.display().to_string());
456 }
457 }
458 }
459 }
460 }
461 UnlockType::FullReset => {
462 for path in &paths {
463 if path.exists() && std::fs::remove_file(path).is_ok() {
464 files_reset.push(path.display().to_string());
465 }
466 }
467 }
468 }
469
470 Ok(files_reset)
471}
472
473fn unlock_state_paths(
474 state_dir: Option<&Path>,
475 license_id: &str,
476 state_integrity_key: &[u8; 32],
477) -> Vec<PathBuf> {
478 if let Some(dir) = state_dir {
479 vec![dir.join("license.state")]
480 } else {
481 StateManager::new(license_id, *state_integrity_key)
482 .paths()
483 .to_vec()
484 }
485}
486
487pub fn get_lockout_status(
496 state_dir: Option<&Path>,
497 license_id: &str,
498 state_integrity_key: &[u8; 32],
499) -> Result<LockoutStatus> {
500 let paths = unlock_state_paths(state_dir, license_id, state_integrity_key);
501
502 let mut is_locked = false;
503 let mut lock_reason = None;
504 let mut locked_at = None;
505 let mut clock_status = ClockStatus::Ok {
506 current: Utc::now(),
507 };
508 let mut last_validated = None;
509 let mut validation_count = 0u64;
510
511 for path in &paths {
513 if path.exists() {
514 if let Ok(Some(state)) = LicenseState::load(path, license_id, state_integrity_key) {
515 if last_validated.is_none() || state.last_validated > last_validated.unwrap() {
516 last_validated = Some(state.last_validated);
517 }
518 validation_count = validation_count.max(state.validation_count);
519
520 if let Ok(status) = state.detect_clock_manipulation(Duration::hours(1)) {
521 match &status {
522 ClockStatus::Backwards { last_seen, .. } => {
523 is_locked = true;
524 lock_reason =
525 Some("Clock tampering detected: time moved backwards".to_string());
526 locked_at = Some(*last_seen);
527 clock_status = status.clone();
528 }
529 ClockStatus::SuspiciousJump { last_seen, .. } => {
530 is_locked = true;
531 lock_reason =
532 Some("Clock tampering suspected: suspicious time jump".to_string());
533 locked_at = Some(*last_seen);
534 clock_status = status.clone();
535 }
536 ClockStatus::Ok { .. } => {
537 if matches!(clock_status, ClockStatus::Ok { .. }) {
538 clock_status = status.clone();
539 }
540 }
541 }
542 }
543 }
544 }
545 }
546
547 Ok(LockoutStatus {
548 is_locked,
549 lock_reason,
550 locked_at,
551 clock_status,
552 last_validated,
553 validation_count,
554 })
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn test_challenge_code_format() {
563 let fingerprint = HardwareFingerprint::default();
564 let timestamp = Utc::now();
565 let nonce = generate_nonce();
566
567 let code = generate_challenge_code(&fingerprint, timestamp, &nonce);
568
569 assert_eq!(code.len(), 15); assert!(code.chars().filter(|c| *c == '-').count() == 3);
572
573 for c in code.chars() {
575 if c != '-' {
576 assert!(c.is_ascii_alphanumeric());
577 }
578 }
579 }
580
581 #[test]
582 fn test_nonce_uniqueness() {
583 let nonce1 = generate_nonce();
584 let nonce2 = generate_nonce();
585
586 assert_ne!(nonce1, nonce2);
587 assert_eq!(nonce1.len(), 64); }
589
590 #[test]
591 fn test_unlock_type_default() {
592 let unlock_type = UnlockType::default();
593 assert_eq!(unlock_type, UnlockType::ClockReset);
594 }
595
596 #[test]
597 fn test_pending_challenge_round_trip() {
598 let dir = tempfile::tempdir().unwrap();
599 let key = [0xABu8; 32];
600
601 let pending = PendingChallenge {
602 nonce: "test-nonce-123".to_string(),
603 timestamp: Utc::now(),
604 unlock_type: UnlockType::ClockReset,
605 fingerprint_hash: "abc123".to_string(),
606 };
607
608 pending.save(dir.path(), &key).unwrap();
609
610 let loaded = PendingChallenge::load_and_delete(dir.path(), &key).unwrap();
611 assert_eq!(loaded.nonce, pending.nonce);
612 assert_eq!(loaded.fingerprint_hash, pending.fingerprint_hash);
613 assert_eq!(loaded.unlock_type, pending.unlock_type);
614
615 assert!(PendingChallenge::load_and_delete(dir.path(), &key).is_err());
617 }
618
619 #[test]
620 fn test_pending_challenge_wrong_key_fails() {
621 let dir = tempfile::tempdir().unwrap();
622 let key1 = [0xABu8; 32];
623 let key2 = [0xCDu8; 32];
624
625 let pending = PendingChallenge {
626 nonce: "test-nonce".to_string(),
627 timestamp: Utc::now(),
628 unlock_type: UnlockType::FullReset,
629 fingerprint_hash: "xyz".to_string(),
630 };
631
632 pending.save(dir.path(), &key1).unwrap();
633
634 assert!(PendingChallenge::load_and_delete(dir.path(), &key2).is_err());
636 }
637
638 #[test]
639 fn test_validate_response_with_real_signature() {
640 use crate::crypto::algorithm_ids;
641 use crate::keys::CryptoKeyPair;
642
643 let dir = tempfile::tempdir().unwrap();
644 let state_key = [0x42u8; 32];
645
646 let challenge = generate_challenge_from_state(
648 Some(dir.path()),
649 Some("TEST-LIC"),
650 UnlockType::ClockReset,
651 &state_key,
652 )
653 .unwrap();
654
655 let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
657
658 let timestamp = Utc::now();
659 let timestamp_bytes = timestamp.timestamp().to_le_bytes();
660 let unlock_type_byte: u8 = 1; let mut hasher = Sha256::new();
664 hasher.update(challenge.nonce.as_bytes());
665 hasher.update(timestamp_bytes);
666 hasher.update([unlock_type_byte]);
667 hasher.update(challenge.fingerprint_hash.as_bytes());
668 let message = hasher.finalize();
669
670 let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_ids::ED25519).unwrap();
671 let signature = algorithm.sign(&message, keypair.private_key_pem()).unwrap();
672
673 let mut response = Vec::new();
675 response.extend_from_slice(×tamp_bytes);
676 response.push(unlock_type_byte);
677 response.extend_from_slice(&signature);
678
679 let result = validate_response_code(
681 &response,
682 Some(dir.path()),
683 "TEST-LIC",
684 &state_key,
685 &keypair.public_key_pem,
686 algorithm_ids::ED25519,
687 )
688 .unwrap();
689
690 assert!(result.success, "Expected success, got: {}", result.message);
691 assert_eq!(result.unlock_type, UnlockType::ClockReset);
692 }
693
694 #[test]
695 fn test_validate_response_wrong_signature_fails() {
696 use crate::crypto::algorithm_ids;
697 use crate::keys::CryptoKeyPair;
698
699 let dir = tempfile::tempdir().unwrap();
700 let state_key = [0x42u8; 32];
701
702 let _challenge = generate_challenge_from_state(
703 Some(dir.path()),
704 Some("TEST-LIC"),
705 UnlockType::ClockReset,
706 &state_key,
707 )
708 .unwrap();
709
710 let _signing_keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
712 let wrong_keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
713
714 let timestamp_bytes = Utc::now().timestamp().to_le_bytes();
715
716 let mut response = Vec::new();
718 response.extend_from_slice(×tamp_bytes);
719 response.push(1); response.extend_from_slice(&[0u8; 64]); let result = validate_response_code(
723 &response,
724 Some(dir.path()),
725 "TEST-LIC",
726 &state_key,
727 &wrong_keypair.public_key_pem,
728 algorithm_ids::ED25519,
729 );
730
731 assert!(result.is_err());
732 }
733
734 #[test]
735 fn test_validate_response_no_pending_challenge_fails() {
736 use crate::crypto::algorithm_ids;
737 use crate::keys::CryptoKeyPair;
738
739 let dir = tempfile::tempdir().unwrap();
740 let state_key = [0x42u8; 32];
741 let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
742
743 let mut response = Vec::new();
745 response.extend_from_slice(&Utc::now().timestamp().to_le_bytes());
746 response.push(1);
747 response.extend_from_slice(&[0u8; 64]);
748
749 let result = validate_response_code(
750 &response,
751 Some(dir.path()),
752 "TEST-LIC",
753 &state_key,
754 &keypair.public_key_pem,
755 algorithm_ids::ED25519,
756 );
757
758 assert!(result.is_err());
759 }
760
761 #[test]
762 fn test_validate_response_replay_fails() {
763 use crate::crypto::algorithm_ids;
764 use crate::keys::CryptoKeyPair;
765
766 let dir = tempfile::tempdir().unwrap();
767 let state_key = [0x42u8; 32];
768
769 let challenge = generate_challenge_from_state(
770 Some(dir.path()),
771 Some("TEST-LIC"),
772 UnlockType::ClockReset,
773 &state_key,
774 )
775 .unwrap();
776
777 let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
778
779 let timestamp = Utc::now();
780 let timestamp_bytes = timestamp.timestamp().to_le_bytes();
781
782 let mut hasher = Sha256::new();
783 hasher.update(challenge.nonce.as_bytes());
784 hasher.update(timestamp_bytes);
785 hasher.update([1u8]);
786 hasher.update(challenge.fingerprint_hash.as_bytes());
787 let message = hasher.finalize();
788
789 let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_ids::ED25519).unwrap();
790 let signature = algorithm.sign(&message, keypair.private_key_pem()).unwrap();
791
792 let mut response = Vec::new();
793 response.extend_from_slice(×tamp_bytes);
794 response.push(1);
795 response.extend_from_slice(&signature);
796
797 let result = validate_response_code(
799 &response,
800 Some(dir.path()),
801 "TEST-LIC",
802 &state_key,
803 &keypair.public_key_pem,
804 algorithm_ids::ED25519,
805 )
806 .unwrap();
807 assert!(result.success);
808
809 let result2 = validate_response_code(
811 &response,
812 Some(dir.path()),
813 "TEST-LIC",
814 &state_key,
815 &keypair.public_key_pem,
816 algorithm_ids::ED25519,
817 );
818 assert!(result2.is_err());
819 }
820}