1use 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#[derive(Debug, Clone)]
16pub struct LicenseInfo {
17 pub is_valid: bool,
19 pub issued_at: DateTime<Utc>,
21 pub expires_at: DateTime<Utc>,
23 pub hours_remaining: f64,
25}
26
27pub struct LicenseValidator {
29 secret_key: String,
30}
31
32impl LicenseValidator {
33 pub fn new<S: Into<String>>(secret_key: S) -> Self {
44 Self {
45 secret_key: secret_key.into(),
46 }
47 }
48
49 pub fn validate_key(&self, license_key: &str) -> Result<LicenseInfo> {
70 self.validate_key_at_time(license_key, Utc::now())
71 }
72
73 pub fn validate_key_at_time(
75 &self,
76 license_key: &str,
77 current_time: DateTime<Utc>,
78 ) -> Result<LicenseInfo> {
79 if !license_key.starts_with(LICENSE_PREFIX) {
81 return Err(LicenseError::InvalidFormat);
82 }
83
84 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 let separator_pos = data
92 .iter()
93 .position(|&b| b == b'.')
94 .ok_or(LicenseError::InvalidFormat)?;
95
96 let payload = &data[..separator_pos];
98 let provided_signature = &data[separator_pos + 1..];
99
100 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 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 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 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 key.push('X');
204
205 let result = validator.validate_key(&key);
206 assert!(result.is_err());
207 }
208}