Java
Simple
Main class
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.
*
* Recommended obfuscators:
* - Zelix KlassMaster (https://www.zelix.com/)
* - JNIC (https://jnic.dev/)
*/
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
}
}
License class
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();
}
}
}
On this page