mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 01:50:30 +08:00
feat(rust): add device fingerprint and selfhosted provider (cache, license verify, offline validation)
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FingerprintLayer {
|
||||
pub source: &'static str,
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceFingerprint {
|
||||
pub composite_hash: String,
|
||||
pub stability_score: u8,
|
||||
pub layers: Vec<FingerprintLayer>,
|
||||
}
|
||||
|
||||
pub fn collect() -> DeviceFingerprint {
|
||||
let mut layers = Vec::new();
|
||||
|
||||
let l1 = try_dmi_uuid();
|
||||
layers.push(FingerprintLayer { source: "hw_uuid", value: l1 });
|
||||
|
||||
let l2 = try_machine_id();
|
||||
layers.push(FingerprintLayer { source: "os_id", value: l2 });
|
||||
|
||||
let l3 = try_rootfs_uuid();
|
||||
layers.push(FingerprintLayer { source: "fs_uuid", value: l3 });
|
||||
|
||||
let l4 = Some(try_physical_mac());
|
||||
layers.push(FingerprintLayer { source: "mac", value: l4 });
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
for layer in &layers {
|
||||
if let Some(ref v) = layer.value {
|
||||
hasher.update(v.as_bytes());
|
||||
}
|
||||
hasher.update(b"|");
|
||||
}
|
||||
let hash = format!("HC-{:x}", hasher.finalize());
|
||||
|
||||
let stability = compute_stability(&layers);
|
||||
|
||||
DeviceFingerprint {
|
||||
composite_hash: hash,
|
||||
stability_score: stability,
|
||||
layers,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_stability(layers: &[FingerprintLayer]) -> u8 {
|
||||
let mut score: u8 = 0;
|
||||
if layers[0].value.is_some() { score += 40; }
|
||||
if layers[1].value.is_some() { score += 30; }
|
||||
if layers[2].value.is_some() { score += 20; }
|
||||
if layers[3].value.is_some() { score += 10; }
|
||||
score
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn try_dmi_uuid() -> Option<String> {
|
||||
std::fs::read_to_string("/sys/class/dmi/id/product_uuid")
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty() && s != "00000000-0000-0000-0000-000000000000")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn try_dmi_uuid() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn try_machine_id() -> Option<String> {
|
||||
std::fs::read_to_string("/etc/machine-id")
|
||||
.or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id"))
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn try_machine_id() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn try_rootfs_uuid() -> Option<String> {
|
||||
std::fs::read_to_string("/proc/cmdline")
|
||||
.ok()
|
||||
.and_then(|cmdline| {
|
||||
for part in cmdline.split_whitespace() {
|
||||
if part.starts_with("root=") {
|
||||
return Some(part.trim_start_matches("root=").to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn try_rootfs_uuid() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_physical_mac() -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mut macs: Vec<String> = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
|
||||
for entry in entries.flatten() {
|
||||
let iface = entry.file_name().to_string_lossy().to_string();
|
||||
if iface == "lo" || iface.starts_with("docker") || iface.starts_with("veth") || iface.starts_with("tap") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(addr) = std::fs::read_to_string(entry.path().join("address")) {
|
||||
let a = addr.trim().to_string();
|
||||
if !a.is_empty() && a != "00:00:00:00:00:00" {
|
||||
macs.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if macs.is_empty() { "unknown-mac".to_string() } else { macs.sort(); macs.join(",") }
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{ "unknown-mac".to_string() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_collect_returns_valid_structure() {
|
||||
let fp = collect();
|
||||
assert!(fp.composite_hash.starts_with("HC-"));
|
||||
assert_eq!(fp.layers.len(), 4);
|
||||
for layer in &fp.layers {
|
||||
assert!(matches!(layer.source, "hw_uuid" | "os_id" | "fs_uuid" | "mac"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stability_score_never_exceeds_100() {
|
||||
let fp = collect();
|
||||
assert!(fp.stability_score <= 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composite_hash_deterministic() {
|
||||
let fp1 = collect();
|
||||
let fp2 = collect();
|
||||
assert_eq!(fp1.composite_hash, fp2.composite_hash);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ mod security;
|
||||
mod error;
|
||||
mod session;
|
||||
pub mod crypto;
|
||||
pub mod device;
|
||||
pub mod provider_selfhosted;
|
||||
|
||||
pub struct CraftContext {
|
||||
dummy: i32,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
use crate::crypto;
|
||||
use crate::device::DeviceFingerprint;
|
||||
use crate::error::LicenseError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CachedLicense {
|
||||
pub license_id: String,
|
||||
pub not_before: Option<i64>,
|
||||
pub not_after: Option<i64>,
|
||||
pub offline_grace_days: u32,
|
||||
pub heartbeat_interval_hours: u32,
|
||||
pub max_devices: u32,
|
||||
pub max_concurrent_users: u32,
|
||||
#[serde(default)]
|
||||
pub features: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LicenseCache {
|
||||
pub license: Option<CachedLicense>,
|
||||
pub last_checkpoint: i64,
|
||||
pub cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct HeartbeatState {
|
||||
last_heartbeat: i64,
|
||||
}
|
||||
|
||||
impl LicenseCache {
|
||||
pub fn load(fp: &DeviceFingerprint) -> Result<Self, LicenseError> {
|
||||
let cache_dir = craftlabs_dir();
|
||||
let cache_file = cache_dir.join("license_cache.json");
|
||||
|
||||
let license = if cache_file.exists() {
|
||||
let encrypted = std::fs::read(&cache_file)
|
||||
.map_err(|_| LicenseError::ConfigMissing("cannot read cache file"))?;
|
||||
let aes_key = cache_encryption_key(fp);
|
||||
let plain = crypto::aes_gcm_decrypt(&aes_key, &encrypted)?;
|
||||
let cached: CachedLicense = serde_json::from_slice(&plain)
|
||||
.map_err(|_| LicenseError::CorruptedPayload)?;
|
||||
Some(cached)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let checkpoint_file = cache_dir.join("heartbeat_state.json");
|
||||
let last_checkpoint = if checkpoint_file.exists() {
|
||||
let content = std::fs::read_to_string(&checkpoint_file).unwrap_or_default();
|
||||
serde_json::from_str::<HeartbeatState>(&content)
|
||||
.map(|h| h.last_heartbeat)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(LicenseCache { license, last_checkpoint, cache_dir })
|
||||
}
|
||||
|
||||
pub fn store(&mut self, cached: CachedLicense) -> Result<(), LicenseError> {
|
||||
self.license = Some(cached);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_checkpoint(&mut self) {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
self.last_checkpoint = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
}
|
||||
|
||||
pub fn persist(&self, fp: &DeviceFingerprint) -> Result<(), LicenseError> {
|
||||
if let Some(ref cached) = self.license {
|
||||
let json = serde_json::to_vec(cached).unwrap();
|
||||
let aes_key = cache_encryption_key(fp);
|
||||
let encrypted = crypto::aes_gcm_encrypt(&aes_key, &json);
|
||||
let f = self.cache_dir.join("license_cache.json");
|
||||
std::fs::write(&f, encrypted)
|
||||
.map_err(|_| LicenseError::ConfigMissing("cannot write cache"))?;
|
||||
}
|
||||
|
||||
let state = HeartbeatState { last_heartbeat: self.last_checkpoint };
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
let f = self.cache_dir.join("heartbeat_state.json");
|
||||
std::fs::write(&f, json).ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_encryption_key(fp: &DeviceFingerprint) -> [u8; 32] {
|
||||
let salt = format!("cache-{}", fp.composite_hash);
|
||||
crypto::derive_aes_key(&salt)
|
||||
}
|
||||
|
||||
fn craftlabs_dir() -> PathBuf {
|
||||
let home = std::env::var("HOME").ok().map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
let dir = home.join(".craftlabs");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
use crate::crypto;
|
||||
use crate::error::LicenseError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RawLicense {
|
||||
pub version: i32,
|
||||
pub license_id: String,
|
||||
#[serde(default)]
|
||||
pub issued_at: Option<String>,
|
||||
pub payload: String,
|
||||
pub signature: SignatureBlock,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SignatureBlock {
|
||||
pub algorithm: String,
|
||||
pub key_id: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct LicensePayload {
|
||||
pub tenant_id: String,
|
||||
pub product: String,
|
||||
pub grant: Grant,
|
||||
pub constraints: Constraints,
|
||||
#[serde(default)]
|
||||
pub features: HashMap<String, bool>,
|
||||
#[serde(default)]
|
||||
pub custom: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Grant {
|
||||
#[serde(rename = "type")]
|
||||
pub grant_type: String,
|
||||
pub not_before: Option<i64>,
|
||||
pub not_after: Option<i64>,
|
||||
pub offline_grace_days: u32,
|
||||
pub heartbeat_interval_hours: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Constraints {
|
||||
pub max_devices: u32,
|
||||
#[serde(default)]
|
||||
pub max_concurrent_users: u32,
|
||||
#[serde(default)]
|
||||
pub max_activations: u32,
|
||||
}
|
||||
|
||||
pub fn verify_and_parse(
|
||||
license_json: &str,
|
||||
public_key_pem: &str,
|
||||
) -> Result<LicensePayload, LicenseError> {
|
||||
let raw: RawLicense = serde_json::from_str(license_json)
|
||||
.map_err(|_| LicenseError::InvalidFormat("license json"))?;
|
||||
|
||||
if raw.version != 1 {
|
||||
return Err(LicenseError::InvalidFormat("unsupported license version"));
|
||||
}
|
||||
|
||||
let payload_bytes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
|
||||
&raw.payload,
|
||||
)
|
||||
.map_err(|_| LicenseError::InvalidFormat("payload base64"))?;
|
||||
|
||||
crypto::verify_rsa_signature(public_key_pem, &payload_bytes, &raw.signature.value)?;
|
||||
|
||||
let aes_key = crypto::derive_aes_key(&raw.license_id);
|
||||
let plain = crypto::aes_gcm_decrypt(&aes_key, &payload_bytes)?;
|
||||
|
||||
let license: LicensePayload = serde_json::from_slice(&plain)
|
||||
.map_err(|_| LicenseError::CorruptedPayload)?;
|
||||
|
||||
Ok(license)
|
||||
}
|
||||
|
||||
pub fn validate_time_window(
|
||||
license: &LicensePayload,
|
||||
offline_grace_days: u32,
|
||||
last_checkpoint: i64,
|
||||
) -> Result<(), LicenseError> {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
|
||||
if let Some(not_before) = license.grant.not_before {
|
||||
if now < not_before {
|
||||
return Err(LicenseError::NotYetValid);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(not_after) = license.grant.not_after {
|
||||
let effective_deadline = not_after + (offline_grace_days as i64 * 86400);
|
||||
if now > effective_deadline {
|
||||
return Err(LicenseError::Expired);
|
||||
}
|
||||
}
|
||||
|
||||
if last_checkpoint > 0 {
|
||||
let days_offline = ((now - last_checkpoint) / 86400) as u32;
|
||||
if days_offline > offline_grace_days {
|
||||
return Err(LicenseError::OfflineGraceExceeded {
|
||||
days_offline,
|
||||
max_days: offline_grace_days,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_cached_license(
|
||||
payload: &LicensePayload,
|
||||
offline_grace_days: u32,
|
||||
heartbeat_interval_hours: u32,
|
||||
) -> super::cache::CachedLicense {
|
||||
super::cache::CachedLicense {
|
||||
license_id: payload.tenant_id.clone() + "-" + &payload.product,
|
||||
not_before: payload.grant.not_before,
|
||||
not_after: payload.grant.not_after,
|
||||
offline_grace_days,
|
||||
heartbeat_interval_hours,
|
||||
max_devices: payload.constraints.max_devices,
|
||||
max_concurrent_users: payload.constraints.max_concurrent_users,
|
||||
features: payload.features.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto;
|
||||
|
||||
#[test]
|
||||
fn test_validate_time_window_expired() {
|
||||
let lp = LicensePayload {
|
||||
tenant_id: "test".into(),
|
||||
product: "test".into(),
|
||||
grant: Grant {
|
||||
grant_type: "subscription".into(),
|
||||
not_before: Some(0),
|
||||
not_after: Some(100),
|
||||
offline_grace_days: 0,
|
||||
heartbeat_interval_hours: 24,
|
||||
},
|
||||
constraints: Constraints { max_devices: 5, max_concurrent_users: 0, max_activations: 0 },
|
||||
features: HashMap::new(),
|
||||
custom: HashMap::new(),
|
||||
};
|
||||
assert!(validate_time_window(&lp, 7, 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_time_window_valid() {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
|
||||
let lp = LicensePayload {
|
||||
tenant_id: "test".into(),
|
||||
product: "test".into(),
|
||||
grant: Grant {
|
||||
grant_type: "perpetual".into(),
|
||||
not_before: Some(now - 86400),
|
||||
not_after: Some(now + 86400 * 365),
|
||||
offline_grace_days: 7,
|
||||
heartbeat_interval_hours: 24,
|
||||
},
|
||||
constraints: Constraints { max_devices: 5, max_concurrent_users: 0, max_activations: 0 },
|
||||
features: HashMap::new(),
|
||||
custom: HashMap::new(),
|
||||
};
|
||||
assert!(validate_time_window(&lp, 7, now).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_and_parse_with_generated_license() {
|
||||
use rsa::pkcs8::EncodePublicKey;
|
||||
use rsa::signature::Signer;
|
||||
use rsa::{pkcs8::LineEnding, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
use base64::Engine;
|
||||
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
let priv_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
|
||||
let pub_key = RsaPublicKey::from(&priv_key);
|
||||
let pub_pem = pub_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||
|
||||
let payload = LicensePayload {
|
||||
tenant_id: "test-tenant".into(),
|
||||
product: "test-product".into(),
|
||||
grant: Grant {
|
||||
grant_type: "perpetual".into(),
|
||||
not_before: Some(0),
|
||||
not_after: Some(9999999999),
|
||||
offline_grace_days: 7,
|
||||
heartbeat_interval_hours: 24,
|
||||
},
|
||||
constraints: Constraints { max_devices: 5, max_concurrent_users: 0, max_activations: 0 },
|
||||
features: [("api".into(), true)].into(),
|
||||
custom: HashMap::new(),
|
||||
};
|
||||
|
||||
let payload_json = serde_json::to_vec(&payload).unwrap();
|
||||
let aes_key = crypto::derive_aes_key("test-license-id");
|
||||
let encrypted = crypto::aes_gcm_encrypt(&aes_key, &payload_json);
|
||||
let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&encrypted);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&encrypted);
|
||||
let digest = hasher.finalize();
|
||||
let sig = priv_key.sign(Pkcs1v15Sign::new::<Sha256>(), &digest).unwrap();
|
||||
let sig_bytes: &[u8] = sig.as_ref();
|
||||
let sig_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(sig_bytes);
|
||||
|
||||
let license_json = serde_json::json!({
|
||||
"version": 1,
|
||||
"license_id": "test-license-id",
|
||||
"payload": payload_b64,
|
||||
"signature": {
|
||||
"algorithm": "RS256",
|
||||
"key_id": "test-key",
|
||||
"value": sig_b64
|
||||
}
|
||||
}).to_string();
|
||||
|
||||
let result = verify_and_parse(&license_json, &pub_pem);
|
||||
assert!(result.is_ok(), "{:?}", result.err());
|
||||
assert_eq!(result.unwrap().tenant_id, "test-tenant");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
pub mod cache;
|
||||
pub mod license;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::{
|
||||
CraftContext, LicenseInfo,
|
||||
device::DeviceFingerprint,
|
||||
error::LicenseError,
|
||||
};
|
||||
|
||||
pub struct SelfHostedConfig {
|
||||
pub base_url: String,
|
||||
pub tenant_key: String,
|
||||
pub offline_grace_days: u32,
|
||||
pub heartbeat_interval_hours: u32,
|
||||
pub public_key_pem: String,
|
||||
}
|
||||
|
||||
pub struct SelfHostedProvider {
|
||||
config: Option<SelfHostedConfig>,
|
||||
pub cache: cache::LicenseCache,
|
||||
pub device_fp: DeviceFingerprint,
|
||||
}
|
||||
|
||||
impl SelfHostedProvider {
|
||||
pub fn new() -> Self {
|
||||
let fp = crate::device::collect();
|
||||
let cache = cache::LicenseCache::load(&fp)
|
||||
.unwrap_or_else(|_| cache::LicenseCache {
|
||||
license: None,
|
||||
last_checkpoint: 0,
|
||||
cache_dir: std::path::PathBuf::new(),
|
||||
});
|
||||
|
||||
Self { config: None, cache, device_fp: fp }
|
||||
}
|
||||
|
||||
pub fn initialize(
|
||||
&mut self,
|
||||
base_url: String,
|
||||
tenant_key: String,
|
||||
offline_grace_days: u32,
|
||||
heartbeat_interval_hours: u32,
|
||||
public_key_pem: String,
|
||||
) -> Result<(), LicenseError> {
|
||||
self.config = Some(SelfHostedConfig {
|
||||
base_url,
|
||||
tenant_key,
|
||||
offline_grace_days,
|
||||
heartbeat_interval_hours,
|
||||
public_key_pem,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_license_offline(&self) -> Result<(), LicenseError> {
|
||||
let cached = self.cache.license.as_ref()
|
||||
.ok_or(LicenseError::NoCachedLicense)?;
|
||||
|
||||
license::validate_time_window(
|
||||
&license::LicensePayload {
|
||||
tenant_id: String::new(),
|
||||
product: String::new(),
|
||||
grant: license::Grant {
|
||||
grant_type: "cached".into(),
|
||||
not_before: cached.not_before,
|
||||
not_after: cached.not_after,
|
||||
offline_grace_days: cached.offline_grace_days,
|
||||
heartbeat_interval_hours: cached.heartbeat_interval_hours,
|
||||
},
|
||||
constraints: license::Constraints {
|
||||
max_devices: cached.max_devices,
|
||||
max_concurrent_users: cached.max_concurrent_users,
|
||||
max_activations: 0,
|
||||
},
|
||||
features: cached.features.clone(),
|
||||
custom: HashMap::new(),
|
||||
},
|
||||
cached.offline_grace_days,
|
||||
self.cache.last_checkpoint,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_license_info_offline(&self) -> LicenseInfo {
|
||||
if let Some(ref _cached) = self.cache.license {
|
||||
LicenseInfo {
|
||||
is_licensed: 1,
|
||||
expiration_date: 0,
|
||||
feature_names: std::ptr::null(),
|
||||
feature_values: std::ptr::null(),
|
||||
feature_count: 0,
|
||||
}
|
||||
} else {
|
||||
LicenseInfo {
|
||||
is_licensed: 0,
|
||||
expiration_date: 0,
|
||||
feature_names: std::ptr::null(),
|
||||
feature_values: std::ptr::null(),
|
||||
feature_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_feature_offline(&self, name: &str) -> bool {
|
||||
self.cache.license.as_ref()
|
||||
.and_then(|c| c.features.get(name))
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn verify_and_cache_license(
|
||||
&mut self,
|
||||
license_json: &str,
|
||||
) -> Result<(), LicenseError> {
|
||||
let cfg = self.config.as_ref().ok_or(LicenseError::NotInitialized)?;
|
||||
let payload = license::verify_and_parse(license_json, &cfg.public_key_pem)?;
|
||||
|
||||
let cached = license::to_cached_license(
|
||||
&payload,
|
||||
cfg.offline_grace_days,
|
||||
cfg.heartbeat_interval_hours,
|
||||
);
|
||||
self.cache.store(cached)?;
|
||||
self.cache.update_checkpoint();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn persist_cache(&self) -> Result<(), LicenseError> {
|
||||
self.cache.persist(&self.device_fp)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user