Skip to main content

alvan_lic/
generator.rs

1//! License key generation functionality
2
3use crate::{error::Result, LICENSE_PREFIX};
4use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
5use chrono::{DateTime, Duration, Utc};
6use hmac::{Hmac, Mac};
7use sha2::Sha256;
8
9type HmacSha256 = Hmac<Sha256>;
10
11/// License key generator that creates time-based license keys
12pub struct LicenseGenerator {
13    secret_key: String,
14}
15
16impl LicenseGenerator {
17    /// Create a new license generator with the provided secret key
18    ///
19    /// # Arguments
20    /// * `secret_key` - A secret key used for HMAC signing
21    ///
22    /// # Example
23    /// ```rust
24    /// use alvan_lic::LicenseGenerator;
25    /// let generator = LicenseGenerator::new("my-secret-key");
26    /// ```
27    pub fn new<S: Into<String>>(secret_key: S) -> Self {
28        Self {
29            secret_key: secret_key.into(),
30        }
31    }
32
33    /// Generate a license key valid for the specified number of hours
34    ///
35    /// # Arguments
36    /// * `hours` - Number of hours the license should be valid
37    ///
38    /// # Returns
39    /// A license key string starting with "alvan-"
40    ///
41    /// # Example
42    /// ```rust
43    /// use alvan_lic::LicenseGenerator;
44    /// let generator = LicenseGenerator::new("secret");
45    /// let license = generator.generate_key(24).unwrap(); // Valid for 24 hours
46    /// ```
47    pub fn generate_key(&self, hours: u64) -> Result<String> {
48        self.generate_key_with_timestamp(hours, Utc::now())
49    }
50
51    /// Generate a license key with a custom timestamp (useful for testing)
52    pub fn generate_key_with_timestamp(
53        &self,
54        hours: u64,
55        issued_at: DateTime<Utc>,
56    ) -> Result<String> {
57        // Calculate expiration time
58        let expires_at = issued_at + Duration::hours(hours as i64);
59
60        // Create the payload
61        let payload = format!("{}:{}", issued_at.timestamp(), expires_at.timestamp());
62
63        // Create HMAC signature
64        let mut mac = HmacSha256::new_from_slice(self.secret_key.as_bytes())
65            .expect("HMAC can take key of any size");
66        mac.update(payload.as_bytes());
67        let signature = mac.finalize().into_bytes();
68
69        // Combine payload and signature
70        let mut data = Vec::new();
71        data.extend_from_slice(payload.as_bytes());
72        data.push(b'.');
73        data.extend_from_slice(&signature);
74
75        // Encode to base64
76        let encoded = URL_SAFE_NO_PAD.encode(&data);
77
78        // Add prefix
79        Ok(format!("{}{}", LICENSE_PREFIX, encoded))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_generate_key() {
89        let generator = LicenseGenerator::new("test-secret");
90        let key = generator.generate_key(24).unwrap();
91        
92        assert!(key.starts_with(LICENSE_PREFIX));
93        assert!(key.len() > LICENSE_PREFIX.len());
94    }
95
96    #[test]
97    fn test_different_hours() {
98        let generator = LicenseGenerator::new("test-secret");
99        let key1 = generator.generate_key(1).unwrap();
100        let key24 = generator.generate_key(24).unwrap();
101        let key_year = generator.generate_key(24 * 365).unwrap();
102        
103        // All should start with prefix
104        assert!(key1.starts_with(LICENSE_PREFIX));
105        assert!(key24.starts_with(LICENSE_PREFIX));
106        assert!(key_year.starts_with(LICENSE_PREFIX));
107        
108        // Keys should be different
109        assert_ne!(key1, key24);
110        assert_ne!(key24, key_year);
111    }
112}