//! LeTissier licence verification — Rust. //! Used by Vizz and Light. //! //! Verification is entirely offline: the app embeds the studio's public key //! and checks a signed token. No network, no shared secret in the binary. use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use ed25519_dalek::{Signature, VerifyingKey}; use serde::Deserialize; use sha2::{Digest, Sha256}; /// The studio's licence signing key. Public: safe to ship in a binary. pub const PUBLIC_KEY_HEX: &str = "REPLACE_WITH_YOUR_PUBLIC_KEY_HEX"; #[derive(Debug, Clone, Deserialize)] pub struct Claims { pub v: u8, pub key: String, pub product: String, pub edition: String, pub customer: String, #[serde(default)] pub name: Option, pub seats: u32, /// Entitled to builds released at or before this unix time. #[serde(rename = "maintUntil")] pub maint_until: i64, /// Check-in deadline for this lease. pub exp: i64, pub machine: String, pub mode: String, pub iat: i64, pub jti: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { /// Good to run. Active, /// Licence is fine, but this build is newer than the update entitlement. UpdateRequired, /// Lease lapsed. Check in to renew; app policy decides any grace. CheckInRequired, /// A trial that has run out. Expired, /// Token was issued for a different machine. WrongMachine, /// Signature failed, malformed, or wrong version. Invalid, } impl Status { /// Stable string, matching the other language SDKs. pub fn as_str(self) -> &'static str { match self { Status::Active => "active", Status::UpdateRequired => "update_required", Status::CheckInRequired => "check_in_required", Status::Expired => "expired", Status::WrongMachine => "wrong_machine", Status::Invalid => "invalid", } } } #[derive(Debug, Clone)] pub struct Verdict { pub status: Status, pub claims: Option, /// Seconds until check-in is due; negative once overdue. pub check_in_in: Option, } /// Must match the server exactly: sha256 of the trimmed fingerprint, first 32 /// hex characters. pub fn machine_hash(fingerprint: &str) -> String { let digest = Sha256::digest(fingerprint.trim().as_bytes()); hex::encode(digest)[..32].to_string() } /// Verify the signature and parse the claims. No clock or machine checks. pub fn verify(token: &str, public_key_hex: &str) -> Option { let (payload, signature) = token.split_once('.')?; let key_bytes: [u8; 32] = hex::decode(public_key_hex).ok()?.try_into().ok()?; let key = VerifyingKey::from_bytes(&key_bytes).ok()?; let signature_bytes: [u8; 64] = URL_SAFE_NO_PAD.decode(signature).ok()?.try_into().ok()?; key.verify_strict(payload.as_bytes(), &Signature::from_bytes(&signature_bytes)) .ok()?; let claims: Claims = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(payload).ok()?).ok()?; if claims.v == 1 { Some(claims) } else { None } } /// The whole decision, offline. /// /// `build_date` is when THIS build was released (unix seconds), baked in at /// compile time. An older build stays entitled forever; a newer one asks for a /// renewal. That is what makes "a year of updates, yours to keep" work with no /// server involved. pub fn check( token: &str, fingerprint: &str, build_date: i64, now: i64, public_key_hex: &str, ) -> Verdict { let claims = match verify(token, public_key_hex) { Some(claims) => claims, None => { return Verdict { status: Status::Invalid, claims: None, check_in_in: None, } } }; if claims.machine != machine_hash(fingerprint) { return Verdict { status: Status::WrongMachine, claims: Some(claims), check_in_in: None, }; } let check_in_in = claims.exp - now; if check_in_in <= 0 { // A trial's lease is its lifetime, so a lapsed trial is simply over. let status = if claims.edition == "trial" { Status::Expired } else { Status::CheckInRequired }; return Verdict { status, claims: Some(claims), check_in_in: Some(check_in_in), }; } if build_date > claims.maint_until { return Verdict { status: Status::UpdateRequired, claims: Some(claims), check_in_in: Some(check_in_in), }; } Verdict { status: Status::Active, claims: Some(claims), check_in_in: Some(check_in_in), } }