Skip to main content

offline_license_validator/
validator.rs

1use base64::{engine::general_purpose, Engine as _};
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use thiserror::Error;
4
5use crate::types::{LicensePayload, ValidLicenseInfo};
6
7/// License validation errors
8#[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
32/// Offline license validator
33pub struct LicenseValidator {
34    public_key: VerifyingKey,
35}
36
37impl LicenseValidator {
38    /// Create a new validator with the given public key (hex-encoded)
39    ///
40    /// # Example
41    /// ```
42    /// use offline_license_validator::LicenseValidator;
43    ///
44    /// let validator = LicenseValidator::new(
45    ///     "913d5e19269699e51bcdb5c5a7106c278ef0e0fe92d31b76b6daf5bb00594fcf"
46    /// ).unwrap();
47    /// ```
48    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    /// Validate a license string
63    ///
64    /// # Arguments
65    /// * `license_string` - License in format: `Base64(payload).Base64(signature)`
66    /// * `current_hwid` - Hardware ID of the current system
67    ///
68    /// # Returns
69    /// `Ok(ValidLicenseInfo)` if license is valid, otherwise `Err(LicenseError)`
70    ///
71    /// # Example
72    /// ```no_run
73    /// use offline_license_validator::LicenseValidator;
74    ///
75    /// let validator = LicenseValidator::new("public_key_hex").unwrap();
76    /// match validator.validate("license_string", "hwid123") {
77    ///     Ok(info) => println!("Valid license: {:?}", info),
78    ///     Err(e) => eprintln!("Invalid license: {}", e),
79    /// }
80    /// ```
81    pub fn validate(
82        &self,
83        license_string: &str,
84        current_hwid: &str,
85    ) -> Result<ValidLicenseInfo, LicenseError> {
86        // Step 1: Parse format "base64(payload).base64(signature)"
87        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        // Step 2: Decode payload and signature
96        let payload_bytes = general_purpose::STANDARD.decode(parts[0])?;
97        let signature_bytes = general_purpose::STANDARD.decode(parts[1])?;
98
99        // Step 3: Verify Ed25519 signature
100        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        // Step 4: Parse JSON payload
111        let payload: LicensePayload = serde_json::from_slice(&payload_bytes)?;
112
113        // Step 5: Verify HWID
114        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        // Step 6: Verify expiration
122        let now = chrono::Utc::now();
123        if now > payload.expires_at {
124            return Err(LicenseError::Expired(payload.expires_at));
125        }
126
127        // License is valid!
128        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}