offline_license_validator/
validator.rs1use base64::{engine::general_purpose, Engine as _};
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use thiserror::Error;
4
5use crate::types::{LicensePayload, ValidLicenseInfo};
6
7#[derive(Debug, Error)]
9pub enum LicenseError {
10 #[error("Invalid license format: {0}")]
11 InvalidFormat(String),
12
13 #[error("Invalid base64 encoding: {0}")]
14 InvalidEncoding(#[from] base64::DecodeError),
15
16 #[error("Invalid signature")]
17 InvalidSignature,
18
19 #[error("Invalid JSON payload: {0}")]
20 InvalidJson(#[from] serde_json::Error),
21
22 #[error("HWID mismatch: expected {expected}, got {actual}")]
23 HwidMismatch { expected: String, actual: String },
24
25 #[error("License expired on {0}")]
26 Expired(chrono::DateTime<chrono::Utc>),
27
28 #[error("Invalid public key: {0}")]
29 InvalidPublicKey(String),
30}
31
32pub struct LicenseValidator {
34 public_key: VerifyingKey,
35}
36
37impl LicenseValidator {
38 pub fn new(public_key_hex: &str) -> Result<Self, LicenseError> {
49 let public_key_bytes = hex::decode(public_key_hex)
50 .map_err(|e| LicenseError::InvalidPublicKey(format!("Invalid hex: {}", e)))?;
51
52 let key_bytes: [u8; 32] = public_key_bytes
53 .try_into()
54 .map_err(|_| LicenseError::InvalidPublicKey("Public key must be 32 bytes".into()))?;
55
56 let public_key = VerifyingKey::from_bytes(&key_bytes)
57 .map_err(|e| LicenseError::InvalidPublicKey(format!("Invalid Ed25519 key: {}", e)))?;
58
59 Ok(LicenseValidator { public_key })
60 }
61
62 pub fn validate(
82 &self,
83 license_string: &str,
84 current_hwid: &str,
85 ) -> Result<ValidLicenseInfo, LicenseError> {
86 let parts: Vec<&str> = license_string.split('.').collect();
88 if parts.len() != 2 {
89 return Err(LicenseError::InvalidFormat(format!(
90 "Expected 2 parts separated by '.', got {}",
91 parts.len()
92 )));
93 }
94
95 let payload_bytes = general_purpose::STANDARD.decode(parts[0])?;
97 let signature_bytes = general_purpose::STANDARD.decode(parts[1])?;
98
99 let signature_array: [u8; 64] = signature_bytes
101 .try_into()
102 .map_err(|_| LicenseError::InvalidFormat("Signature must be 64 bytes".into()))?;
103
104 let signature = Signature::from_bytes(&signature_array);
105
106 self.public_key
107 .verify(&payload_bytes, &signature)
108 .map_err(|_| LicenseError::InvalidSignature)?;
109
110 let payload: LicensePayload = serde_json::from_slice(&payload_bytes)?;
112
113 if payload.hwid != current_hwid {
115 return Err(LicenseError::HwidMismatch {
116 expected: payload.hwid.clone(),
117 actual: current_hwid.to_string(),
118 });
119 }
120
121 let now = chrono::Utc::now();
123 if now > payload.expires_at {
124 return Err(LicenseError::Expired(payload.expires_at));
125 }
126
127 Ok(payload.into())
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_invalid_format() {
138 let validator = LicenseValidator::new(
139 "913d5e19269699e51bcdb5c5a7106c278ef0e0fe92d31b76b6daf5bb00594fcf",
140 )
141 .unwrap();
142
143 let result = validator.validate("invalid-format", "test-hwid");
144 assert!(matches!(result, Err(LicenseError::InvalidFormat(_))));
145 }
146
147 #[test]
148 fn test_invalid_public_key() {
149 let result = LicenseValidator::new("invalid-hex");
150 assert!(matches!(result, Err(LicenseError::InvalidPublicKey(_))));
151 }
152}