Skip to content

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

CODEMeaning
SECRET_KEY_NOT_FOUNDNo user with the specified key was found.
LICENSE_NOT_FOUNDNo such license key was found.
LICENSE_NOT_ASSIGNEDThe license key does not match the secret key.
BAD_IP_ADDRESSNot a valid IP address. (Failed to get api address from query)
BLACKLISTED_IPThe IP address from the query has been blocked.
BLACKLISTED_HWIDThe equipment identifier has been blocked.
PRODUCT_NOT_FOUNDThe product assigned to the license key does not exist.
PRODUCT_NOT_MATCHProduct assigned to the license key does not match the specified product.
MAX_IP_IN_USEThe maximum number of ip addresses for the specified license key has been reached.
MAX_MACHINES_IN_USEThe maximum number of hardware IDs for the specified license key has been reached.