use crate::anti_tamper::{ClockStatus, HardwareFingerprint, LicenseState};
use crate::crypto::CryptoRegistry;
use crate::error::{LicenseError, Result};
use crate::state_manager::StateManager;
use chrono::{DateTime, Duration, Utc};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum UnlockType {
#[default]
ClockReset,
ActivationReset,
FullReset,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnlockChallenge {
pub challenge_code: String,
pub fingerprint_hash: String,
pub fingerprint: HardwareFingerprint,
pub timestamp: DateTime<Utc>,
pub nonce: String,
pub unlock_type: UnlockType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnlockResult {
pub success: bool,
pub unlock_type: UnlockType,
pub message: String,
pub files_reset: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockoutStatus {
pub is_locked: bool,
pub lock_reason: Option<String>,
pub locked_at: Option<DateTime<Utc>>,
pub clock_status: ClockStatus,
pub last_validated: Option<DateTime<Utc>>,
pub validation_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PendingChallenge {
nonce: String,
timestamp: DateTime<Utc>,
unlock_type: UnlockType,
fingerprint_hash: String,
}
type HmacSha256 = Hmac<Sha256>;
impl PendingChallenge {
fn save(&self, state_dir: &Path, state_integrity_key: &[u8; 32]) -> Result<()> {
let json = serde_json::to_vec(self)
.map_err(|e| LicenseError::SerializationError(e.to_string()))?;
let mut mac = HmacSha256::new_from_slice(state_integrity_key)
.map_err(|e| LicenseError::SigningFailed(format!("HMAC init: {}", e)))?;
mac.update(&json);
let tag = mac.finalize().into_bytes();
let mut data = Vec::with_capacity(32 + json.len());
data.extend_from_slice(&tag);
data.extend_from_slice(&json);
let path = Self::challenge_path(state_dir);
std::fs::create_dir_all(state_dir)?;
std::fs::write(&path, &data)?;
Ok(())
}
fn load_and_delete(state_dir: &Path, state_integrity_key: &[u8; 32]) -> Result<Self> {
let path = Self::challenge_path(state_dir);
let data = std::fs::read(&path).map_err(|_| {
LicenseError::InvalidResponseCode(
"No pending challenge found. Generate a challenge first.".to_string(),
)
})?;
let _ = std::fs::remove_file(&path);
if data.len() < 33 {
return Err(LicenseError::InvalidResponseCode(
"Corrupted challenge file".to_string(),
));
}
let (tag_bytes, json) = data.split_at(32);
let mut mac = HmacSha256::new_from_slice(state_integrity_key)
.map_err(|e| LicenseError::VerificationFailed(format!("HMAC init: {}", e)))?;
mac.update(json);
mac.verify_slice(tag_bytes).map_err(|_| {
LicenseError::VerificationFailed("Challenge file integrity check failed".to_string())
})?;
serde_json::from_slice(json)
.map_err(|e| LicenseError::InvalidResponseCode(format!("Corrupt challenge: {}", e)))
}
fn challenge_path(state_dir: &Path) -> PathBuf {
state_dir.join("pending.challenge")
}
}
pub fn generate_challenge_from_state(
state_dir: Option<&Path>,
_license_id: Option<&str>,
unlock_type: UnlockType,
state_integrity_key: &[u8; 32],
) -> Result<UnlockChallenge> {
let fingerprint = HardwareFingerprint::generate();
let nonce = generate_nonce();
let timestamp = Utc::now();
let challenge_code = generate_challenge_code(&fingerprint, timestamp, &nonce);
let pending = PendingChallenge {
nonce: nonce.clone(),
timestamp,
unlock_type,
fingerprint_hash: fingerprint.combined_hash.clone(),
};
let dir = state_dir.unwrap_or_else(|| Path::new("."));
pending.save(dir, state_integrity_key)?;
Ok(UnlockChallenge {
challenge_code,
fingerprint_hash: fingerprint.combined_hash.clone(),
fingerprint,
timestamp,
nonce,
unlock_type,
})
}
fn generate_challenge_code(
fingerprint: &HardwareFingerprint,
timestamp: DateTime<Utc>,
nonce: &str,
) -> String {
const ALPHABET: &[u8] = b"23456789ABCDEFGHJKMNPQRSTUVWXYZ";
let mut hasher = Sha256::new();
hasher.update(fingerprint.combined_hash.as_bytes());
hasher.update(timestamp.timestamp().to_le_bytes());
hasher.update(nonce.as_bytes());
let hash = hasher.finalize();
let mut code = String::with_capacity(12);
for i in 0..12 {
let idx = (hash[i] as usize) % ALPHABET.len();
code.push(ALPHABET[idx] as char);
}
format!(
"{}-{}-{}-{}",
&code[0..3],
&code[3..6],
&code[6..9],
&code[9..12]
)
}
fn generate_nonce() -> String {
use rand::Rng;
let bytes: [u8; 32] = rand::thread_rng().gen();
hex::encode(bytes)
}
pub fn validate_response_code(
response_bytes: &[u8],
state_dir: Option<&Path>,
license_id: &str,
state_integrity_key: &[u8; 32],
public_key_pem: &str,
algorithm_id: &str,
) -> Result<UnlockResult> {
if response_bytes.len() < 10 {
return Err(LicenseError::InvalidResponseCode(
"Response too short".to_string(),
));
}
let timestamp_bytes: [u8; 8] = response_bytes[0..8]
.try_into()
.map_err(|_| LicenseError::InvalidResponseCode("Invalid timestamp".to_string()))?;
let timestamp = i64::from_le_bytes(timestamp_bytes);
let response_time = DateTime::from_timestamp(timestamp, 0)
.ok_or_else(|| LicenseError::InvalidResponseCode("Invalid timestamp value".to_string()))?;
let now = Utc::now();
let age = now - response_time;
if age > Duration::hours(24) {
return Ok(UnlockResult {
success: false,
unlock_type: UnlockType::ClockReset,
message: "Response code has expired (older than 24 hours)".to_string(),
files_reset: vec![],
});
}
let unlock_type = match response_bytes[8] {
1 => UnlockType::ClockReset,
2 => UnlockType::ActivationReset,
3 => UnlockType::FullReset,
_ => {
return Err(LicenseError::InvalidResponseCode(
"Invalid unlock type".to_string(),
))
}
};
let signature = &response_bytes[9..];
let dir = state_dir.unwrap_or_else(|| Path::new("."));
let pending = PendingChallenge::load_and_delete(dir, state_integrity_key)?;
let challenge_age = now - pending.timestamp;
if challenge_age > Duration::hours(24) {
return Ok(UnlockResult {
success: false,
unlock_type,
message: "Challenge has expired (older than 24 hours). Generate a new one.".to_string(),
files_reset: vec![],
});
}
let unlock_type_byte = response_bytes[8];
let mut hasher = Sha256::new();
hasher.update(pending.nonce.as_bytes());
hasher.update(timestamp_bytes);
hasher.update([unlock_type_byte]);
hasher.update(pending.fingerprint_hash.as_bytes());
let message = hasher.finalize();
let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_id)?;
algorithm
.verify(&message, signature, public_key_pem)
.map_err(|_| {
LicenseError::InvalidResponseCode("Invalid signature on response code".to_string())
})?;
let files_reset = apply_unlock(state_dir, unlock_type, license_id, state_integrity_key)?;
Ok(UnlockResult {
success: true,
unlock_type,
message: "Machine successfully unlocked".to_string(),
files_reset,
})
}
fn apply_unlock(
state_dir: Option<&Path>,
unlock_type: UnlockType,
license_id: &str,
state_integrity_key: &[u8; 32],
) -> Result<Vec<String>> {
let mut files_reset = Vec::new();
let paths = unlock_state_paths(state_dir, license_id, state_integrity_key);
match unlock_type {
UnlockType::ClockReset => {
for path in &paths {
if path.exists() {
if let Ok(Some(mut state)) =
LicenseState::load(path, license_id, state_integrity_key)
{
state.last_system_time = Utc::now();
if state.save(path, state_integrity_key).is_ok() {
files_reset.push(path.display().to_string());
}
}
}
}
}
UnlockType::ActivationReset => {
for path in &paths {
if path.exists() {
if let Ok(Some(mut state)) =
LicenseState::load(path, license_id, state_integrity_key)
{
state.last_validated = Utc::now();
state.last_system_time = Utc::now();
if state.save(path, state_integrity_key).is_ok() {
files_reset.push(path.display().to_string());
}
}
}
}
}
UnlockType::FullReset => {
for path in &paths {
if path.exists() && std::fs::remove_file(path).is_ok() {
files_reset.push(path.display().to_string());
}
}
}
}
Ok(files_reset)
}
fn unlock_state_paths(
state_dir: Option<&Path>,
license_id: &str,
state_integrity_key: &[u8; 32],
) -> Vec<PathBuf> {
if let Some(dir) = state_dir {
vec![dir.join("license.state")]
} else {
StateManager::new(license_id, *state_integrity_key)
.paths()
.to_vec()
}
}
pub fn get_lockout_status(
state_dir: Option<&Path>,
license_id: &str,
state_integrity_key: &[u8; 32],
) -> Result<LockoutStatus> {
let paths = unlock_state_paths(state_dir, license_id, state_integrity_key);
let mut is_locked = false;
let mut lock_reason = None;
let mut locked_at = None;
let mut clock_status = ClockStatus::Ok {
current: Utc::now(),
};
let mut last_validated = None;
let mut validation_count = 0u64;
for path in &paths {
if path.exists() {
if let Ok(Some(state)) = LicenseState::load(path, license_id, state_integrity_key) {
if last_validated.is_none() || state.last_validated > last_validated.unwrap() {
last_validated = Some(state.last_validated);
}
validation_count = validation_count.max(state.validation_count);
if let Ok(status) = state.detect_clock_manipulation(Duration::hours(1)) {
match &status {
ClockStatus::Backwards { last_seen, .. } => {
is_locked = true;
lock_reason =
Some("Clock tampering detected: time moved backwards".to_string());
locked_at = Some(*last_seen);
clock_status = status.clone();
}
ClockStatus::SuspiciousJump { last_seen, .. } => {
is_locked = true;
lock_reason =
Some("Clock tampering suspected: suspicious time jump".to_string());
locked_at = Some(*last_seen);
clock_status = status.clone();
}
ClockStatus::Ok { .. } => {
if matches!(clock_status, ClockStatus::Ok { .. }) {
clock_status = status.clone();
}
}
}
}
}
}
}
Ok(LockoutStatus {
is_locked,
lock_reason,
locked_at,
clock_status,
last_validated,
validation_count,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_challenge_code_format() {
let fingerprint = HardwareFingerprint::default();
let timestamp = Utc::now();
let nonce = generate_nonce();
let code = generate_challenge_code(&fingerprint, timestamp, &nonce);
assert_eq!(code.len(), 15); assert!(code.chars().filter(|c| *c == '-').count() == 3);
for c in code.chars() {
if c != '-' {
assert!(c.is_ascii_alphanumeric());
}
}
}
#[test]
fn test_nonce_uniqueness() {
let nonce1 = generate_nonce();
let nonce2 = generate_nonce();
assert_ne!(nonce1, nonce2);
assert_eq!(nonce1.len(), 64); }
#[test]
fn test_unlock_type_default() {
let unlock_type = UnlockType::default();
assert_eq!(unlock_type, UnlockType::ClockReset);
}
#[test]
fn test_pending_challenge_round_trip() {
let dir = tempfile::tempdir().unwrap();
let key = [0xABu8; 32];
let pending = PendingChallenge {
nonce: "test-nonce-123".to_string(),
timestamp: Utc::now(),
unlock_type: UnlockType::ClockReset,
fingerprint_hash: "abc123".to_string(),
};
pending.save(dir.path(), &key).unwrap();
let loaded = PendingChallenge::load_and_delete(dir.path(), &key).unwrap();
assert_eq!(loaded.nonce, pending.nonce);
assert_eq!(loaded.fingerprint_hash, pending.fingerprint_hash);
assert_eq!(loaded.unlock_type, pending.unlock_type);
assert!(PendingChallenge::load_and_delete(dir.path(), &key).is_err());
}
#[test]
fn test_pending_challenge_wrong_key_fails() {
let dir = tempfile::tempdir().unwrap();
let key1 = [0xABu8; 32];
let key2 = [0xCDu8; 32];
let pending = PendingChallenge {
nonce: "test-nonce".to_string(),
timestamp: Utc::now(),
unlock_type: UnlockType::FullReset,
fingerprint_hash: "xyz".to_string(),
};
pending.save(dir.path(), &key1).unwrap();
assert!(PendingChallenge::load_and_delete(dir.path(), &key2).is_err());
}
#[test]
fn test_validate_response_with_real_signature() {
use crate::crypto::algorithm_ids;
use crate::keys::CryptoKeyPair;
let dir = tempfile::tempdir().unwrap();
let state_key = [0x42u8; 32];
let challenge = generate_challenge_from_state(
Some(dir.path()),
Some("TEST-LIC"),
UnlockType::ClockReset,
&state_key,
)
.unwrap();
let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
let timestamp = Utc::now();
let timestamp_bytes = timestamp.timestamp().to_le_bytes();
let unlock_type_byte: u8 = 1;
let mut hasher = Sha256::new();
hasher.update(challenge.nonce.as_bytes());
hasher.update(timestamp_bytes);
hasher.update([unlock_type_byte]);
hasher.update(challenge.fingerprint_hash.as_bytes());
let message = hasher.finalize();
let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_ids::ED25519).unwrap();
let signature = algorithm.sign(&message, keypair.private_key_pem()).unwrap();
let mut response = Vec::new();
response.extend_from_slice(×tamp_bytes);
response.push(unlock_type_byte);
response.extend_from_slice(&signature);
let result = validate_response_code(
&response,
Some(dir.path()),
"TEST-LIC",
&state_key,
&keypair.public_key_pem,
algorithm_ids::ED25519,
)
.unwrap();
assert!(result.success, "Expected success, got: {}", result.message);
assert_eq!(result.unlock_type, UnlockType::ClockReset);
}
#[test]
fn test_validate_response_wrong_signature_fails() {
use crate::crypto::algorithm_ids;
use crate::keys::CryptoKeyPair;
let dir = tempfile::tempdir().unwrap();
let state_key = [0x42u8; 32];
let _challenge = generate_challenge_from_state(
Some(dir.path()),
Some("TEST-LIC"),
UnlockType::ClockReset,
&state_key,
)
.unwrap();
let _signing_keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
let wrong_keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
let timestamp_bytes = Utc::now().timestamp().to_le_bytes();
let mut response = Vec::new();
response.extend_from_slice(×tamp_bytes);
response.push(1); response.extend_from_slice(&[0u8; 64]);
let result = validate_response_code(
&response,
Some(dir.path()),
"TEST-LIC",
&state_key,
&wrong_keypair.public_key_pem,
algorithm_ids::ED25519,
);
assert!(result.is_err());
}
#[test]
fn test_validate_response_no_pending_challenge_fails() {
use crate::crypto::algorithm_ids;
use crate::keys::CryptoKeyPair;
let dir = tempfile::tempdir().unwrap();
let state_key = [0x42u8; 32];
let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
let mut response = Vec::new();
response.extend_from_slice(&Utc::now().timestamp().to_le_bytes());
response.push(1);
response.extend_from_slice(&[0u8; 64]);
let result = validate_response_code(
&response,
Some(dir.path()),
"TEST-LIC",
&state_key,
&keypair.public_key_pem,
algorithm_ids::ED25519,
);
assert!(result.is_err());
}
#[test]
fn test_validate_response_replay_fails() {
use crate::crypto::algorithm_ids;
use crate::keys::CryptoKeyPair;
let dir = tempfile::tempdir().unwrap();
let state_key = [0x42u8; 32];
let challenge = generate_challenge_from_state(
Some(dir.path()),
Some("TEST-LIC"),
UnlockType::ClockReset,
&state_key,
)
.unwrap();
let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
let timestamp = Utc::now();
let timestamp_bytes = timestamp.timestamp().to_le_bytes();
let mut hasher = Sha256::new();
hasher.update(challenge.nonce.as_bytes());
hasher.update(timestamp_bytes);
hasher.update([1u8]);
hasher.update(challenge.fingerprint_hash.as_bytes());
let message = hasher.finalize();
let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_ids::ED25519).unwrap();
let signature = algorithm.sign(&message, keypair.private_key_pem()).unwrap();
let mut response = Vec::new();
response.extend_from_slice(×tamp_bytes);
response.push(1);
response.extend_from_slice(&signature);
let result = validate_response_code(
&response,
Some(dir.path()),
"TEST-LIC",
&state_key,
&keypair.public_key_pem,
algorithm_ids::ED25519,
)
.unwrap();
assert!(result.success);
let result2 = validate_response_code(
&response,
Some(dir.path()),
"TEST-LIC",
&state_key,
&keypair.public_key_pem,
algorithm_ids::ED25519,
);
assert!(result2.is_err());
}
}