rymga ← Back to Lock Master

Type decoupling (CHF)

Class encryption is the strongest thing Lock Master does — but on its own it can only seal classes nothing else names by concrete type. Type decoupling removes that limit: it rewrites your code to reach those classes through auto-generated cleartext interfaces, so almost all of your logic becomes sealable. It is the enabler that lifts ClassGuard from a handful of classes to most of your codebase.

What it does

The JVM verifies, at load time, that every class a loadable class names actually exists. So a cleartext class that holds a field of type SecretVault, does new SecretVault(), or casts to it, cannot coexist with a sealed SecretVault — there'd be no bytecode to resolve. That's why plain class encryption can only seal classes hidden behind an interface already.

Type decoupling (the paper's Class Hierarchy Flattening family — CHF) fixes this automatically. For each class it can safely transform it:

  • extracts a cleartext interface from the class's public API;
  • rewrites every reference elsewhere to use that interface instead of the concrete type;
  • routes construction (new X(args)) through a generated factory;
  • turns cross-class field access into accessor calls.

Now nothing cleartext names the concrete class — only its interface — so ClassGuard can seal it. You write nothing; it's a single switch.

{
  "classGuard":     { "enabled": true, "encrypt": ["*"] },
  "typeDecoupling": { "enabled": true, "exclude": [], "protectHotPaths": true }
}

Why it exists — a real before/after

Here's the same jar — a small licensing engine whose Main constructs and calls its internal classes by name, like any normal program. On the left, class encryption alone. On the right, the same config with type decoupling on:

ClassGuard alone
# classGuard alone, encrypt ["*"], NO type decoupling
> obfuscate demo.jar

ClassGuard: cleartext class 'com.rymga.demo.Main' a call passes/returns an
encrypted type (encrypted class 'SecretVault'), which can't be bridged.
Either add 'com.rymga.demo.Main' to the encrypt set too, or access
'SecretVault' through a cleartext interface + no-arg construction.

# build refused — a broken jar is never produced
+ type decoupling
# same config + typeDecoupling.enabled = true
> obfuscate demo.jar

Type decoupling analysis:
  Classes requested for protection: 11
  Classes decoupled: 9
  Classes encrypted: 10
  Classes left cleartext: 5
JAR written: demo-obf.jar        # runs identically

Without decoupling you'd have to hand-write an interface for every class you wanted sealed, and route all your own code through it — for a real codebase, unworkable. With it on, the engine does exactly that transformation for you: nine of eleven classes decoupled and sealed into the encrypted pool, the jar runs byte-for-byte the same, and the two that can't be transformed safely are left cleartext rather than breaking the build.

Turning it on

You express intent — which packages to protect — and the engine decides what it can safely transform. The whole public surface is three fields:

FieldWhat it does
enabledTurn type decoupling on. With it on, the classes ClassGuard seals are driven by what decoupling could safely prepare.
excludeInternal names / package prefixes (JVM form, slashes) to keep out of decoupling — e.g. a performance hot path you'd rather leave direct. Same matcher as everywhere else.
protectHotPathsAvoid the more expensive bridges (reflective static access) in performance-sensitive classes. On by default.

Everything below this — how interfaces are extracted, how factories are built, when a class is merged or flattened — the engine decides on its own. There are no internal knobs to combine; the complexity lives in the tool, not in your config.

Works with your encrypt list. Type decoupling doesn't change what you choose to seal — it makes more of that choice possible. Pair it with classGuard.encrypt as usual: the sealed set is your requested classes, intersected with what decoupling made safe.

How it works with class encryption

Decoupling and ClassGuard run as one pipeline:

  1. The planner analyses every class you asked to protect and classifies it: safe to decouple, or must stay concrete.
  2. For the safe set it generates the cleartext interfaces and factory, and rewrites all references.
  3. ClassGuard then seals exactly that decoupled set into the encrypted pool.
  4. The result is verified — parsed, frame-checked, and linked in a clean JVM — before the jar is written.

The build prints a short report so you can see the split at a glance:

Type decoupling analysis:
  Classes requested for protection: 184
  Classes decoupled: 137
  Classes encrypted: 137
  Classes left cleartext: 47
  Exclusion reasons:
    21 lambda / invokedynamic constraints
    14 external hierarchy constraints
     8 unresolved optional dependencies

What stays cleartext — automatically

Some classes must not be sealed or renamed, because the runtime loads them by name. Lock Master detects these and keeps them cleartext for you — no config needed:

  • the manifest Main-Class and every plugin descriptor's main;
  • ServiceLoader / SPI providers listed under META-INF/services/;
  • the guardian loader and its own infrastructure.

Framework callbacks keep working too: a Bukkit onEnable, an event handler, anything that overrides a library type, is resolved by the platform against that library — so its name is preserved independently. You select packages to protect; the engine protects the true internals and leaves the entry points loadable.

Fail-safe by construction

A class that can't be transformed safely — one reached by a lambda, an unresolved optional dependency, an external superclass, a reflection target — is excluded automatically and left cleartext. It's never forced through. And three gates stand before the jar is written: a duplicate-method check, an intra-jar linkage check, and full JVM verification. If any transformation would produce bytecode that wouldn't load, the whole decoupling pass is reverted and the jar ships valid (just less decoupled). A broken jar is never produced.

Give it the libraries. Decoupling reasons about your class hierarchy, so — like the rest of the pipeline — it needs your compile/runtime dependencies on the libraries path to resolve external types. Without them it stays conservative and decouples less.

The ceiling

Type decoupling exists to make class encryption reach further — and encryption is the real protection: it takes your bytecode off disk. But no obfuscator is unbreakable, and we won't pretend otherwise. The JVM must run the code, so a sealed class does exist in cleartext in memory during execution, on a machine the attacker controls — it can be captured by an agent or an instrumented runtime. Anti-dump raises the cost of that runtime capture a great deal — it makes a debugger or agent in the process fail-closed, so the classes never decrypt under observation — but it can't defeat an attacker with full scripted control of the runtime. What you get is concrete: decompilation of the artifact at rest is defeated, casual and automated reversing is stopped, and the cost of a determined manual reverse is raised a great deal. What you don't get is protection against someone with full control of the runtime — a ceiling every bytecode obfuscator shares. Size a plan around that.