feat(license): add gyandoot-license crate + CLI subcommand
- New crate: gyandoot-license with: - Machine fingerprinting (CPU, MAC, Disk, hostname) - Ed25519 JWT signing and verification - Agent counting with RAII guards - AES-256-GCM encrypted license cache - Anti-tamper binary integrity checks - Debugger detection - Added to workspace Cargo.toml - Added 'gyandoot license' CLI subcommand (activate, status, deactivate, fingerprint)
This commit is contained in:
@@ -21,6 +21,7 @@ members = [
|
|||||||
"crates/gyandoot-gateway",
|
"crates/gyandoot-gateway",
|
||||||
"crates/gyandoot-browser",
|
"crates/gyandoot-browser",
|
||||||
"crates/gyandoot-tui",
|
"crates/gyandoot-tui",
|
||||||
|
"crates/gyandoot-license",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@@ -73,6 +74,7 @@ gyandoot-profile = { path = "crates/gyandoot-profile" }
|
|||||||
gyandoot-gateway = { path = "crates/gyandoot-gateway" }
|
gyandoot-gateway = { path = "crates/gyandoot-gateway" }
|
||||||
gyandoot-browser = { path = "crates/gyandoot-browser" }
|
gyandoot-browser = { path = "crates/gyandoot-browser" }
|
||||||
gyandoot-tui = { path = "crates/gyandoot-tui" }
|
gyandoot-tui = { path = "crates/gyandoot-tui" }
|
||||||
|
gyandoot-license = { path = "crates/gyandoot-license" }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
28
crates/gyandoot-license/Cargo.toml
Normal file
28
crates/gyandoot-license/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "gyandoot-license"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "GyanDoot License Validation — JWT, Machine Binding, Agent Counting"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
aes-gcm.workspace = true
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
jsonwebtoken = "9"
|
||||||
|
ed25519-dalek = "2"
|
||||||
|
|
||||||
|
# Machine fingerprint
|
||||||
|
machine-uid = "0.3"
|
||||||
100
crates/gyandoot-license/src/agent_counter.rs
Normal file
100
crates/gyandoot-license/src/agent_counter.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//! Agent Counter
|
||||||
|
//!
|
||||||
|
//! Tracks and enforces the maximum number of concurrent agents
|
||||||
|
//! allowed by the license.
|
||||||
|
|
||||||
|
use crate::LicenseError;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Tracks active agent count and enforces license limits
|
||||||
|
pub struct AgentCounter {
|
||||||
|
max_agents: u32,
|
||||||
|
active_agents: Arc<AtomicU32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentCounter {
|
||||||
|
/// Create a new agent counter with a license limit
|
||||||
|
pub fn new(max_agents: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
max_agents,
|
||||||
|
active_agents: Arc::new(AtomicU32::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to acquire an agent slot
|
||||||
|
pub fn acquire(&self) -> Result<AgentGuard, LicenseError> {
|
||||||
|
let current = self.active_agents.fetch_add(1, Ordering::SeqCst);
|
||||||
|
if current >= self.max_agents {
|
||||||
|
self.active_agents.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
return Err(LicenseError::MaxAgentsExceeded {
|
||||||
|
max: self.max_agents,
|
||||||
|
current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(AgentGuard {
|
||||||
|
counter: self.active_agents.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current active agent count
|
||||||
|
pub fn active_count(&self) -> u32 {
|
||||||
|
self.active_agents.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get max allowed agents
|
||||||
|
pub fn max_agents(&self) -> u32 {
|
||||||
|
self.max_agents
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if we can spawn more agents
|
||||||
|
pub fn can_spawn(&self) -> bool {
|
||||||
|
self.active_agents.load(Ordering::SeqCst) < self.max_agents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RAII guard that decrements agent count on drop
|
||||||
|
pub struct AgentGuard {
|
||||||
|
counter: Arc<AtomicU32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AgentGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.counter.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_agent_counting() {
|
||||||
|
let counter = AgentCounter::new(3);
|
||||||
|
assert_eq!(counter.active_count(), 0);
|
||||||
|
assert!(counter.can_spawn());
|
||||||
|
|
||||||
|
let _g1 = counter.acquire().unwrap();
|
||||||
|
assert_eq!(counter.active_count(), 1);
|
||||||
|
|
||||||
|
let _g2 = counter.acquire().unwrap();
|
||||||
|
let _g3 = counter.acquire().unwrap();
|
||||||
|
assert_eq!(counter.active_count(), 3);
|
||||||
|
assert!(!counter.can_spawn());
|
||||||
|
|
||||||
|
// 4th should fail
|
||||||
|
let result = counter.acquire();
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_agent_guard_drop() {
|
||||||
|
let counter = AgentCounter::new(2);
|
||||||
|
{
|
||||||
|
let _g1 = counter.acquire().unwrap();
|
||||||
|
assert_eq!(counter.active_count(), 1);
|
||||||
|
}
|
||||||
|
// After drop, count should decrease
|
||||||
|
assert_eq!(counter.active_count(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
crates/gyandoot-license/src/anti_tamper.rs
Normal file
118
crates/gyandoot-license/src/anti_tamper.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
//! Anti-Tamper & Debugger Detection
|
||||||
|
//!
|
||||||
|
//! Prevents the binary from being modified or debugged.
|
||||||
|
//! - Binary integrity hash verification
|
||||||
|
//! - Debugger detection (ptrace)
|
||||||
|
//! - String obfuscation helpers
|
||||||
|
|
||||||
|
use crate::LicenseError;
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
|
/// Compute SHA-256 hash of the current binary
|
||||||
|
pub fn current_binary_hash() -> String {
|
||||||
|
let exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("unknown"));
|
||||||
|
if let Ok(data) = std::fs::read(&exe_path) {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&data);
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
} else {
|
||||||
|
"unknown_binary".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify binary integrity against an expected hash
|
||||||
|
pub fn verify_binary(expected_hash: &str) -> Result<(), LicenseError> {
|
||||||
|
let actual = current_binary_hash();
|
||||||
|
if actual != expected_hash {
|
||||||
|
return Err(LicenseError::BinaryTampered);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect if a debugger is attached
|
||||||
|
pub fn detect_debugger() -> bool {
|
||||||
|
// Linux: check if being ptraced
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Try ptrace(PTRACE_TRACEME, ...) — if it fails, we're being debugged
|
||||||
|
let result = unsafe { libc::ptrace(libc::PTRACE_TRACEME, 0, 0, 0) };
|
||||||
|
if result == -1 {
|
||||||
|
return true; // Being traced
|
||||||
|
}
|
||||||
|
// Detach
|
||||||
|
unsafe { libc::ptrace(libc::PTRACE_DETACH, 0, 0, 0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common debugger environment variables
|
||||||
|
let debug_env_vars = [
|
||||||
|
"DEBUG", "DEBUGGER", "JAVA_DEBUG", "PYTHONDEBUG",
|
||||||
|
"TRACE", "TRACEME", "LD_PRELOAD",
|
||||||
|
];
|
||||||
|
for var in &debug_env_vars {
|
||||||
|
if std::env::var(var).is_ok() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for /proc/self/status TracerPid
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Ok(content) = std::fs::read_to_string("/proc/self/status") {
|
||||||
|
for line in content.lines() {
|
||||||
|
if line.starts_with("TracerPid:") {
|
||||||
|
let pid = line.trim_start_matches("TracerPid:").trim();
|
||||||
|
if pid != "0" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run all anti-tamper checks at startup
|
||||||
|
pub fn run_startup_checks(expected_binary_hash: &str) -> Result<(), LicenseError> {
|
||||||
|
// Check 1: Debugger
|
||||||
|
if detect_debugger() {
|
||||||
|
return Err(LicenseError::DebuggerDetected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 2: Binary integrity
|
||||||
|
verify_binary(expected_binary_hash)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obfuscate a string literal (compile-time)
|
||||||
|
/// Usage: obfstr!("sensitive_string")
|
||||||
|
/// In production, use the `obfstr` crate for true compile-time obfuscation
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! obf {
|
||||||
|
($s:expr) => {{
|
||||||
|
let bytes: &[u8] = $s.as_bytes();
|
||||||
|
let mut obfuscated: Vec<u8> = bytes.iter().map(|b| b ^ 0xAA).collect();
|
||||||
|
let deobf: Vec<u8> = obfuscated.iter().map(|b| b ^ 0xAA).collect();
|
||||||
|
String::from_utf8(deobf).unwrap_or_default()
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_binary_hash_not_empty() {
|
||||||
|
let hash = current_binary_hash();
|
||||||
|
assert!(!hash.is_empty());
|
||||||
|
assert_eq!(hash.len(), 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_obf_macro() {
|
||||||
|
let original = "license.gyannetra.com";
|
||||||
|
let result = obf!(original);
|
||||||
|
assert_eq!(result, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
crates/gyandoot-license/src/cache.rs
Normal file
130
crates/gyandoot-license/src/cache.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
//! Encrypted License Cache
|
||||||
|
//!
|
||||||
|
//! Stores the license JWT locally in an AES-256-GCM encrypted file.
|
||||||
|
//! This prevents casual reading/modification of the cached license.
|
||||||
|
|
||||||
|
use crate::LicenseError;
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, KeyInit, OsRng},
|
||||||
|
Aes256Gcm, Nonce,
|
||||||
|
};
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Get the path to the license cache file
|
||||||
|
fn cache_path() -> PathBuf {
|
||||||
|
let config_dir = dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("gyandoot");
|
||||||
|
config_dir.join("license.enc")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive an AES-256 key from machine fingerprint + a static salt
|
||||||
|
fn derive_key() -> Vec<u8> {
|
||||||
|
let fingerprint = crate::fingerprint::collect_fingerprint();
|
||||||
|
let salt = b"GyAnDoOt_LiCeNsE_v1"; // Static salt, not secret
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(salt);
|
||||||
|
hasher.update(fingerprint.as_bytes());
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a license JWT to encrypted local cache
|
||||||
|
pub fn save_license(jwt_token: &str) -> Result<(), LicenseError> {
|
||||||
|
let key = derive_key();
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key)
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Key init failed: {}", e)))?;
|
||||||
|
|
||||||
|
let nonce_bytes: [u8; 12] = rand::random();
|
||||||
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||||
|
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(nonce, jwt_token.as_bytes())
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Encryption failed: {}", e)))?;
|
||||||
|
|
||||||
|
// Store: 12 bytes nonce + ciphertext
|
||||||
|
let mut data = nonce_bytes.to_vec();
|
||||||
|
data.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
let path = cache_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Failed to create cache dir: {}", e)))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set restrictive permissions (owner read/write only)
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
if let Ok(file) = std::fs::File::create(&path) {
|
||||||
|
let mut perms = file.metadata().map(|m| m.permissions()).unwrap_or_else(|_| {
|
||||||
|
std::fs::Permissions::from_mode(0o600)
|
||||||
|
});
|
||||||
|
perms.set_mode(0o600);
|
||||||
|
let _ = file.set_permissions(perms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(&path, &data)
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Failed to write cache: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a license JWT from encrypted local cache
|
||||||
|
pub fn load_license() -> Result<String, LicenseError> {
|
||||||
|
let path = cache_path();
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(LicenseError::Cache("No cached license found".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = std::fs::read(&path)
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Failed to read cache: {}", e)))?;
|
||||||
|
|
||||||
|
if data.len() < 12 {
|
||||||
|
return Err(LicenseError::Cache("Corrupted cache file".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (nonce_bytes, ciphertext) = data.split_at(12);
|
||||||
|
let nonce = Nonce::from_slice(nonce_bytes);
|
||||||
|
|
||||||
|
let key = derive_key();
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key)
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Key init failed: {}", e)))?;
|
||||||
|
|
||||||
|
let plaintext = cipher
|
||||||
|
.decrypt(nonce, ciphertext)
|
||||||
|
.map_err(|_| LicenseError::Cache("Decryption failed — cache tampered or wrong machine".to_string()))?;
|
||||||
|
|
||||||
|
String::from_utf8(plaintext)
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Invalid UTF-8 in cache: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a cached license exists
|
||||||
|
pub fn has_cached_license() -> bool {
|
||||||
|
cache_path().exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the cached license
|
||||||
|
pub fn clear_cache() -> Result<(), LicenseError> {
|
||||||
|
let path = cache_path();
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(&path)
|
||||||
|
.map_err(|e| LicenseError::Cache(format!("Failed to clear cache: {}", e)))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_save_and_load_roundtrip() {
|
||||||
|
let token = "eyJhbGciOiJFZERTQSJ9.test_token";
|
||||||
|
save_license(token).unwrap();
|
||||||
|
let loaded = load_license().unwrap();
|
||||||
|
assert_eq!(loaded, token);
|
||||||
|
clear_cache().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
171
crates/gyandoot-license/src/fingerprint.rs
Normal file
171
crates/gyandoot-license/src/fingerprint.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
//! Machine Fingerprinting
|
||||||
|
//!
|
||||||
|
//! Collects hardware identifiers to create a unique machine fingerprint.
|
||||||
|
//! This prevents license sharing by binding licenses to specific hardware.
|
||||||
|
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
/// Collected machine identifiers
|
||||||
|
pub struct MachineId {
|
||||||
|
pub cpu_id: String,
|
||||||
|
pub mac_hash: String,
|
||||||
|
pub disk_serial: String,
|
||||||
|
pub hostname_hash: String,
|
||||||
|
pub machine_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all available machine identifiers
|
||||||
|
pub fn collect_ids() -> MachineId {
|
||||||
|
MachineId {
|
||||||
|
cpu_id: get_cpu_id(),
|
||||||
|
mac_hash: get_mac_hash(),
|
||||||
|
disk_serial: get_disk_serial(),
|
||||||
|
hostname_hash: get_hostname_hash(),
|
||||||
|
machine_id: get_machine_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a single fingerprint string from all identifiers
|
||||||
|
pub fn collect_fingerprint() -> String {
|
||||||
|
let ids = collect_ids();
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(ids.cpu_id.as_bytes());
|
||||||
|
hasher.update(ids.mac_hash.as_bytes());
|
||||||
|
hasher.update(ids.disk_serial.as_bytes());
|
||||||
|
hasher.update(ids.hostname_hash.as_bytes());
|
||||||
|
hasher.update(ids.machine_id.as_bytes());
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cpu_id() -> String {
|
||||||
|
// Try /proc/cpuinfo first (Linux)
|
||||||
|
if let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") {
|
||||||
|
for line in content.lines() {
|
||||||
|
if let Some(val) = line.strip_prefix("model name\t: ") {
|
||||||
|
return val.trim().to_string();
|
||||||
|
}
|
||||||
|
if let Some(val) = line.strip_prefix("processor\t: ") {
|
||||||
|
return format!("cpu_{}", val.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: use a hash of /proc/cpuinfo
|
||||||
|
if let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(content.as_bytes());
|
||||||
|
return format!("cpu_{}", hex::encode(&hasher.finalize()[..8]));
|
||||||
|
}
|
||||||
|
"unknown_cpu".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mac_hash() -> String {
|
||||||
|
// Try common network interface paths
|
||||||
|
let paths = [
|
||||||
|
"/sys/class/net/eth0/address",
|
||||||
|
"/sys/class/net/enp0s3/address",
|
||||||
|
"/sys/class/net/enp0s8/address",
|
||||||
|
"/sys/class/net/wlan0/address",
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in &paths {
|
||||||
|
if let Ok(content) = std::fs::read_to_string(path) {
|
||||||
|
let mac = content.trim().to_lowercase();
|
||||||
|
if !mac.is_empty() && mac != "00:00:00:00:00:00" {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(mac.as_bytes());
|
||||||
|
return hex::encode(&hasher.finalize()[..8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try 'ip link' or 'ifconfig'
|
||||||
|
if let Ok(output) = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("ip link show 2>/dev/null | grep -E 'link/ether' | head -1 | awk '{print $2}'")
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
let mac = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if !mac.is_empty() {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(mac.as_bytes());
|
||||||
|
return hex::encode(&hasher.finalize()[..8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"unknown_mac".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_disk_serial() -> String {
|
||||||
|
// Try /dev/disk/by-id/ (Linux)
|
||||||
|
if let Ok(entries) = std::fs::read_dir("/dev/disk/by-id/") {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if !name.contains("part") && !name.contains("wwn") {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(name.as_bytes());
|
||||||
|
return hex::encode(&hasher.finalize()[..8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use root filesystem UUID
|
||||||
|
if let Ok(content) = std::fs::read_to_string("/etc/fstab") {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(content.as_bytes());
|
||||||
|
return hex::encode(&hasher.finalize()[..8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
"unknown_disk".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_hostname_hash() -> String {
|
||||||
|
let hostname = std::env::var("HOSTNAME")
|
||||||
|
.or_else(|_| std::env::var("HOST"))
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
std::fs::read_to_string("/etc/hostname")
|
||||||
|
.unwrap_or_else(|_| "unknown".to_string())
|
||||||
|
});
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(hostname.trim().as_bytes());
|
||||||
|
hex::encode(&hasher.finalize()[..8])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_machine_id() -> String {
|
||||||
|
// Try /etc/machine-id (systemd systems)
|
||||||
|
if let Ok(content) = std::fs::read_to_string("/etc/machine-id") {
|
||||||
|
return content.trim().to_string();
|
||||||
|
}
|
||||||
|
// Try /var/lib/dbus/machine-id
|
||||||
|
if let Ok(content) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
|
||||||
|
return content.trim().to_string();
|
||||||
|
}
|
||||||
|
"unknown_machine".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fingerprint_is_deterministic() {
|
||||||
|
let fp1 = collect_fingerprint();
|
||||||
|
let fp2 = collect_fingerprint();
|
||||||
|
assert_eq!(fp1, fp2, "Fingerprint should be deterministic within same session");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fingerprint_length() {
|
||||||
|
let fp = collect_fingerprint();
|
||||||
|
assert_eq!(fp.len(), 64, "SHA-256 fingerprint should be 64 hex chars");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collect_ids_no_panic() {
|
||||||
|
let ids = collect_ids();
|
||||||
|
assert!(!ids.cpu_id.is_empty());
|
||||||
|
assert!(!ids.mac_hash.is_empty());
|
||||||
|
assert!(!ids.hostname_hash.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
101
crates/gyandoot-license/src/jwt.rs
Normal file
101
crates/gyandoot-license/src/jwt.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
//! JWT License Token Management
|
||||||
|
//!
|
||||||
|
//! Handles signing, verifying, and parsing license JWTs using Ed25519.
|
||||||
|
|
||||||
|
use crate::{LicenseClaims, LicenseError};
|
||||||
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm};
|
||||||
|
|
||||||
|
/// Sign a license claims into a JWT (server-side)
|
||||||
|
pub fn sign_jwt(claims: &LicenseClaims, private_key_pem: &[u8]) -> Result<String, LicenseError> {
|
||||||
|
let key = EncodingKey::from_ed_pem(private_key_pem)
|
||||||
|
.map_err(|e| LicenseError::Other(format!("Invalid private key PEM: {}", e)))?;
|
||||||
|
|
||||||
|
let header = Header::new(Algorithm::EdDSA);
|
||||||
|
encode(&header, claims, &key)
|
||||||
|
.map_err(|e| LicenseError::Other(format!("JWT signing failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a JWT and extract claims (client-side)
|
||||||
|
pub fn verify_jwt(token: &str, public_key_bytes: &[u8]) -> Result<LicenseClaims, LicenseError> {
|
||||||
|
let key = DecodingKey::from_ed_der(public_key_bytes);
|
||||||
|
let mut validation = Validation::new(Algorithm::EdDSA);
|
||||||
|
validation.set_issuer(&["gyandoot.gyannetra.com"]);
|
||||||
|
validation.validate_exp = true;
|
||||||
|
|
||||||
|
let token_data = decode::<LicenseClaims>(token, &key, &validation)
|
||||||
|
.map_err(|e| {
|
||||||
|
match e.kind() {
|
||||||
|
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
|
||||||
|
LicenseError::Expired("JWT has expired".to_string())
|
||||||
|
}
|
||||||
|
jsonwebtoken::errors::ErrorKind::InvalidSignature => {
|
||||||
|
LicenseError::InvalidSignature
|
||||||
|
}
|
||||||
|
_ => LicenseError::Other(format!("JWT verification failed: {}", e)),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(token_data.claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a new Ed25519 key pair (for initial setup)
|
||||||
|
pub fn generate_keypair() -> (Vec<u8>, Vec<u8>) {
|
||||||
|
use ed25519_dalek::{SigningKey, VerifyingKey, SecretKey};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
|
let mut csprng = OsRng;
|
||||||
|
let signing_key = SigningKey::generate(&mut csprng);
|
||||||
|
let verifying_key = signing_key.verifying_key();
|
||||||
|
|
||||||
|
(signing_key.to_bytes().to_vec(), verifying_key.to_bytes().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::LicenseType;
|
||||||
|
|
||||||
|
fn make_test_claims() -> LicenseClaims {
|
||||||
|
LicenseClaims {
|
||||||
|
iss: "gyandoot.gyannetra.com".to_string(),
|
||||||
|
sub: "cust_test123".to_string(),
|
||||||
|
iat: chrono::Utc::now().timestamp(),
|
||||||
|
exp: chrono::Utc::now().timestamp() + 86400, // 1 day
|
||||||
|
license_type: "yearly".to_string(),
|
||||||
|
max_agents: 5,
|
||||||
|
features: vec!["browser".to_string(), "telegram".to_string()],
|
||||||
|
hw_fingerprint: "test_fingerprint".to_string(),
|
||||||
|
hw_fallback: vec![],
|
||||||
|
customer_name: "Test Corp".to_string(),
|
||||||
|
support_level: "standard".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_and_verify_jwt() {
|
||||||
|
let (private_key, public_key) = generate_keypair();
|
||||||
|
let claims = make_test_claims();
|
||||||
|
|
||||||
|
// Sign
|
||||||
|
let pem = generate_pem_from_private(&private_key);
|
||||||
|
let token = sign_jwt(&claims, pem.as_bytes()).unwrap();
|
||||||
|
assert!(!token.is_empty());
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
let verified = verify_jwt(&token, &public_key).unwrap();
|
||||||
|
assert_eq!(verified.sub, "cust_test123");
|
||||||
|
assert_eq!(verified.max_agents, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_pem_from_private(private_bytes: &[u8]) -> String {
|
||||||
|
// Simple PEM wrapper for Ed25519 private key
|
||||||
|
let b64 = base64::Engine::encode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
private_bytes
|
||||||
|
);
|
||||||
|
format!(
|
||||||
|
"-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----",
|
||||||
|
b64
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
207
crates/gyandoot-license/src/lib.rs
Normal file
207
crates/gyandoot-license/src/lib.rs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
//! GyanDoot License Validation Crate
|
||||||
|
//!
|
||||||
|
//! This crate provides:
|
||||||
|
//! - Machine fingerprinting (CPU, MAC, Disk, TPM)
|
||||||
|
//! - JWT-based license validation (Ed25519)
|
||||||
|
//! - Agent counting and enforcement
|
||||||
|
//! - Encrypted local license cache
|
||||||
|
//! - Anti-tamper binary integrity checks
|
||||||
|
//! - Debugger detection
|
||||||
|
|
||||||
|
pub mod fingerprint;
|
||||||
|
pub mod jwt;
|
||||||
|
pub mod agent_counter;
|
||||||
|
pub mod cache;
|
||||||
|
pub mod anti_tamper;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Supported license types
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum LicenseType {
|
||||||
|
/// 1-year renewable subscription
|
||||||
|
Yearly,
|
||||||
|
/// 10-year non-expiring license
|
||||||
|
Lifetime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for LicenseType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
LicenseType::Yearly => write!(f, "yearly"),
|
||||||
|
LicenseType::Lifetime => write!(f, "lifetime"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Support level
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum SupportLevel {
|
||||||
|
Standard,
|
||||||
|
Premium,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The license payload embedded in the JWT
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LicenseClaims {
|
||||||
|
// Standard JWT claims
|
||||||
|
pub iss: String,
|
||||||
|
pub sub: String,
|
||||||
|
pub iat: i64,
|
||||||
|
pub exp: i64,
|
||||||
|
|
||||||
|
// GyanDoot-specific
|
||||||
|
pub license_type: String,
|
||||||
|
pub max_agents: u32,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
pub hw_fingerprint: String,
|
||||||
|
pub hw_fallback: Vec<String>,
|
||||||
|
pub customer_name: String,
|
||||||
|
pub support_level: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors from the license system
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum LicenseError {
|
||||||
|
#[error("License expired: {0}")]
|
||||||
|
Expired(String),
|
||||||
|
|
||||||
|
#[error("Invalid license signature")]
|
||||||
|
InvalidSignature,
|
||||||
|
|
||||||
|
#[error("Machine fingerprint mismatch — license bound to different machine")]
|
||||||
|
MachineMismatch,
|
||||||
|
|
||||||
|
#[error("Max agents ({max}) exceeded — currently {current} active")]
|
||||||
|
MaxAgentsExceeded { max: u32, current: u32 },
|
||||||
|
|
||||||
|
#[error("Feature '{0}' not included in license")]
|
||||||
|
FeatureNotLicensed(String),
|
||||||
|
|
||||||
|
#[error("Binary integrity check failed — binary has been modified")]
|
||||||
|
BinaryTampered,
|
||||||
|
|
||||||
|
#[error("Debugger detected — refusing to run")]
|
||||||
|
DebuggerDetected,
|
||||||
|
|
||||||
|
#[error("Network error: {0}")]
|
||||||
|
Network(String),
|
||||||
|
|
||||||
|
#[error("Cache error: {0}")]
|
||||||
|
Cache(String),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a license validation
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ValidationResult {
|
||||||
|
Valid(LicenseClaims),
|
||||||
|
Expired,
|
||||||
|
Invalid,
|
||||||
|
MachineMismatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main license validator
|
||||||
|
pub struct LicenseValidator {
|
||||||
|
/// Ed25519 public key (hex-encoded) embedded in binary
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
/// License server URL
|
||||||
|
server_url: String,
|
||||||
|
/// Current machine fingerprint
|
||||||
|
fingerprint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LicenseValidator {
|
||||||
|
/// Create a new validator with the embedded public key
|
||||||
|
pub fn new(public_key_hex: &str, server_url: &str) -> Self {
|
||||||
|
let public_key = hex::decode(public_key_hex)
|
||||||
|
.expect("Invalid embedded public key — this is a build-time constant");
|
||||||
|
let fingerprint = fingerprint::collect_fingerprint();
|
||||||
|
Self {
|
||||||
|
public_key,
|
||||||
|
server_url: server_url.to_string(),
|
||||||
|
fingerprint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a license JWT locally (no network call)
|
||||||
|
pub fn validate_local(&self, jwt_token: &str) -> Result<LicenseClaims, LicenseError> {
|
||||||
|
let claims = jwt::verify_jwt(jwt_token, &self.public_key)?;
|
||||||
|
|
||||||
|
// Check expiry
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
if now > claims.exp {
|
||||||
|
return Err(LicenseError::Expired(format!(
|
||||||
|
"License expired at {}",
|
||||||
|
chrono::DateTime::from_timestamp(claims.exp, 0)
|
||||||
|
.map(|d| d.to_rfc3339())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string())
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check machine binding
|
||||||
|
if claims.hw_fingerprint != self.fingerprint {
|
||||||
|
// Check fallback machines
|
||||||
|
if !claims.hw_fallback.contains(&self.fingerprint) {
|
||||||
|
return Err(LicenseError::MachineMismatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activate a license key with the server
|
||||||
|
pub async fn activate(&self, license_key: &str) -> Result<LicenseClaims, LicenseError> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let resp = client
|
||||||
|
.post(format!("{}/api/v1/activate", self.server_url))
|
||||||
|
.json(&serde_json::json!({
|
||||||
|
"license_key": license_key,
|
||||||
|
"hw_fingerprint": self.fingerprint,
|
||||||
|
"binary_hash": anti_tamper::current_binary_hash(),
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LicenseError::Network(e.to_string()))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
return Err(LicenseError::Other(format!(
|
||||||
|
"Activation failed ({}): {}",
|
||||||
|
status, body
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: serde_json::Value = resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LicenseError::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
let jwt_token = body["jwt"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| LicenseError::Other("No JWT in response".to_string()))?;
|
||||||
|
|
||||||
|
let claims = self.validate_local(jwt_token)?;
|
||||||
|
|
||||||
|
// Cache the license locally
|
||||||
|
cache::save_license(jwt_token)?;
|
||||||
|
|
||||||
|
Ok(claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a feature is licensed
|
||||||
|
pub fn check_feature(&self, claims: &LicenseClaims, feature: &str) -> Result<(), LicenseError> {
|
||||||
|
if !claims.features.iter().any(|f| f == feature) {
|
||||||
|
return Err(LicenseError::FeatureNotLicensed(feature.to_string()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the machine fingerprint (for display/debug)
|
||||||
|
pub fn get_fingerprint(&self) -> &str {
|
||||||
|
&self.fingerprint
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/main.rs
116
src/main.rs
@@ -87,6 +87,12 @@ enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
action: PlatformAction,
|
action: PlatformAction,
|
||||||
},
|
},
|
||||||
|
/// License management (activate, status, deactivate, fingerprint)
|
||||||
|
#[command(name = "license")]
|
||||||
|
License {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: LicenseAction,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
@@ -145,6 +151,25 @@ enum PlatformAction {
|
|||||||
Configure,
|
Configure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum LicenseAction {
|
||||||
|
/// Activate GyanDoot with a license key
|
||||||
|
Activate {
|
||||||
|
/// License key (e.g., GYANDOOT-XXXX-XXXX)
|
||||||
|
#[arg(short, long)]
|
||||||
|
key: String,
|
||||||
|
/// License server URL
|
||||||
|
#[arg(short, long, default_value = "https://license.gyannetra.com")]
|
||||||
|
server: String,
|
||||||
|
},
|
||||||
|
/// Show current license status
|
||||||
|
Status,
|
||||||
|
/// Deactivate license on this machine
|
||||||
|
Deactivate,
|
||||||
|
/// Show machine fingerprint (for support)
|
||||||
|
Fingerprint,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -196,6 +221,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
Some(Commands::Platform { action }) => {
|
Some(Commands::Platform { action }) => {
|
||||||
cmd_platform(action).await?;
|
cmd_platform(action).await?;
|
||||||
}
|
}
|
||||||
|
Some(Commands::License { action }) => {
|
||||||
|
cmd_license(action).await?;
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
// Default: run interactive CLI mode
|
// Default: run interactive CLI mode
|
||||||
cmd_run(&cli.profile, "cli", None).await?;
|
cmd_run(&cli.profile, "cli", None).await?;
|
||||||
@@ -753,3 +781,91 @@ async fn cmd_platform(action: PlatformAction) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle license management subcommands
|
||||||
|
async fn cmd_license(action: LicenseAction) -> anyhow::Result<()> {
|
||||||
|
match action {
|
||||||
|
LicenseAction::Activate { key, server } => {
|
||||||
|
println!("🔑 Activating GyanDoot license...");
|
||||||
|
println!(" Server: {}", server);
|
||||||
|
println!(" Key: {}...{}", &key[..20], &key[key.len()-8..]);
|
||||||
|
|
||||||
|
let validator = gyandoot_license::LicenseValidator::new(
|
||||||
|
// Public key will be embedded at build time
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
&server,
|
||||||
|
);
|
||||||
|
|
||||||
|
match validator.activate(&key).await {
|
||||||
|
Ok(claims) => {
|
||||||
|
println!("\n✅ License activated successfully!");
|
||||||
|
println!(" Customer: {}", claims.customer_name);
|
||||||
|
println!(" License Type: {}", claims.license_type);
|
||||||
|
println!(" Max Agents: {}", claims.max_agents);
|
||||||
|
println!(" Features: {}", claims.features.join(", "));
|
||||||
|
println!(" Expires: {}", chrono::DateTime::from_timestamp(claims.exp, 0)
|
||||||
|
.map(|d| d.to_rfc3339())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("\n❌ License activation failed: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LicenseAction::Status => {
|
||||||
|
match gyandoot_license::cache::load_license() {
|
||||||
|
Ok(jwt) => {
|
||||||
|
let validator = gyandoot_license::LicenseValidator::new(
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"https://license.gyannetra.com",
|
||||||
|
);
|
||||||
|
match validator.validate_local(&jwt) {
|
||||||
|
Ok(claims) => {
|
||||||
|
println!("📋 License Status: Active");
|
||||||
|
println!(" Customer: {}", claims.customer_name);
|
||||||
|
println!(" Type: {}", claims.license_type);
|
||||||
|
println!(" Max Agents: {}", claims.max_agents);
|
||||||
|
println!(" Features: {}", claims.features.join(", "));
|
||||||
|
println!(" Expires: {}", chrono::DateTime::from_timestamp(claims.exp, 0)
|
||||||
|
.map(|d| d.to_rfc3339())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string()));
|
||||||
|
println!(" Machine: {}", &claims.hw_fingerprint[..16]);
|
||||||
|
}
|
||||||
|
Err(gyandoot_license::LicenseError::Expired(msg)) => {
|
||||||
|
println!("❌ License Expired: {}", msg);
|
||||||
|
}
|
||||||
|
Err(gyandoot_license::LicenseError::MachineMismatch) => {
|
||||||
|
println!("❌ License bound to a different machine");
|
||||||
|
println!(" Run 'gyandoot license activate' to re-activate");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("❌ License invalid: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("❌ No active license found");
|
||||||
|
println!(" Run 'gyandoot license activate --key YOUR_KEY' to activate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LicenseAction::Deactivate => {
|
||||||
|
match gyandoot_license::cache::clear_cache() {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("✅ License deactivated on this machine");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ Failed to deactivate: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LicenseAction::Fingerprint => {
|
||||||
|
let fp = gyandoot_license::fingerprint::collect_fingerprint();
|
||||||
|
println!("🔍 Machine Fingerprint");
|
||||||
|
println!(" {}", fp);
|
||||||
|
println!("\n Send this to support if you need to move your license.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user