offline-license-validator 0.1.1

Offline license validation library using Ed25519 signatures
Documentation
use base64::{engine::general_purpose, Engine as _};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use thiserror::Error;

use crate::types::{LicensePayload, ValidLicenseInfo};

/// License validation errors
#[derive(Debug, Error)]
pub enum LicenseError {
    #[error("Invalid license format: {0}")]
    InvalidFormat(String),

    #[error("Invalid base64 encoding: {0}")]
    InvalidEncoding(#[from] base64::DecodeError),

    #[error("Invalid signature")]
    InvalidSignature,

    #[error("Invalid JSON payload: {0}")]
    InvalidJson(#[from] serde_json::Error),

    #[error("HWID mismatch: expected {expected}, got {actual}")]
    HwidMismatch { expected: String, actual: String },

    #[error("License expired on {0}")]
    Expired(chrono::DateTime<chrono::Utc>),

    #[error("Invalid public key: {0}")]
    InvalidPublicKey(String),
}

/// Offline license validator
pub struct LicenseValidator {
    public_key: VerifyingKey,
}

impl LicenseValidator {
    /// Create a new validator with the given public key (hex-encoded)
    ///
    /// # Example
    /// ```
    /// use offline_license_validator::LicenseValidator;
    ///
    /// let validator = LicenseValidator::new(
    ///     "913d5e19269699e51bcdb5c5a7106c278ef0e0fe92d31b76b6daf5bb00594fcf"
    /// ).unwrap();
    /// ```
    pub fn new(public_key_hex: &str) -> Result<Self, LicenseError> {
        let public_key_bytes = hex::decode(public_key_hex)
            .map_err(|e| LicenseError::InvalidPublicKey(format!("Invalid hex: {}", e)))?;

        let key_bytes: [u8; 32] = public_key_bytes
            .try_into()
            .map_err(|_| LicenseError::InvalidPublicKey("Public key must be 32 bytes".into()))?;

        let public_key = VerifyingKey::from_bytes(&key_bytes)
            .map_err(|e| LicenseError::InvalidPublicKey(format!("Invalid Ed25519 key: {}", e)))?;

        Ok(LicenseValidator { public_key })
    }

    /// Validate a license string
    ///
    /// # Arguments
    /// * `license_string` - License in format: `Base64(payload).Base64(signature)`
    /// * `current_hwid` - Hardware ID of the current system
    ///
    /// # Returns
    /// `Ok(ValidLicenseInfo)` if license is valid, otherwise `Err(LicenseError)`
    ///
    /// # Example
    /// ```no_run
    /// use offline_license_validator::LicenseValidator;
    ///
    /// let validator = LicenseValidator::new("public_key_hex").unwrap();
    /// match validator.validate("license_string", "hwid123") {
    ///     Ok(info) => println!("Valid license: {:?}", info),
    ///     Err(e) => eprintln!("Invalid license: {}", e),
    /// }
    /// ```
    pub fn validate(
        &self,
        license_string: &str,
        current_hwid: &str,
    ) -> Result<ValidLicenseInfo, LicenseError> {
        // Step 1: Parse format "base64(payload).base64(signature)"
        let parts: Vec<&str> = license_string.split('.').collect();
        if parts.len() != 2 {
            return Err(LicenseError::InvalidFormat(format!(
                "Expected 2 parts separated by '.', got {}",
                parts.len()
            )));
        }

        // Step 2: Decode payload and signature
        let payload_bytes = general_purpose::STANDARD.decode(parts[0])?;
        let signature_bytes = general_purpose::STANDARD.decode(parts[1])?;

        // Step 3: Verify Ed25519 signature
        let signature_array: [u8; 64] = signature_bytes
            .try_into()
            .map_err(|_| LicenseError::InvalidFormat("Signature must be 64 bytes".into()))?;

        let signature = Signature::from_bytes(&signature_array);

        self.public_key
            .verify(&payload_bytes, &signature)
            .map_err(|_| LicenseError::InvalidSignature)?;

        // Step 4: Parse JSON payload
        let payload: LicensePayload = serde_json::from_slice(&payload_bytes)?;

        // Step 5: Verify HWID
        if payload.hwid != current_hwid {
            return Err(LicenseError::HwidMismatch {
                expected: payload.hwid.clone(),
                actual: current_hwid.to_string(),
            });
        }

        // Step 6: Verify expiration
        let now = chrono::Utc::now();
        if now > payload.expires_at {
            return Err(LicenseError::Expired(payload.expires_at));
        }

        // License is valid!
        Ok(payload.into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_invalid_format() {
        let validator = LicenseValidator::new(
            "913d5e19269699e51bcdb5c5a7106c278ef0e0fe92d31b76b6daf5bb00594fcf",
        )
        .unwrap();

        let result = validator.validate("invalid-format", "test-hwid");
        assert!(matches!(result, Err(LicenseError::InvalidFormat(_))));
    }

    #[test]
    fn test_invalid_public_key() {
        let result = LicenseValidator::new("invalid-hex");
        assert!(matches!(result, Err(LicenseError::InvalidPublicKey(_))));
    }
}