Class encryption
Everything else makes your bytecode hard to read. ClassGuard removes it from disk entirely: your classes ship as encrypted blobs with no loadable bytecode to decompile, unsealed only in memory, only at load time. It's the strongest single option in Lock Master.
What it does
With classGuard on, the classes you select are each encrypted with strong
authenticated encryption and the original .class file is stripped from the jar. In its
place ships an opaque encrypted blob. A single generated loader — the guardian, the only extra bytecode
added — reconstructs the key at runtime and unseals each class on demand, in memory. Nothing decrypted ever
touches the disk.
All sealed classes are packed into one compressed, authenticated pool
(META-INF/guardian/pool.bin) — compressed once and encrypted once, so they share a
dictionary and a single tag instead of paying per-class overhead. The encrypted output is markedly smaller than
sealing each class separately, which matters most for jars with many small classes.
{ "classGuard": { "enabled": true, "encrypt": ["com/you/internal/"] } }
There are no modes — one selective model. You list what to encrypt with
encrypt; everything you leave off stays as normal cleartext bytecode and is loaded
by the runtime the ordinary way. The result for the sealed classes: a decompiler opening your jar finds almost
nothing to work with — no class names, no methods, no strings, just ciphertext and a loader.
Before & after
Real output. A six-class jar with everything selected for encryption, before and after ClassGuard. Every
business class is gone from the file listing — folded into the single encrypted pool at
META-INF/guardian/pool.bin — and only the guardian loader (plus any class you left
cleartext) remains as openable bytecode:
demo/Heavy.class
demo/Report.class
demo/Sample.class
shop/App.class
shop/model/Cart.class
shop/util/Money.class
a/a.class
b/b.class
META-INF/guardian/pool.bin
The pool.bin file is high-entropy ciphertext — no readable strings, no class
structure, nothing a decompiler can parse. Your Sample,
Report, Cart and the rest simply aren't there as bytecode
any more. To read them, an attacker has to defeat the encryption and capture the classes as the JVM
unseals them at runtime — which is the path anti-dump
(and the lighter antiDebug) closes.
What to encrypt
One field carries the whole decision: encrypt, an include-list of what to seal.
Everything you don't list stays cleartext and loads the normal way.
| Field | What it does |
|---|---|
| enabled | Turn on whole-class encryption. |
| encrypt | Internal names (com/you/Secret) or package prefixes (com/you/internal/) of the classes to seal. "*" seals everything (except keep). Omit it or leave it empty to seal everything too. A specific list seals only the sensitive classes you name — the recommended shape. |
The list takes classes and packages, mixed freely, as many as you like — a bare name
(com/you/licensing/KeyCheck) is one class and its inner classes; a trailing slash
(com/you/internal/) is a whole package tree. List individual classes and several
packages together:
{
"classGuard": {
"enabled": true,
"encrypt": [
"com/you/licensing/KeyCheck",
"com/you/internal/",
"com/you/crypto/"
]
}
}
encrypt obeys keep:
anything in keep is never sealed, even under "*". So the usual
pattern is encrypt: ["*"] plus a keep list for your entry
points, or a tight encrypt list naming just the classes worth hiding. It's the same
matcher used by rename.exclude,
flow.exclude and
stringEncryptionExclude.
The bridging rule
A cleartext class can't name a sealed class directly — at link time the JVM would try to resolve a
class that isn't there as bytecode. Lock Master bridges the one safe pattern automatically: a sealed class used
through a cleartext interface or superclass (the common "sensitive implementation behind a
clean interface"). A cleartext new Secret() becomes a reflective construction through
the guardian, cast back to that interface — you write nothing.
Any other direct reference from cleartext to a sealed class — a field of that type, a cast, a static
call, Secret.class, extending it — fails the build immediately with a
message naming both classes and telling you the fix (seal that class too, or route through a cleartext
interface). A broken jar is never produced. In practice: encrypt cohesive units — a package, or an
implementation plus nothing that reaches into it by name.
Architectural patterns
Because of this ClassLoader boundary, a cleartext class — your Main, which must stay cleartext so the host JVM
can load it — can't hold a field, cast, or new of an encrypted concrete class.
Left to itself, ClassGuard only bridges the one safe pattern (a sealed class behind a cleartext interface or
superclass) and fails the build on anything else, so you'd have to hand-write an interface for every internal
class you wanted to reference:
// Without type decoupling, this fails the build:
public class Main {
private ConfigService configService; // hard ref to an encrypted concrete class
public void onEnable() { this.configService = new ConfigService(); }
}
// …you'd have to refactor everything behind a cleartext interface by hand:
public class Main {
private IService configService; // OK: cleartext interface
public void onEnable() { this.configService = new ConfigService(); }
}
typeDecoupling
and you don't refactor anything: the engine extracts the cleartext interface, routes construction through a
generated factory, and rewrites every reference — automatically. Your code keeps naming concrete types; the
obfuscator makes them sealable. It's the recommended way to run ClassGuard on a real codebase, and it's what
lifts sealing from "a few interfaces" to most of your classes.Entry points — never seal what the runtime finds by name
This is a general-purpose Java obfuscator, not a plugin tool, and the rule is the same everywhere: any class the runtime locates by its name must stay cleartext under that name. If it's encrypted, whatever goes looking for it can't find loadable bytecode, and your jar won't start. That includes:
- the
Main-Classof an executable jar (java -jar); - a plugin/mod main class a host looks up from a descriptor (
plugin.ymland friends); ServiceLoaderproviders listed inMETA-INF/services;- anything a framework or your own code loads reflectively by string (Spring beans, JDBC drivers,
Class.forName(...)).
The common ones are handled for you: Lock Master detects the manifest Main-Class, every
plugin descriptor's main, and ServiceLoader providers under
META-INF/services, and keeps them cleartext automatically — even under
encrypt: ["*"]. What it can't see is a class you load reflectively from a
string you built yourself; leave those out of encrypt (that call is yours).
The same goes for renaming: don't rename a
class the runtime resolves by a name it won't sync. But excluding a class from sealing and renaming
doesn't leave it unprotected — that's exactly why each technique has its own exclusions. Your
entry point still gets string encryption and control-flow obfuscation; it just keeps its name
and stays loadable. Protect it from the inside, don't hide it from the loader.
encrypt) and don't rename it
(rename.exclude), while flow and string encryption still apply. Reach for
keep only for a public API whose names must not move at all.Runtime protection: antiDebug & anti-dump
Sealing the classes closes static analysis, so the remaining attack is dynamic: run the program under a debugger and dump the classes as they're unsealed. Two options close that door, at two levels of strength.
antiDebug is the lightweight one: a single entry-point check that
inspects the JVM's launch configuration and, if a debugger is attached, refuses to start — so the encrypted
classes are never unsealed under observation. It's a top-level block and works even without ClassGuard.
| Field | What it does |
|---|---|
| enabled | Turn it on. |
| action | "throw" — refuse to unseal (fail to enable/load). "exit" — kill the JVM outright. |
| checkAgents | Also flag -javaagent / -agentpath. Off by default — many legitimate servers run monitoring or profiling agents, so only enable it if yours doesn't. |
classGuard.antiDump is the far stronger,
ClassGuard-native runtime protection: many independent layers (native debugger detection, OS-level
anti-dump hardening, tamper-evident native libraries, scattered tripwires), and — the key difference — the
detection is folded into the decryption key rather than a single check you can patch out. Under a
debugger the key simply comes out wrong and nothing decrypts. It has its
own page, including the important caveat
that it's fail-closed against any agent (profilers included).watermark
An invisible, recoverable identifier stamped redundantly across a jar, so a leaked copy traces back to whoever it was handed to. It's split and checksummed across classes, so it survives re-jarring or partial deletion, and reading it back needs a secret that never ships in the build.
rymga-cli watermark stamp, and recover it from a leak
with watermark trace — both run server-side. See
Per-buyer watermark.Before you ship
- Test on a real run. ClassGuard changes how your program loads — always run the sealed build on a real server or launch before shipping.
- Works alongside other plugins. The guard now loads its own encrypted classes
child-first, so two plugins whose obfuscated classes end up with the same short name no longer
collide on a shared server — the failure that used to surface as a
ClassNotFoundException/NoSuchMethodErrorwhen another plugin was installed. Async scheduler tasks and threads your own code starts resolve encrypted classes correctly too. - One edge remains: name-based loading from a foreign thread. A bundled library that loads
a class by name off a thread it started itself, using that thread's context class loader rather than
its own (some builds of bStats do this), can still miss an encrypted class. If a sealed build throws
ClassNotFoundExceptionfrom inside such a library, leave that library's package offencrypt(or add it to keep), or skip ClassGuard for that jar — rename + string encryption + flow still protect strongly. See the real-plugin example. - It's non-reproducible by design. The encryption key is drawn from a secure random source, so a ClassGuard build differs every time even with a fixed seed. That's deliberate — a reproducible key would defeat the purpose.
- Mind the entry point. Leave whatever the runtime finds by name — your
Main-Class, plugin main, service providers — offencryptand unrenamed. Flow and string encryption still cover it.