Notice: Usage on SpigotMC You are prohibited from implementing any form of licensing system in the premium plugins you sell on SpigotMC.org. For more details, refer to the Premium Resource Guidelines on SpigotMC, specifically the section regarding DRM systems.
Lukittu - The Ultimate Open-Source Software Licensing Solution Lukittu is a cutting-edge, open-source software licensing service designed to secure and track your applications. With powerful APIs, it adds an essential licensing layer to protect your proprietary software from unauthorized use and distribution. Lukittu is ideal for applications such as game scripts and add-ons on platforms like Minecraft, FiveM, and Roblox.
Key Features That Set Lukittu Apart
Fully Automated Licensing Flows & Integrations: Easily integrate with popular platforms like BuiltByBit, Stripe, Discord and others for seamless licensing management.
Comprehensive Customer & Configuration Management: Enjoy full control with powerful tools for customer management and flexible configuration options.
Real-Time Analytics & Detailed Audit Logs: Track usage and monitor security in real-time with in-depth analytics and audit logs.
Collaboration Made Easy: Manage multiple teams with ease and streamline collaboration on your projects.
Keys-In-Hand, No Hosting Required: Take advantage of free managed hosting without the hassle of managing servers yourself.
Completely Free & Open-Source: Fully open-source, allowing you to contribute and improve the system.
Community-Driven Development: Features and improvements are shaped by the community, ensuring Lukittu evolves based on real user needs.
Industry-Leading Security: Secure your applications with advanced features like Java classloader and robust industry standards.
Try Lukittu Now - Free Managed Hosting Lukittu offers a free managed hosting solution that you can start using instantly. No server setup is required, making it perfect for those who need a seamless, hassle-free experience.
Why Lukittu? Finding a reliable, open-source licensing system can be tough. I’ve been in this space since 2020, and the market has been saturated with limited options. Lukittu aims to fill this gap and become the go-to solution for software licensing.
This project is driven by the community—your feedback and contributions will directly influence its evolution. Lukittu is here to serve YOU, not to make a profit. All revenue goes toward covering hosting and operational costs.
Hosting Options Lukittu offers managed hosting for free, ensuring a smooth out-of-the-box experience. For those who prefer self-hosting, be aware that the system is designed for B2C SaaS, and self-hosting requires knowledge of Node.js and Docker.
Getting Started - Java & Code Examples Need help getting started? Check out our simple
Java code examples to see how easy it is to integrate Lukittu into your projects.
/** * Main plugin class for the LukittuSimple plugin with Lukittu license * verification. * Handles plugin lifecycle, license checks, and periodic heartbeat requests. * * ⚠️ SECURITY WARNING FOR PRODUCTION USE ⚠️ * * This implementation loads all license parameters from configuration for * simplicity. * In a production environment, only the customer license key should be * configurable. * * All other Lukittu parameters (team ID, product ID, public key) should be * hardcoded * in your compiled JAR and protected with code obfuscation to prevent * tampering. */ publicfinalclass Simple
extends JavaPlugin
{
/** * Static instance of the plugin for global access from other classes. */ publicstatic Simple INSTANCE
;
/** * Flag indicating whether the license validation was successful. * This is set by the LukittuLicenseVerify class after verification. */ publicboolean valid
;
/** * Called when the plugin is enabled by the server. * Handles configuration loading, license verification and setup. */ @Override
publicvoid onEnable
(){ INSTANCE
=this;
// Set up logging format setupLogging
();
// Save default config if it doesn't exist saveDefaultConfig
();
// Define a record to hold license configuration with proper types record LicenseConfig
(String key,
String teamId,
String productId,
String publicKey
){ }
// NOTE: In production, only load the license key from config // and hardcode other values for security var licenseConfig
=new LicenseConfig
( getConfig
().
getString("license.key",
""),
getConfig
().
getString("license.team-id",
""),
// Should be hardcoded in production getConfig
().
getString("license.product-id",
""),
// Should be hardcoded in production getConfig
().
getString("license.public-key",
""));// Should be hardcoded in production
// Check if all required license configuration values are provided if(licenseConfig.
key().
isEmpty()|| licenseConfig.
teamId().
isEmpty()|| licenseConfig.
productId().
isEmpty()){ logMessage
("License configuration missing. Check your config.yml!"); getServer
().
getPluginManager().
disablePlugin(this); return; }
// If license verification failed, disable the plugin if(!valid
){ // Verification already showed the appropriate error message getServer
().
getPluginManager().
disablePlugin(this); return; }
// Only setup heartbeat and enable plugin if license is valid setupHeartbeatScheduler
( licenseConfig.
key(),
licenseConfig.
teamId(),
licenseConfig.
productId());
logMessage
("Plugin enabled with valid license"); }catch(Exception e
){ getLogger
().
severe("Unexpected error during license verification: "+ e.
getMessage()); logMessage
("License verification failed due to an unexpected error"); // Ensure plugin is always disabled on any exception getServer
().
getPluginManager().
disablePlugin(this); } }
/** * Sets up a scheduled task to send periodic heartbeat requests to the license * server. * This keeps the license active and validates it's still in use. * * @param licenseKey The license key to validate * @param teamId The team ID for the license API * @param productId The product ID for the license API */ privatevoid setupHeartbeatScheduler
(String licenseKey,
String teamId,
String productId
){ scheduler
= Executors.
newSingleThreadScheduledExecutor(); scheduler.
scheduleAtFixedRate(()->{ try{ LukittuLicenseVerify.
sendHeartbeat(teamId, licenseKey, productId
); getLogger
().
fine("Heartbeat sent successfully"); }catch(Exception e
){ // Heartbeat failures should be silent, just log at warning level getLogger
().
log(Level.
WARNING,
"Failed to send heartbeat", e
); } },
15,
15, TimeUnit.
MINUTES); }
/** * Configure default logging level for the plugin */ privatevoid setupLogging
(){ getLogger
().
setLevel(Level.
INFO); }
/** * Logs a license-related message with a specific prefix to make it easily * identifiable * * @param message The message to log */ publicvoid logMessage
(String message
){ getLogger
().
info("LUKITTU LICENSE: "+ message
); }
/** * Called when the plugin is disabled by the server. * Handles graceful shutdown of background tasks. */ @Override
publicvoid onDisable
(){ // Shutdown the scheduler if it exists if(scheduler
!=null&&!scheduler.
isShutdown()){ scheduler.
shutdown(); try{ if(!scheduler.
awaitTermination(5, TimeUnit.
SECONDS)){ scheduler.
shutdownNow(); } }catch(InterruptedException e
){ scheduler.
shutdownNow(); Thread.
currentThread().
interrupt(); } }
/** * Handles license verification and validation for the Lukittu licensing system. * Manages the communication with the license server, validates responses, * and performs cryptographic verification of license challenges. * * ⚠️ SECURITY WARNING FOR PRODUCTION USE ⚠️ * * THIS IS A DEMONSTRATION IMPLEMENTATION ONLY. * * In a production environment: * 1. This entire class should be heavily obfuscated * 2. All constants (API URLs, team ID, product ID, public key) should be * hardcoded * and encrypted/obfuscated rather than loaded from config * 3. Only the license key itself should be configurable by end users * 4. Add anti-tampering measures to detect modifications to your code * 5. Consider using native code protection (JNI/JNIC) for critical sections * * Failure to properly protect this code may result in license bypass attempts. */ publicclass LukittuLicenseVerify
{
// Constants for API communication privatestaticfinalString RESULT_KEY
="result"; privatestaticfinalString VALID_KEY
="valid"; privatestaticfinalString API_BASE_URL
="https://app.lukittu.com/api/v1/client/teams"; privatestaticfinalString VERIFY_ENDPOINT
="/verification/verify"; privatestaticfinalString HEARTBEAT_ENDPOINT
="/verification/heartbeat"; privatestaticfinalString VERSION
="1.0.0"; privatestaticfinalint TIMEOUT_MILLIS
=10000;// 10 seconds privatestaticfinalString ERROR_CODE_KEY
="code"; privatestaticfinalString ERROR_DETAILS_KEY
="details";
// Static variables /** * Unique identifier for this server/device installation */ publicstaticString DEVICE_IDENTIFIER
;
/** * Map of error codes to user-friendly error messages */ privatestaticfinal Map
<String, String
> ERROR_MESSAGES
;
/** * JSON parser/formatter for API communication */ privatestaticfinal Gson GSON
=new GsonBuilder
() .
disableHtmlEscaping() .
setPrettyPrinting() .
create();
// Initialize static error messages static{ Map
<String, String
> messages
=new HashMap
<>(); messages.
put("RELEASE_NOT_FOUND",
"Invalid version specified in config."); messages.
put("LICENSE_NOT_FOUND",
"License not specified in config.yml, or it is invalid."); messages.
put("IP_LIMIT_REACHED",
"License's IP address limit has been reached. Contact support if you have issues with this."); messages.
put("MAXIMUM_CONCURRENT_SEATS",
"Maximum devices connected from the same license."); messages.
put("RATE_LIMIT",
"Too many connections in a short time from the same IP address. Please wait a while!"); messages.
put("LICENSE_EXPIRED",
"The license has expired."); messages.
put("INTERNAL_SERVER_ERROR",
"Upstream service has issues. Please notify support!"); messages.
put("BAD_REQUEST",
"Invalid request format or parameters. Check your license configuration."); ERROR_MESSAGES
=Collections.
unmodifiableMap(messages
); }
/** * Main license verification method that initiates the verification process. * Generates a challenge, sends it to the license server, and validates the * response. * * ⚠️ SECURITY NOTE: In production, only licenseKey should come from * configuration. * The teamId, productId, and publicKey should be hardcoded, obfuscated * constants. * * @param licenseKey The license key from config * @param teamId The team ID from config (should be hardcoded in production) * @param productId The product ID from config (should be hardcoded in * production) * @param publicKey The public key used to verify the server's signature * (should be hardcoded in production) * @throws IOException If a network or server error occurs * @throws Exception If validation fails for any reason */ publicstaticvoid verifyKey
(String licenseKey,
String teamId,
String productId,
String publicKey
) throwsException{ DEVICE_IDENTIFIER
= getHardwareIdentifier
();
// Generate a random challenge var challenge
= generateRandomChallenge
();
// Construct the URL for the API call with team ID var url
= API_BASE_URL
+"/"+ teamId
+ VERIFY_ENDPOINT
;
// Throw exception if verification failed to ensure it's caught in Simple's // onEnable if(!verificationSuccess
){ thrownewException("License verification failed"); } }
/** * Generates a random challenge string to prevent replay attacks. * The server will sign this challenge in its response. * * @return A secure random hex string to use as challenge */ privatestaticString generateRandomChallenge
(){ var secureRandom
=newSecureRandom(); var randomBytes
=newbyte[32]; secureRandom.
nextBytes(randomBytes
); return bytesToHex
(randomBytes
); }
/** * Utility method to convert byte arrays to hexadecimal strings. * * @param bytes The byte array to convert * @return A hex string representation of the bytes */ publicstaticString bytesToHex
(byte[] bytes
){ var result
=new StringBuilder
(bytes.
length*2); for(byte b
: bytes
){ result.
append(String.
format("%02x", b
)); } return result.
toString(); }
/** * Makes the HTTP request to the license server and processes the response. * * @param urlString The full URL of the license API endpoint * @param jsonBody The request body to send * @param publicKeyBase64 The public key to verify the response * @param challenge The challenge string that should be signed in the * response * @return true if verification succeeded, false if it failed * @throws IOException If a network error occurs */ publicstaticboolean fetchAndHandleResponse
(String urlString,
String jsonBody,
String publicKeyBase64,
String challenge
)throwsIOException{ HttpURLConnection connection
=null; boolean success
=false;
try(var os
= connection.
getOutputStream()){ var input
= jsonBody.
getBytes(StandardCharsets.
UTF_8); os.
write(input,
0, input.
length); }
int responseCode
= connection.
getResponseCode();
if(responseCode
==HttpURLConnection.
HTTP_OK){ try(var inputStream
= connection.
getInputStream()){ success
= handleJsonResponse
(inputStream, publicKeyBase64, challenge
); } }else{ try(var errorStream
= connection.
getErrorStream()){ // Try to extract error details from the error response if(errorStream
!=null){ handleJsonResponse
(errorStream,
null,
null); } } // Show HTTP error if in 4xx or 5xx range if(responseCode
>=400){ Simple.
INSTANCE.
logMessage("HTTP Error: "+ responseCode
+ " - Check your team ID, product ID and license key"); } } }catch(Exception e
){ Simple.
INSTANCE.
getLogger().
log(Level.
SEVERE,
"Connection to Lukittu service failed", e
); Simple.
INSTANCE.
logMessage("Connection failure! Check server connectivity"); try{ if(connection
!=null&& connection.
getErrorStream()!=null){ handleJsonResponse
(connection.
getErrorStream(),
null,
null); } }catch(IOException e1
){ Simple.
INSTANCE.
getLogger().
log(Level.
SEVERE,
"Failed to parse error response", e1
); } thrownewIOException("Connection to license server failed", e
); }finally{ if(connection
!=null){ connection.
disconnect(); } }
return success
; }
/** * Parses and validates the JSON response from the license server. * If verification succeeds, it sets the valid state in the main plugin. * * @param inputStream The input stream containing the JSON response * @param publicKey The public key for signature verification * @param challenge The original challenge to verify * @throws IOException If there's an error reading the response * @return true if validation was successful, false if errors were encountered */ privatestaticboolean handleJsonResponse
(InputStream inputStream,
String publicKey,
String challenge
) throwsIOException{ if(inputStream
==null){ thrownewIOException("Input stream is null"); }
// If we didn't find specific error info, use the general handling return!handleErrorCodes
(respString
); } }
/** * Validates the digital signature of the challenge in the server response. * This proves the response came from the legitimate license server. * * @param response The JSON response from the server * @param originalChallenge The original challenge we sent * @param base64PublicKey The public key to verify the signature * @return true if the signature is valid, false otherwise */ publicstaticboolean validateChallenge
(JsonObject response,
String originalChallenge,
String base64PublicKey
){ try{ if(!validateResponse
(response
)|| originalChallenge
==null|| base64PublicKey
==null){ returnfalse; }
var signedChallenge
= response.
getAsJsonObject(RESULT_KEY
) .
get("challengeResponse").
getAsString();
return verifySignature
(originalChallenge, signedChallenge, base64PublicKey
); }catch(Exception e
){ Simple.
INSTANCE.
getLogger().
log(Level.
SEVERE,
"Challenge validation failed", e
); Simple.
INSTANCE.
logMessage("Signature verification failed! Possible tampering detected"); returnfalse; } }
/** * Performs the actual cryptographic signature verification. * Uses RSA with SHA256 to verify that the challenge was signed by the license * server. * * @param challenge The original challenge string * @param signatureHex The hex-encoded signature to verify * @param base64PublicKey The base64-encoded public key * @return true if the signature is valid, false otherwise */ publicstaticboolean verifySignature
(String challenge,
String signatureHex,
String base64PublicKey
){ try{ var signatureBytes
= hexStringToByteArray
(signatureHex
); var decodedKeyBytes
= Base64.
getDecoder().
decode(base64PublicKey
);
var decodedKeyString
=newString(decodedKeyBytes
) .
replace("-----BEGIN PUBLIC KEY-----",
"") .
replace("-----END PUBLIC KEY-----",
"") .
replaceAll("\\s",
"");
var publicKeyBytes
= Base64.
getDecoder().
decode(decodedKeyString
); var keySpec
=newX509EncodedKeySpec(publicKeyBytes
); var keyFactory
=KeyFactory.
getInstance("RSA"); var publicKey
= keyFactory.
generatePublic(keySpec
);
var signature
=Signature.
getInstance("SHA256withRSA"); signature.
initVerify(publicKey
); signature.
update(challenge.
getBytes());
return signature.
verify(signatureBytes
); }catch(IllegalArgumentException e
){ Simple.
INSTANCE.
getLogger().
log(Level.
SEVERE,
"Invalid Base64 input for public key", e
); Simple.
INSTANCE.
logMessage("Invalid public key format! Contact support"); returnfalse; }catch(Exception e
){ Simple.
INSTANCE.
getLogger().
log(Level.
SEVERE,
"Signature verification failed", e
); returnfalse; } }
/** * Utility method to convert a hex string to a byte array. * * @param hex The hex string to convert * @return The resulting byte array */ privatestaticbyte[] hexStringToByteArray
(String hex
){ var len
= hex.
length(); var data
=newbyte[len
/2]; for(int i
=0; i
< len
; i
+=2){ data
[i
/2]=(byte)((Character.
digit(hex.
charAt(i
),
16)<<4) +Character.
digit(hex.
charAt(i
+1),
16)); } return data
; }
/** * Checks if the license server response indicates a valid license. * * @param json The JSON response object to validate * @return true if the license is valid, false otherwise */ privatestaticboolean validateResponse
(JsonObject json
){ try{ var result
= json.
getAsJsonObject(RESULT_KEY
); return result
!=null&& result.
has(VALID_KEY
)&& result.
get(VALID_KEY
).
getAsBoolean(); }catch(Exception e
){ returnfalse; } }
/** * Updates the main plugin to indicate the license is valid. * Uses reflection to avoid direct dependencies. */ privatestaticvoid setValidState
(){ try{ var validField
= Simple.
class.
getDeclaredField("valid"); validField.
setAccessible(true); validField.
set(Simple.
INSTANCE,
true); }catch(Exception e
){ Simple.
INSTANCE.
getLogger().
log(Level.
WARNING,
"Failed to set valid state", e
); } }
/** * Builds a consistent User-Agent string for API requests. * Helps with tracking and debugging on the server side. * * @return The formatted User-Agent string */ privatestaticString buildUserAgent
(){ returnString.
format("LukittuLoader/%s (%s %s; %s)",
VERSION,
System.
getProperty("os.name"),
System.
getProperty("os.version"),
System.
getProperty("os.arch")); }
/** * Checks API responses for known error codes and provides user-friendly * messages. * * @param response The response string to check * @return true if an error was found and handled, false otherwise */ privatestaticboolean handleErrorCodes
(finalString response
){ if(response
==null){ returnfalse; }
// Find specific error in the response var errorEntry
= findErrorInResponse
(response
);
// Handle error if found if(errorEntry.
isPresent()){ var errorMessage
= errorEntry.
get().
getValue(); Simple.
INSTANCE.
getLogger().
severe(errorMessage
); Simple.
INSTANCE.
logMessage("Error: "+ errorMessage
); returntrue; }
// Generic error for any validation failure not otherwise caught if(response.
contains("\"valid\":false")){ Simple.
INSTANCE.
logMessage("License validation failed. Check your license configuration"); returntrue; }
// No error found returnfalse; }
/** * Finds the first matching error code in the response. * * @param response The API response to search * @return An Optional containing the matching error entry, or empty if none * found */ privatestatic Optional
<Map.
Entry<String, String
>> findErrorInResponse
(String response
){ return ERROR_MESSAGES.
entrySet().
stream() .
filter(entry
-> response.
contains(entry.
getKey())) .
findFirst(); }
/** * Sends a periodic heartbeat to the license server. * This keeps the license active and helps detect license violations. * * @param teamId The team ID associated with the license * @param licenseKey The license key to validate * @param productId The product ID associated with the license * @throws Exception If there's an error sending the heartbeat */ publicstaticvoid sendHeartbeat
(String teamId,
String licenseKey,
String productId
)throwsException{ var urlString
= API_BASE_URL
+"/"+ teamId
+ HEARTBEAT_ENDPOINT
; var url
= URI.
create(urlString
).
toURL();
try(var os
= connection.
getOutputStream()){ var input
= jsonBody.
getBytes(StandardCharsets.
UTF_8); os.
write(input,
0, input.
length); }
int responseCode
= connection.
getResponseCode();
// Read the response for logging purposes, even if we don't use it try(var is
=(responseCode
<HttpURLConnection.
HTTP_BAD_REQUEST) ? connection.
getInputStream() : connection.
getErrorStream(); var br
=newBufferedReader(newInputStreamReader(is
))){
var response
=new StringBuilder
(); String line
; while((line
= br.
readLine())!=null){ response.
append(line
); }
if(responseCode
>=HttpURLConnection.
HTTP_BAD_REQUEST){ Simple.
INSTANCE.
getLogger().
warning("Heartbeat failed with response code: "+ responseCode
); handleErrorCodes
(response.
toString()); } }catch(IOException e
){ Simple.
INSTANCE.
getLogger().
log(Level.
WARNING,
"Failed to read heartbeat response", e
); }finally{ connection.
disconnect(); } }
/** * Generates a unique identifier for this server/device based on system * properties. * Used to limit the number of installations per license. * * Note: On virtual machines or containers, this may change between restarts. * * @return A UUID based on system properties or a random UUID if hardware info * can't be retrieved */ publicstaticString getHardwareIdentifier
(){ try{ // Use standard approach instead of StructuredTaskScope (preview API) var osName
=System.
getProperty("os.name"); var osVersion
=System.
getProperty("os.version"); var osArch
=System.
getProperty("os.arch"); var hostname
=InetAddress.
getLocalHost().
getHostName();
var combinedIdentifier
= osName
+ osVersion
+ osArch
+ hostname
; return UUID.
nameUUIDFromBytes(combinedIdentifier.
getBytes()).
toString(); }catch(Exception e
){ Simple.
INSTANCE.
getLogger().
warning("Failed to get hardware identifier: "+ e.
getMessage()); Simple.
INSTANCE.
logMessage("Hostname retrieval failed, using random identifier"); return UUID.
randomUUID().
toString(); } } }
Support Lukittu! Love what you see? Support the project by starring it on GitHub! It’s a free and easy way to show your support and help Lukittu grow. You can also earn exclusive Stargazer and Contributor roles on our Discord server.