rymga ← Back to Lock Master

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:

Your jar — 6 classes
demo/Heavy.class
demo/Report.class
demo/Sample.class
shop/App.class
shop/model/Cart.class
shop/util/Money.class
After — sealed
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.

FieldWhat it does
enabledTurn on whole-class encryption.
encryptInternal 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(); }
}
Type decoupling does this for you. Turn on 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-Class of an executable jar (java -jar);
  • a plugin/mod main class a host looks up from a descriptor (plugin.yml and friends);
  • ServiceLoader providers listed in META-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.

keep vs. per-technique exclude. Putting a class in keep takes it out of everything — no rename, no flow, no strings, no sealing — leaving it fully readable. To keep an entry point loadable and protected, don't seal it (leave it off 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.

FieldWhat it does
enabledTurn it on.
action"throw" — refuse to unseal (fail to enable/load). "exit" — kill the JVM outright.
checkAgentsAlso flag -javaagent / -agentpath. Off by default — many legitimate servers run monitoring or profiling agents, so only enable it if yours doesn't.
For sealed builds, prefer anti-dump. When you're already encrypting classes, 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.

It's a CLI command, not a config switch. You stamp a per-buyer id into an already-obfuscated jar with 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 / NoSuchMethodError when 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 ClassNotFoundException from inside such a library, leave that library's package off encrypt (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 — off encrypt and unrenamed. Flow and string encryption still cover it.