Protecting Java and Spigot Plugins From Decompilation
If you sell a Spigot, Paper, or Bukkit plugin, someone has already tried to open your JAR. Java is one of the easiest languages to reverse. Unlike native code, compiled Java keeps most of the structure of your source, and modern decompilers turn that structure back into code you can read on the first try.
This article walks through why Java is so exposed, why the popular renaming tools only get you part way, and what class encryption actually changes for your threat model. It is honest about the ceiling too, because the JVM has one and pretending otherwise helps no one.
Why Java decompilation is so easy
Java source compiles to JVM bytecode, not machine code. Bytecode is a high-level, well-documented instruction set that still carries class names, method signatures, field types, and often line numbers and local variable names. A decompiler does not have to guess much. It reconstructs control flow, loops, and expressions almost directly.
Tools like JD-GUI, CFR, and Procyon are free, fast, and good. Point any of them at your JAR and you usually get back Java that compiles again with minor edits. For a Minecraft plugin, that means your economy logic, your license check, your API keys, and your clever features are all sitting there in plain reading order.
// What a decompiler recovers from an unprotected plugin
public class LicenseCheck {
private static final String SECRET = "sk_live_9f2a...";
public boolean isValid(String key) {
return key.equals(hash(SECRET + serverId));
}
}Rule of thumb: if your plugin ships as a plain JAR, assume anyone can read the full source in under a minute. The only question is how much you slow them down.
Why renaming-only tools leave readable logic
ProGuard is the classic Java tool, and it is genuinely useful. It renames classes, methods, and fields to short names, strips unused code, and shrinks the JAR. Allatori and similar commercial tools do the same and add extras like string encryption and light control-flow tricks.
The catch with a rename-only approach is that renaming does not remove logic. It removes names. After ProGuard, a decompiler still reconstructs your methods; the variables are just called a, b, and c now. The structure, the branches, the string comparisons, and the order of operations are all intact and readable. A patient reader renames things back as they go.
// After rename-only obfuscation: harder to skim, still fully readable
public class a {
private static final String b = "sk_live_9f2a..."; // strings often still visible
public boolean a(String c) {
return c.equals(a(b + d));
}
}Renaming is a real speed bump, and for many projects it is enough. But for paid plugins where the whole value is the logic, it is a low bar. The literal strings frequently survive too, which is how leaked API keys and license secrets keep showing up.
What class encryption changes
Joker takes a different approach for Java than for its other seven languages. Instead of translating your code into a bytecode VM, it encrypts the compiled classes themselves. Your real bytecode never sits on disk in readable form. A small loader ships alongside it, and that loader decrypts and defines the classes at runtime.
The practical effect: point JD-GUI, CFR, or Procyon at the protected JAR and they see encrypted blobs, not your classes. There is no control flow to reconstruct because there is no plaintext bytecode to read. Static decompilation, the fast and free attack that most plugin thieves rely on, stops working.
On top of encryption, Joker also applies string encryption, field and identifier renaming, debug info stripping, and per-build variation. So even the pieces that a static tool might otherwise glimpse are scrambled, and no two builds look the same.
This is validated on real plugins, including EssentialsX-class Spigot and Paper builds, so the loader works with the plugin lifecycle rather than fighting it.
How the custom classloader decrypts at load
When the JVM needs a class, the Joker loader intercepts the request. It reads the encrypted bytes, decrypts them in memory, and defines the resulting class directly to the runtime. From the JVM's point of view it is a normal class definition. From an attacker's point of view, nothing readable ever touched the filesystem.
// Simplified shape of the load path
byte[] encrypted = readResource("com/you/Feature.class.enc");
byte[] bytecode = decrypt(encrypted, buildKey);
Class<?> cls = defineClass(bytecode); // defined in memory, never written to diskBecause decryption happens per build with per-build keys and variation, copying the loader from one JAR does not help you open another. The attacker cannot just grab a universal unlock routine.
The honest ceiling: JVM run-and-dump
Here is the part other vendors gloss over. Java runs on the JVM, and the JVM has to hold real bytecode in memory to execute it. That means a determined attacker can attach a Java agent, hook class definition, and dump the decrypted classes after your loader defines them. This is the run-and-dump attack, and no pure-Java scheme can physically prevent it. If your bytecode runs, it exists in memory at some point.
So class encryption is not unbreakable, and we will not call it that. What it does is change the economics. It defeats casual decompilation completely and forces any serious attacker into a live, tooling-heavy process: set up an agent, hook the right point, dump, reassemble, and untangle the renamed and string-encrypted result. That is a large jump in skill and time compared to dragging a JAR into JD-GUI.
For a threat model, that framing matters. Most plugin theft is opportunistic: someone wants a free copy or wants to strip your license check in ten minutes. Class encryption removes that entire tier of attacker. It raises the bar high enough that copying your plugin costs more effort than most people are willing to spend, which for a commercial product is usually the real goal.
How it compares to ProGuard and Allatori
- ProGuard: renames and shrinks. Great for size and casual protection, but the logic decompiles back to readable code with short names. Strings often remain visible.
- Allatori and similar: renaming plus string encryption and some control-flow obfuscation. A stronger speed bump, but the bytecode is still present and decompilable, just messier to read.
- Joker class encryption: no plaintext bytecode on disk, so static decompilers get encrypted blobs. Plus string encryption, renaming, debug stripping, and per-build variation. The remaining attack is live run-and-dump, which is far more costly than static reading.
None of these are magic, and the right choice depends on what you are protecting. If you just want to discourage lazy copying, ProGuard may be plenty. If your plugin's value is the logic and you sell it, encryption raises the cost meaningfully further than renaming alone.
Practical advice for plugin sellers
- Never ship secrets in the JAR. Keys and license validation that live client-side can be dumped no matter what. Move critical checks server-side where you can.
- Assume the logic can eventually be extracted by a skilled attacker, and price and design around raising cost, not around impossibility.
- Use per-build variation so a leaked crack for one release does not open the next.
- Encrypt strings so grep and simple dumps do not hand over your endpoints and tokens.
- Combine protection with a license or entitlement system so even an extracted copy is less useful.
The goal is not a wall that no one can ever climb. It is a wall tall enough that climbing it costs more than the plugin is worth to the person climbing. Class encryption puts that wall well above what renaming alone offers.
Want to see what your plugin looks like after class encryption? Joker gives you 300 free credits with no card required, so you can protect a real build and try to decompile it yourself.