Java ClassLoader validation
This is simple class for license validation with class loader in Java.
Example class
This is simple class for license validation in Java.
- You need fill
SECRET_KEY
constant with your account secret key. The secret key is used only to identify your licenses. - You need fill all constructor parameters from application.
You can replace this class with your own implementation of license validation, remember for use same checks.
java
package org.example;
import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Logger;
public class LicenseValidator {
private static final String API_URL = "https://valid.mlicense.net/api/v1/validation";
private static final String UNKNOWN = "unknown";
private static final String OS = System.getProperty("os.name").toLowerCase();
private final Gson gson;
private final Logger logger;
private final String key;
private final String secretKey;
private final String fileKey;
private final String responseKey;
private final String product;
private final String version;
private final List<String> enabledAddons;
private UUID requestUniqueId;
public LicenseValidator(String secretKey, String fileKey, String responseKey, String key, String product, String version, List<String> enabledAddons) {
this.gson = new Gson();
this.logger = Logger.getLogger("mLicense");
this.secretKey = secretKey;
this.fileKey = fileKey;
this.responseKey = responseKey;
this.key = key;
this.product = product;
this.version = version;
this.enabledAddons = enabledAddons;
}
public LicenceResponse isValid(ClassLoader parentClassLoader) throws Exception {
long now = System.currentTimeMillis();
DecryptedResponse decryptedResponse = getDecryptedResponse();
Status status = checkLicense(decryptedResponse);
JsonObject object = status.object;
if (!status.isValid()) {
String reason = determineErrorReason(status);
logger.info("License is invalid: " + reason);
return new LicenceResponse(false, null, null, null, null, null);
}
logger.info("License is valid! (took: " + (System.currentTimeMillis() - now) + "ms)");
return new LicenceResponse(
true,
new LinceseClassLoader(readFiles(object), parentClassLoader),
object.get("mainClass").getAsString(),
object.get("version").getAsString(),
readAvailableAddons(object),
readEnabledAddons(object)
);
}
private List<String> readAvailableAddons(JsonObject object) {
List<String> addons = new ArrayList<>();
JsonArray addonsArray = object.getAsJsonArray("availableAddons");
addonsArray.forEach((addon) -> {
addons.add(addon.getAsString());
});
return addons;
}
private List<Addon> readEnabledAddons(JsonObject object) {
List<Addon> addons = new ArrayList<>();
JsonArray addonsArray = object.getAsJsonArray("enabledAddons");
addonsArray.forEach((addon) -> {
JsonObject jsonObject = addon.getAsJsonObject();
addons.add(new Addon(
jsonObject.get("name").getAsString(),
jsonObject.get("mainClass").getAsString(),
jsonObject.get("priority").getAsInt()
));
});
addons.sort(Comparator.comparing(Addon::priority));
Collections.reverse(addons);
return addons;
}
private List<byte[]> readFiles(JsonObject object) {
List<String> files = new ArrayList<>();
JsonArray filesArray = object.getAsJsonArray("files");
filesArray.forEach((file) -> {
files.add(file.getAsString());
});
List<byte[]> filesBytes = files.stream().map((file) -> Base64.getDecoder().decode(file)).toList();
byte[] fileKeyBytes = Base64.getDecoder().decode(fileKey);
SecretKeySpec fileKey = new SecretKeySpec(fileKeyBytes, 0, fileKeyBytes.length, "AES");
return filesBytes.stream().map((file) -> {
try {
return decrypt(file, fileKey);
} catch (Exception e) {
throw new RuntimeException("Cannot decrypt file!", e);
}
}).toList();
}
private String determineErrorReason(Status status) {
try {
return status.object().get("message").getAsString();
} catch (Exception e) {
return "Unknown error";
}
}
private DecryptedResponse getDecryptedResponse() throws Exception {
byte[] responseKeyBytes = Base64.getDecoder().decode(responseKey);
HttpResponse<byte[]> response = sendLicenseRequest();
byte statusCode = (byte) response.statusCode();
byte[] body = response.body();
byte[] decrypted = decrypt(body, new SecretKeySpec(responseKeyBytes, 0, responseKeyBytes.length, "AES"));
String bodyString = new String(decrypted);
JsonObject parsed = parse(bodyString);
return new DecryptedResponse(
parsed,
statusCode
);
}
private Status checkLicense(DecryptedResponse decryptedResponse) {
JsonObject object = decryptedResponse.object;
byte b = decryptedResponse.statusCode;
if (object.has("code")) {
return new Status(false, object);
}
byte[] decode = Base64.getDecoder().decode(object.get("hash").getAsString());
String hash = new String(decode);
boolean validLength = this.validLength(b, hash, 30);
if (!validLength) {
return new Status(false, object);
}
boolean validUniqueId = this.validUniqueId(b, object);
if (!validUniqueId) {
return new Status(false, object);
}
boolean validLeft = this.validLeft(b, hash, key);
if (!validLeft) {
return new Status(false, object);
}
boolean validSecret = this.validSecret(b, hash);
if (!validSecret) {
return new Status(false, object);
}
boolean validPablo = this.validPablo(b, hash);
if (!validPablo) {
return new Status(false, object);
}
boolean validTime = this.validTime(b, hash);
if (!validTime) {
return new Status(false, object);
}
boolean validRight = this.validRight(b, hash, key);
if (!validRight) {
return new Status(false, object);
}
boolean validStatus = this.validStatus(b, hash);
if (!validStatus) {
return new Status(false, object);
}
return new Status(true, object);
}
//----------------------------------------------
private HttpResponse<byte[]> sendLicenseRequest() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", secretKey)
.POST(HttpRequest.BodyPublishers.ofString(prepareData()))
.build();
try {
return client.send(request, HttpResponse.BodyHandlers.ofByteArray());
} catch (Exception e) {
throw new RuntimeException("License server is not available. Please try again later.");
}
}
private String prepareData() {
String hardwareId = getHardwareId();
JsonObject jsonObject = new JsonObject();
JsonArray enabledAddons = new JsonArray();
this.enabledAddons.forEach((addon) -> enabledAddons.add(addon));
this.requestUniqueId = UUID.randomUUID();
jsonObject.addProperty("key", key);
jsonObject.addProperty("product", product);
jsonObject.addProperty("version", version);
jsonObject.addProperty("hardwareId", hardwareId);
jsonObject.addProperty("uniqueId", requestUniqueId.toString());
jsonObject.addProperty("requestType", "LOADER");
jsonObject.add("enabledAddons", enabledAddons);
return gson.toJson(jsonObject);
}
//----------------------------------------------
private boolean validLength(byte statusByte, String data, int length) {
if (statusByte != -56) {
return false;
} else {
return data.length() == length;
}
}
private boolean validSecret(byte statusByte, String data) {
String secretHash = this.decodeSecretHash(data);
if (statusByte != -56) {
return false;
} else {
return secretHash.equals(secretKey.substring(0, 5));
}
}
private boolean validPablo(byte statusByte, String data) {
long pablo = this.decodePabloHash(data);
if (statusByte != -56) {
return false;
} else {
return pablo == 2520052137L;
}
}
private boolean validTime(byte statusByte, String data) {
String timeHash = this.decodeTimeHash(data);
if (statusByte != -56) {
return false;
} else {
return timeHash.equals(
String.valueOf(Instant.now().atZone(ZoneId.of("UTC+1")).toEpochSecond()).substring(0, 4));
}
}
private boolean validLeft(byte statusByte, String data, String licenseKey) {
String leftHash = this.decodeLeftHash(data);
String left = licenseKey.substring(0, 4);
if (statusByte != -56) {
return false;
} else {
return leftHash.equals(left);
}
}
private boolean validRight(byte statusByte, String data, String licenseKey) {
String rightHash = this.decodeRightHash(data);
String right = licenseKey.substring(licenseKey.length() - 4);
if (statusByte != -56) {
return false;
} else {
return rightHash.equals(right);
}
}
private boolean validStatus(byte statusByte, String data) {
String statusHash = this.decodeStatusHash(data);
if (statusByte != -56) {
return false;
} else {
return statusHash.equals("KIT");
}
}
private boolean validUniqueId(byte statusByte, JsonObject object) {
String uniqueIdString = object.get("uniqueId").getAsString();
UUID uniqueId = UUID.fromString(uniqueIdString);
if (statusByte != -56) {
return false;
} else {
return uniqueId.equals(this.requestUniqueId);
}
}
//----------------------------------------------
private String getHardwareId() {
try {
if (isWindows()) {
return getWindowsIdentifier();
} else if (isMac()) {
return getMacOsIdentifier();
} else if (isLinux()) {
return getLinuxMacAddress();
} else {
return UNKNOWN;
}
} catch (Exception e) {
return UNKNOWN;
}
}
private boolean isWindows() {
return (OS.contains("win"));
}
private boolean isMac() {
return (OS.contains("mac"));
}
private boolean isLinux() {
return (OS.contains("inux"));
}
private String getLinuxMacAddress() throws FileNotFoundException, NoSuchAlgorithmException {
File machineId = new File("/var/lib/dbus/machine-id");
if (!machineId.exists()) {
machineId = new File("/etc/machine-id");
}
if (!machineId.exists()) {
return UNKNOWN;
}
try (Scanner scanner = new Scanner(machineId)) {
String id = scanner.useDelimiter("\\A").next();
return hexStringify(sha256Hash(id.getBytes()));
}
}
private String getMacOsIdentifier() throws SocketException, NoSuchAlgorithmException {
NetworkInterface networkInterface = NetworkInterface.getByName("en0");
byte[] hardwareAddress = networkInterface.getHardwareAddress();
return hexStringify(sha256Hash(hardwareAddress));
}
private String getWindowsIdentifier() throws IOException, NoSuchAlgorithmException {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(new String[]{"wmic", "csproduct", "get", "UUID"});
String result = null;
try (InputStream ignored = process.getInputStream()) {
Scanner sc = new Scanner(process.getInputStream());
while (sc.hasNext()) {
String next = sc.next();
if (next.contains("UUID")) {
result = sc.next().trim();
break;
}
}
}
return result == null ? UNKNOWN : hexStringify(sha256Hash(result.getBytes()));
}
private byte[] sha256Hash(byte[] data) throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
return messageDigest.digest(data);
}
private String hexStringify(byte[] data) {
StringBuilder stringBuilder = new StringBuilder();
for (byte singleByte : data) {
stringBuilder.append(Integer.toString((singleByte & 0xff) + 0x100, 16).substring(1));
}
return stringBuilder.toString();
}
//----------------------------------------------
private String decodeLeftHash(String data) {
return data.substring(0, 4);
}
private String decodeSecretHash(String data) {
return data.substring(4, 9);
}
private long decodePabloHash(String data) {
return Long.parseLong(data.substring(9, 19));
}
private String decodeTimeHash(String data) {
return data.substring(19, 23);
}
private String decodeRightHash(String data) {
return data.substring(23, 27);
}
private String decodeStatusHash(String data) {
return data.substring(27, 30);
}
private JsonObject parse(String body) {
return gson.fromJson(body, JsonObject.class);
}
private JsonElement readPrimitive(JsonReader reader) throws IOException {
return new JsonParser().parse(reader);
}
private JsonElement readArray(JsonReader reader) throws IOException {
return new JsonParser().parse(reader);
}
//----------------------------------------------
public record LicenceResponse(boolean valid, LinceseClassLoader classLoader, String productMainClass, String productVersion, List<String> availableAddons, List<Addon> enabledAddons) { }
private record DecryptedResponse(JsonObject object, byte statusCode) { }
public record Addon(String name, String mainClass, Integer priority) { }
private record Status(boolean isValid, JsonObject object) { }
//----------------------------------------------
private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
public byte[] decrypt(byte[] encryptedData, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
byte[] iv = new byte[IV_LENGTH_BYTE];
System.arraycopy(encryptedData, 0, iv, 0, IV_LENGTH_BYTE);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(encryptedData, IV_LENGTH_BYTE, encryptedData.length - IV_LENGTH_BYTE);
}
//----------------------------------------------
public class LinceseClassLoader extends ClassLoader {
private final Map<String, byte[]> classesBytes = new HashMap<>();
public LinceseClassLoader(List<byte[]> jarBytesList, ClassLoader parent) {
super(parent);
for (byte[] jarBytes : jarBytesList) {
try (JarInputStream jarInputStream = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
JarEntry jarEntry = jarInputStream.getNextJarEntry();
while (jarEntry != null) {
if (jarEntry.getName().endsWith(".class")) {
String className = jarEntry.getName()
.replace('/', '.')
.replace(".class", "");
byte[] classData = jarInputStream.readAllBytes();
classesBytes.put(className, classData);
}
jarEntry = jarInputStream.getNextJarEntry();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> loadedClass = findServiceClass(name);
if (loadedClass != null) {
return loadedClass;
}
return super.findClass(name);
}
private Class<?> findServiceClass(String name) {
byte[] classData = classesBytes.get(name);
if (classData == null) {
return null;
}
return defineClass(name, classData, 0, classData.length);
}
}
}
Example usage
java
public class Main {
public static void main(String[] args) {
LicenseValidation validation = new LicenseValidation("LICENSE_KEY", "PRODUCT_NAME", "PRODUCT_VERSION");
System.exit(validation.isValid() ? 0 : 1);
}
}
Possible errors
CODE | Meaning |
---|---|
SECRET_KEY_NOT_FOUND | No user with the specified key was found. |
LICENSE_NOT_FOUND | No such license key was found. |
LICENSE_NOT_ASSIGNED | The license key does not match the secret key. |
BAD_IP_ADDRESS | Not a valid IP address. (Failed to get api address from query) |
BLACKLISTED_IP | The IP address from the query has been blocked. |
BLACKLISTED_HWID | The equipment identifier has been blocked. |
PRODUCT_NOT_FOUND | The product assigned to the license key does not exist. |
PRODUCT_NOT_MATCH | Product assigned to the license key does not match the specified product. |
MAX_IP_IN_USE | The maximum number of ip addresses for the specified license key has been reached. |
MAX_MACHINES_IN_USE | The maximum number of hardware IDs for the specified license key has been reached. |