Description
Lukittu is a modern software licensing service that provides robust APIs to enhance the security and trackability of your applications. It introduces a licensing layer to protect proprietary software from unauthorized sharing and misuse, offering benefits such as enhanced security, usage analytics, easy integration, and flexible licensing. Lukittu is particularly suitable for applications like game scripts and add-ons running on client servers, including platforms like Minecraft, FiveM, and Roblox.
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.
Features
-
Simple Integration: Easily incorporate licensing into your plugins with minimal code.
-
Advanced Watermarking Technology: Utilizes sophisticated tracing and anti-tampering measures to safeguard your plugins.
-
Developer-Friendly API: Offers straightforward integration with Spigot, Bukkit, and Paper plugins through comprehensive documentation.
-
Usage Restrictions: Allows limitation of plugin usage to specific IP addresses or concurrent users.
-
Enterprise-Grade Security: Employs modern security standards, including RSA 2048-bit encryption and server-side verification, to ensure robust protection.
-
Usage Tracking: Enables monitoring of plugin usage and active installations.
-
Automatic Delivery: Integrates with existing systems to automate license delivery upon purchase.
Integration
Code (Java):
package
app.lukittu.lukkittujavaexample
;
import
org.bukkit.plugin.java.JavaPlugin
;
import
java.util.concurrent.Executors
;
import
java.util.concurrent.ScheduledExecutorService
;
import
java.util.concurrent.TimeUnit
;
/**
* Main plugin class for the Lukittu Java Example.
* This class handles license verification and periodic heartbeat checks.
* Note: Both this main class and the license verification class should be
* heavily obfuscated and native obfuscated for security purposes.
*/
public
final
class LukkittuJavaExample
extends JavaPlugin
{
/**
* Static instance of the plugin for global access.
*/
public
static LukkittuJavaExample INSTANCE
;
/**
* Flag indicating whether the license is valid.
*/
public
boolean valid
;
/**
* Called when the plugin is enabled.
* Initializes the plugin instance, verifies the license key,
* and sets up periodic heartbeat checks.
*/
@Override
public
void onEnable
(
)
{
INSTANCE
=
this
;
String licenseKey
=
"KEY"
;
// Verify the license key on startup
LukittuLicenseVerify.
verifyKey
(licenseKey
)
;
if
(
!valid
)
{
// Handle invalid license case
// Implementation should be added here
return
;
}
// Set up periodic heartbeat checks every 15 minutes
setupHeartbeatScheduler
(licenseKey
)
;
}
/**
* Sets up a scheduled executor to send periodic heartbeat requests.
*
* @param licenseKey The license key to validate in heartbeat requests
*/
private
void setupHeartbeatScheduler
(
String licenseKey
)
{
ScheduledExecutorService scheduler
= Executors.
newScheduledThreadPool
(
1
)
;
scheduler.
scheduleAtFixedRate
(
(
)
->
{
try
{
LukittuLicenseVerify.
sendHeartbeat
(
"TEAM_ID", licenseKey,
"PRODUCT_ID"
)
;
}
catch
(
Exception ignored
)
{
// Heartbeat failures are silently ignored
}
},
15,
15, TimeUnit.
MINUTES
)
;
}
/**
* Called when the plugin is disabled.
* Current implementation contains no shutdown logic.
*/
@Override
public
void onDisable
(
)
{
// Plugin shutdown logic
}
}
Code (Java):
package
app.lukittu.lukkittujavaexample
;
import
java.io.BufferedReader
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStreamReader
;
import
java.io.OutputStream
;
import
java.io.Reader
;
import
java.lang.reflect.Field
;
import
java.net.HttpURLConnection
;
import
java.net.InetAddress
;
import
java.net.URL
;
import
java.nio.charset.StandardCharsets
;
import
java.security.KeyFactory
;
import
java.security.PublicKey
;
import
java.security.SecureRandom
;
import
java.security.Signature
;
import
java.security.spec.X509EncodedKeySpec
;
import
java.util.Base64
;
import
java.util.Collections
;
import
java.util.HashMap
;
import
java.util.Map
;
import
java.util.UUID
;
import
com.google.gson.Gson
;
import
com.google.gson.GsonBuilder
;
import
com.google.gson.JsonObject
;
/**
* Handles license verification and validation for the Lukittu licensing system.
* This class should be natively obfuscated along with the main class of the JAR
* for better security. It is recommended to encrypt the PUBLIC_KEY_BASE_64
* using
* your own encryption method, as most public obfuscators have reverse tools for
* string obfuscation.
*/
public
class LukittuLicenseVerify
{
private
static
final
String R_KEY
=
"re"
+
"su"
+
"lt"
;
private
static
final
String V_KEY
=
"va"
+
"li"
+
"d"
;
private
static
final Map
<
String, String
> ERROR_MESSAGES
;
public
static
String DEVICE_IDENTIFIER
;
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!"
)
;
ERROR_MESSAGES
=
Collections.
unmodifiableMap
(messages
)
;
}
/**
* Verifies a license key by making an API call to the Lukittu verification
* service.
* Generates a random challenge and validates the response signature.
*
* @param key The license key to verify
*/
public
static
void verifyKey
(
String key
)
{
DEVICE_IDENTIFIER
= getHardwareIdentifier
(
)
;
String PUBLIC_KEY_BASE_64
=
"YOUR_PUBLIC_KEY"
;
// Generate a random challenge
SecureRandom secureRandom
=
new
SecureRandom
(
)
;
byte
[
] randomBytes
=
new
byte
[
32
]
;
secureRandom.
nextBytes
(randomBytes
)
;
String challenge
= bytesToHex
(randomBytes
)
;
// Construct the URL for the API call with team ID
String TEAM_ID
=
"YOUR_TEAM_ID"
;
String url
=
"https://app.lukittu.com/api/v1/client/teams/${TEAM_ID}/verification/verify"
.
replace
(
"${TEAM_ID}", TEAM_ID
)
;
String jsonBody
=
String.
format
(
"{\n"
+
" \"licenseKey\": \"%s\",\n"
+
" \"productId\": \"%s\",\n"
+
" \"challenge\": \"%s\",\n"
+
" \"version\": \"%s\",\n"
+
" \"deviceIdentifier\": \"%s\"\n"
+
"}", key,
"YOUR_PRODUCT_ID", challenge,
"1.0.0", DEVICE_IDENTIFIER
)
;
fetchAndHandleResponse
(url, jsonBody, PUBLIC_KEY_BASE_64, challenge
)
;
}
/**
* Converts a byte array to its hexadecimal string representation.
*
* @param bytes The byte array to convert
* @return A string containing the hexadecimal representation of the bytes
*/
public
static
String bytesToHex
(
byte
[
] bytes
)
{
StringBuilder result
=
new StringBuilder
(bytes.
length
*
2
)
;
for
(
byte b
: bytes
)
{
result.
append
(
String.
format
(
"%02x", b
)
)
;
}
return result.
toString
(
)
;
}
/**
* Makes an HTTP request to the Lukittu API and handles the response.
*
* @param urlString The URL to send the request to
* @param jsonBody The JSON request body
* @param PUBLIC_KEY_BASE_64 The public key used for response verification
* @param challenge The challenge string to verify in the response
*/
public
static
void fetchAndHandleResponse
(
String urlString,
String jsonBody,
String PUBLIC_KEY_BASE_64,
String challenge
)
{
HttpURLConnection connection
=
null
;
try
{
URL url
=
new
URL
(urlString
)
;
connection
=
(
HttpURLConnection
) url.
openConnection
(
)
;
connection.
setRequestMethod
(
"POST"
)
;
connection.
setRequestProperty
(
"Content-Type",
"application/json"
)
;
connection.
setRequestProperty
(
"User-Agent", buildUserAgent
(
)
)
;
connection.
setDoOutput
(
true
)
;
try
(
OutputStream os
= connection.
getOutputStream
(
)
)
{
byte
[
] input
= jsonBody.
getBytes
(StandardCharsets.
UTF_8
)
;
os.
write
(input,
0, input.
length
)
;
}
if
(connection.
getResponseCode
(
)
==
HttpURLConnection.
HTTP_OK
)
{
handleJsonResponse
(connection.
getInputStream
(
), PUBLIC_KEY_BASE_64, challenge
)
;
}
}
catch
(
Exception e
)
{
try
{
handleJsonResponse
(connection.
getErrorStream
(
),
null,
null
)
;
}
catch
(
IOException e1
)
{
System.
out.
println
(
"ERROR: JSON response: "
+ e1.
getMessage
(
)
)
;
}
}
finally
{
if
(connection
!=
null
)
{
connection.
disconnect
(
)
;
}
}
}
/**
* Processes the JSON response from the API and validates its contents.
*
* @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
*/
private
static
void handleJsonResponse
(
InputStream inputStream,
String publickey,
String challenge
)
throws
IOException
{
if
(inputStream
==
null
)
{
throw
new
IOException
(
"Input stream is null"
)
;
}
final Gson gson
=
new GsonBuilder
(
)
.
disableHtmlEscaping
(
)
.
setPrettyPrinting
(
)
.
create
(
)
;
try
(
Reader reader
=
new
BufferedReader
(
new
InputStreamReader
(inputStream, StandardCharsets.
UTF_8
)
)
)
{
JsonObject json
= gson.
fromJson
(reader, JsonObject.
class
)
;
if
(validateResponse
(json
)
&& validateChallenge
(json, challenge, publickey
)
)
{
setValidState
(
)
;
return
;
}
String resp
= gson.
toJson
(json
)
;
logResponse
(resp
)
;
handleErrorCodes
(resp
)
;
}
}
/**
* Validates the challenge response from the server.
*
* @param response The JSON response object from the server
* @param originalChallenge The original challenge string sent to the server
* @param base64PublicKey The base64-encoded public key for signature
* verification
* @return true if the challenge response is valid, false otherwise
*/
public
static
boolean validateChallenge
(JsonObject response,
String originalChallenge,
String base64PublicKey
)
{
try
{
if
(
!validateResponse
(response
)
)
{
return
false
;
}
String signedChallenge
= response.
getAsJsonObject
(
"result"
)
.
get
(
"challengeResponse"
).
getAsString
(
)
;
return verifySignature
(originalChallenge, signedChallenge, base64PublicKey
)
;
}
catch
(
Exception e
)
{
e.
printStackTrace
(
)
;
return
false
;
}
}
/**
* Verifies the digital signature of the challenge response.
*
* @param challenge The original challenge string
* @param signatureHex The hexadecimal signature to verify
* @param base64PublicKey The base64-encoded public key
* @return true if the signature is valid, false otherwise
*/
public
static
boolean verifySignature
(
String challenge,
String signatureHex,
String base64PublicKey
)
{
try
{
byte
[
] signatureBytes
= hexStringToByteArray
(signatureHex
)
;
byte
[
] decodedKeyBytes
= Base64.
getDecoder
(
).
decode
(base64PublicKey
)
;
String decodedKeyString
=
new
String
(decodedKeyBytes
)
.
replace
(
"-----BEGIN PUBLIC KEY-----",
""
)
.
replace
(
"-----END PUBLIC KEY-----",
""
)
.
replaceAll
(
"\\s",
""
)
;
byte
[
] publicKeyBytes
= Base64.
getDecoder
(
).
decode
(decodedKeyString
)
;
X509EncodedKeySpec keySpec
=
new
X509EncodedKeySpec
(publicKeyBytes
)
;
KeyFactory keyFactory
=
KeyFactory.
getInstance
(
"RSA"
)
;
PublicKey publicKey
= keyFactory.
generatePublic
(keySpec
)
;
Signature signature
=
Signature.
getInstance
(
"SHA256withRSA"
)
;
signature.
initVerify
(publicKey
)
;
signature.
update
(challenge.
getBytes
(
)
)
;
return signature.
verify
(signatureBytes
)
;
}
catch
(
IllegalArgumentException e
)
{
System.
err.
println
(
"Invalid Base64 input for public key."
)
;
e.
printStackTrace
(
)
;
return
false
;
}
catch
(
Exception e
)
{
e.
printStackTrace
(
)
;
return
false
;
}
}
/**
* Converts a hexadecimal string to a byte array.
*
* @param hex The hexadecimal string to convert
* @return The resulting byte array
*/
private
static
byte
[
] hexStringToByteArray
(
String hex
)
{
int len
= hex.
length
(
)
;
byte
[
] data
=
new
byte
[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
;
}
/**
* Validates the structure and content of the API response.
*
* @param json The JSON response object to validate
* @return true if the response is valid, false otherwise
*/
private
static
boolean validateResponse
(JsonObject json
)
{
try
{
JsonObject result
= json.
getAsJsonObject
(R_KEY
)
;
return result
!=
null
&&
result.
has
(V_KEY
)
&&
result.
get
(V_KEY
).
getAsBoolean
(
)
;
}
catch
(
Exception e
)
{
return
false
;
}
}
/**
* Sets the valid state in the main application class.
*/
private
static
void setValidState
(
)
{
try
{
Field validField
= LukkittuJavaExample.
class.
getDeclaredField
(
"valid"
)
;
validField.
setAccessible
(
true
)
;
validField.
set
(LukkittuJavaExample.
INSTANCE,
true
)
;
}
catch
(
Exception ignored
)
{
}
}
/**
* Builds the User-Agent string for API requests.
*
* @return The formatted User-Agent string
*/
private
static
String buildUserAgent
(
)
{
return
String.
format
(
"LukittuLoader/%s (%s %s; %s)",
"1.0",
System.
getProperty
(
"os.name"
),
System.
getProperty
(
"os.version"
),
System.
getProperty
(
"os.arch"
)
)
;
}
/**
* Logs the API response for debugging purposes.
*
* @param response The response string to log
*/
private
static
void logResponse
(
String response
)
{
if
(response
!=
null
)
{
System.
out.
println
(
"Received JSON response (pretty printed):"
)
;
System.
out.
println
(response
)
;
}
}
/**
* Handles error codes from the API response and prints appropriate messages.
*
* @param response The response string to check for error codes
*/
private
static
void handleErrorCodes
(
final
String response
)
{
if
(response
==
null
)
return
;
ERROR_MESSAGES.
entrySet
(
).
stream
(
)
.
filter
(entry
-> response.
contains
(entry.
getKey
(
)
)
)
.
findFirst
(
)
.
ifPresent
(entry
->
System.
err.
println
(entry.
getValue
(
)
)
)
;
}
/**
* Sends a heartbeat request to the Lukittu verification service.
*
* @param TEAM_ID The team ID for the API request
* @param LICENSE_KEY The license key to validate
* @param PRODUCT_ID The product ID associated with the license
* @throws Exception If there's an error sending the heartbeat
*/
public
static
void sendHeartbeat
(
String TEAM_ID,
String LICENSE_KEY,
String PRODUCT_ID
)
throws
Exception
{
String urlString
=
"https://app.lukittu.com/api/v1/client/teams/${TEAM_ID}/verification/heartbeat"
.
replace
(
"${TEAM_ID}", TEAM_ID
)
;
URL url
=
new
URL
(urlString
)
;
HttpURLConnection connection
=
(
HttpURLConnection
) url.
openConnection
(
)
;
connection.
setRequestMethod
(
"POST"
)
;
connection.
setRequestProperty
(
"Content-Type",
"application/json"
)
;
connection.
setRequestProperty
(
"User-Agent", buildUserAgent
(
)
)
;
connection.
setDoOutput
(
true
)
;
String jsonBody
=
String.
format
(
"{"
+
"\"licenseKey\":\"%s\","
+
"\"productId\":\"%s\","
+
"\"deviceIdentifier\":\"%s\""
+
"}", LICENSE_KEY, PRODUCT_ID, DEVICE_IDENTIFIER
)
;
try
(
OutputStream os
= connection.
getOutputStream
(
)
)
{
byte
[
] input
= jsonBody.
getBytes
(StandardCharsets.
UTF_8
)
;
os.
write
(input,
0, input.
length
)
;
}
int responseCode
= connection.
getResponseCode
(
)
;
try
(
InputStream is
=
(responseCode
<
HttpURLConnection.
HTTP_BAD_REQUEST
)
? connection.
getInputStream
(
)
: connection.
getErrorStream
(
)
;
BufferedReader br
=
new
BufferedReader
(
new
InputStreamReader
(is
)
)
)
{
StringBuilder response
=
new StringBuilder
(
)
;
String line
;
while
(
(line
= br.
readLine
(
)
)
!=
null
)
{
response.
append
(line
)
;
}
}
catch
(
IOException ignored
)
{
}
connection.
disconnect
(
)
;
}
/**
* Generates a hardware identifier based on system properties.
* Note: This method may not work reliably in Docker environments.
*
* @return A unique hardware identifier string
*/
public
static
String getHardwareIdentifier
(
)
{
try
{
String osName
=
System.
getProperty
(
"os.name"
)
;
String osVersion
=
System.
getProperty
(
"os.version"
)
;
String osArch
=
System.
getProperty
(
"os.arch"
)
;
String hostname
=
InetAddress.
getLocalHost
(
).
getHostName
(
)
;
String combinedIdentifier
= osName
+ osVersion
+ osArch
+ hostname
;
return UUID.
nameUUIDFromBytes
(combinedIdentifier.
getBytes
(
)
).
toString
(
)
;
}
catch
(
Exception e
)
{
LukkittuJavaExample.
INSTANCE.
getLogger
(
).
info
(
"Hostname getting failed, contact support"
)
;
return UUID.
randomUUID
(
).
toString
(
)
;
}
}
}
Support
You can join our community Discord-server for support:
https://discord.lukittu.com
If you like Lukittu consider giving it star on GitHub! It's free way to support the project!
Links:
GitHub-link
Discord-link
![[IMG]](//proxy.spigotmc.org/699ee58766fa5aeaf49f09281fd49c3b32e75939/68747470733a2f2f696d6775722e636f6d2f746a42477030482e706e67)