Skip to main content

alvan_lic/
validator.rs

1//! License key validation functionality
2
3use crate::{
4    error::{LicenseError, Result},
5    LICENSE_PREFIX,
6};
7use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
8use chrono::{DateTime, Utc};
9use hmac::{Hmac, Mac};
10use sha2::Sha256;
11
12type HmacSha256 = Hmac<Sha256>;
13
14/// Information about a validated license
15#[derive(Debug, Clone)]
16pub struct LicenseInfo {
17    /// Whether the license is currently valid
18    pub is_valid: bool,
19    /// When the license was issued
20    pub issued_at: DateTime<Utc>,
21    /// When the license expires
22    pub expires_at: DateTime<Utc>,
23    /// Hours remaining until expiration (0 if expired)
24    pub hours_remaining: f64,
25}
26
27/// License key validator for offline validation
28pub struct LicenseValidator {
29    secret_key: String,
30}
31
32impl LicenseValidator {
33    /// Create a new license validator with the provided secret key
34    ///
35    /// # Arguments
36    /// * `secret_key` - The same secret key used for generation
37    ///
38    /// # Example
39    /// ```rust
40    /// use alvan_lic::LicenseValidator;
41    /// let validator = LicenseValidator::new("my-secret-key");
42    /// ```
43    pub fn new<S: Into<String>>(secret_key: S) -> Self {
44        Self {
45            secret_key: secret_key.into(),
46        }
47    }
48
49    /// Validate a license key
50    ///
51    /// # Arguments
52    /// * `license_key` - The license key to validate
53    ///
54    /// # Returns
55    /// `LicenseInfo` if the license is valid, or an error
56    ///
57    /// # Example
58    /// ```rust
59    /// use alvan_lic::{LicenseGenerator, LicenseValidator};
60    /// 
61    /// let secret = "secret-key";
62    /// let generator = LicenseGenerator::new(secret);
63    /// let validator = LicenseValidator::new(secret);
64    /// 
65    /// let license = generator.generate_key(24).unwrap();
66    /// let info = validator.validate_key(&license).unwrap();
67    /// assert!(info.is_valid);
68    /// ```
69    pub fn validate_key(&self, license_key: &str) -> Result<LicenseInfo> {
70        self.validate_key_at_time(license_key, Utc::now())
71    }
72
73    /// Validate a license key at a specific time (useful for testing)
74    pub fn validate_key_at_time(
75        &self,
76        license_key: &str,
77        current_time: DateTime<Utc>,
78    ) -> Result<LicenseInfo> {
79        // Check prefix
80        if !license_key.starts_with(LICENSE_PREFIX) {
81            return Err(LicenseError::InvalidFormat);
82        }
83
84        // Remove prefix and decode
85        let encoded = &license_key[LICENSE_PREFIX.len()..];
86        let data = URL_SAFE_NO_PAD
87            .decode(encoded)
88            .map_err(|e| LicenseError::Base64Error(e))?;
89
90        // Find the separator
91        let separator_pos = data
92            .iter()
93            .position(|&b| b == b'.')
94            .ok_or(LicenseError::InvalidFormat)?;
95
96        // Split payload and signature
97        let payload = &data[..separator_pos];
98        let provided_signature = &data[separator_pos + 1..];
99
100        // Verify signature
101        let mut mac = HmacSha256::new_from_slice(self.secret_key.as_bytes())
102            .expect("HMAC can take key of any size");
103        mac.update(payload);
104        
105        mac.verify_slice(provided_signature)
106            .map_err(|_| LicenseError::InvalidSignature)?;
107
108        // Parse payload
109        let payload_str = String::from_utf8(payload.to_vec())
110            .map_err(|e| LicenseError::InvalidData(e.to_string()))?;
111        
112        let parts: Vec<&str> = payload_str.split(':').collect();
113        if parts.len() != 2 {
114            return Err(LicenseError::InvalidFormat);
115        }
116
117        let issued_timestamp = parts[0]
118            .parse::<i64>()
119            .map_err(|e| LicenseError::InvalidData(e.to_string()))?;
120        let expires_timestamp = parts[1]
121            .parse::<i64>()
122            .map_err(|e| LicenseError::InvalidData(e.to_string()))?;
123
124        let issued_at = DateTime::from_timestamp(issued_timestamp, 0)
125            .ok_or_else(|| LicenseError::InvalidData("Invalid issued timestamp".to_string()))?;
126        let expires_at = DateTime::from_timestamp(expires_timestamp, 0)
127            .ok_or_else(|| LicenseError::InvalidData("Invalid expires timestamp".to_string()))?;
128
129        // Check if expired
130        let is_valid = current_time < expires_at;
131        let hours_remaining = if is_valid {
132            (expires_at - current_time).num_minutes() as f64 / 60.0
133        } else {
134            0.0
135        };
136
137        if !is_valid {
138            return Err(LicenseError::Expired);
139        }
140
141        Ok(LicenseInfo {
142            is_valid,
143            issued_at,
144            expires_at,
145            hours_remaining,
146        })
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::LicenseGenerator;
154    use chrono::Duration;
155
156    #[test]
157    fn test_validate_valid_key() {
158        let secret = "test-secret";
159        let generator = LicenseGenerator::new(secret);
160        let validator = LicenseValidator::new(secret);
161
162        let key = generator.generate_key(24).unwrap();
163        let info = validator.validate_key(&key).unwrap();
164
165        assert!(info.is_valid);
166        assert!(info.hours_remaining > 23.0);
167        assert!(info.hours_remaining <= 24.0);
168    }
169
170    #[test]
171    fn test_validate_expired_key() {
172        let secret = "test-secret";
173        let generator = LicenseGenerator::new(secret);
174        let validator = LicenseValidator::new(secret);
175
176        // Generate a key that expired 2 hours ago
177        let past_time = Utc::now() - Duration::hours(3);
178        let key = generator.generate_key_with_timestamp(1, past_time).unwrap();
179        
180        let result = validator.validate_key(&key);
181        assert!(matches!(result, Err(LicenseError::Expired)));
182    }
183
184    #[test]
185    fn test_validate_wrong_secret() {
186        let generator = LicenseGenerator::new("secret1");
187        let validator = LicenseValidator::new("secret2");
188
189        let key = generator.generate_key(24).unwrap();
190        let result = validator.validate_key(&key);
191        
192        assert!(matches!(result, Err(LicenseError::InvalidSignature)));
193    }
194
195    #[test]
196    fn test_validate_tampered_key() {
197        let secret = "test-secret";
198        let generator = LicenseGenerator::new(secret);
199        let validator = LicenseValidator::new(secret);
200
201        let mut key = generator.generate_key(24).unwrap();
202        // Tamper with the key
203        key.push('X');
204        
205        let result = validator.validate_key(&key);
206        assert!(result.is_err());
207    }
208}