r Lock MasterAccount

Control-flow obfuscation

This is the deep layer. It rewrites how your methods branch, loop and compute so that no clean Java reproduces them — while they run bit-for-bit identically. Every snippet below is real engine output: one small method, one transform, decompiled back to Java so you can see the damage.

Presets

You don't have to think about individual transforms. Set enabled and pick apreset, and Lock Master chooses a sensible set for you:

{ "flow": { "enabled": true, "preset": "balanced" } }
presetRoughlyWhat runs
"light"~1.4× sizeCheap structural obfuscation only.
"balanced"~2× sizeControl-flow restructuring plus indirection, no heavy arithmetic.
"aggressive"~3.4× sizeEverything below.
"custom"Honor the explicit per-technique blocks you set.

For full control, use "preset": "custom", an "intensity"(1–10), and switch on individual techniques — each is an object{ "enabled": true, "weight": 5 }, where weight(1–10) sets how aggressively that one is applied. The rest of this page is those techniques, grouped by what they attack.

Predicates & dead code

The foundation: statements that look meaningful but aren't. An opaque predicate is a condition the engine knows the answer to but a decompiler can't — so it can't discard the branch it guards.

bogusControlFlow

Weaves in branches guarded by opaque predicates. They never change the result, but a reader can't prove that and has to trace every one. A two-line method becomes a thicket:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
bogusControlFlow
public String tier(int n) {
    int n2 = a0;
    if (((n2 & n | n2 ^ n) - (n2 | n) | (n2 & n2) + (n2 | n2) - (n2 + n2)) != 0) {
        n2 = Integer.reverse(n2);
    }
    if (Integer.numberOfLeadingZeros(-1) != 0) {
        Integer n3 = n2;
    }
    if (n >= 500) {
        a0 ^= n2 & ~n2;
        return "gold";
    }
    a0 ^= Integer.compare(n2, n2);
    return "standard";
}

deadCode

Injects plausible-looking code that never executes — here a computed value that's always zero, guarding athrow that can never fire. It reads like a real safety check:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
deadCode
public String tier(int n) {
    int n2 = a0;
    int flags_9 = Integer.reverse(Integer.reverse(n2)) - n2;
    if (flags_9 != 0) {
        throw new IllegalArgumentException("assertion failed");
    }
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}

branchAugmentation

Splits existing branches with extra opaque conditions, so a single if turns into a nested maze with a dead arm that appears reachable:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
branchAugmentation
public String tier(int n) {
    if (n >= 500) {
        int n2 = a0;
        if ((n2 | ~n2) == 0) {
            throw new AssertionError("unexpected value");
        }
        return "gold";
    }
    return "standard";
}

Structure destruction

These attack the shape of a method — the very thing a decompiler reconstructs to give you readable Java. Push it far enough and the decompiler produces garbage, or gives up.

controlFlowFlattening

Dissolves the method's structure into a dispatcher loop driven by a state variable. Theif/return is gone; what's left is a switch jumping between opaque numeric states. There's no clean control flow to read:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
controlFlowFlattening
public String tier(int n) {
    int n2 = a0;
    int n3 = 0x7E31B6E ^ n2;
    block5: while (true) {
        switch (n3 ^ n2) {
            case 132324206: 
            case 1894319798: {
                n3 = n >= 500 ? 0x1794DDD2 ^ n2 : 0x22892144 ^ n2;
            }
            default: {
                continue block5;
            }
            case 395632082: {
                return "gold";
            }
            case 579412292: 
        }
        break;
    }
    return "standard";
}

gotoSpaghetti

Reorders basic blocks and rewires them with jumps, destroying readable fall-through. Notice the switch cases arrive out of order and the default lands in the middle — the logic is intact, the layout is nonsense:

Your code
public String label(int n) {
    switch (n) {
        case 1: {
            return "start";
        }
        case 2: {
            return "stop";
        }
        case 3: {
            return "pause";
        }
    }
    return "unknown";
}
gotoSpaghetti
public String label(int n) {
    switch (n) {
        default: {
            return "unknown";
        }
        case 2: {
            return "stop";
        }
        case 3: {
            return "pause";
        }
        case 1: 
    }
    return "start";
}

switchBogusCases

Wraps the method in bogus switch dispatchers with unreachable cases keyed by opaque values. A trivial loop becomes three nested state machines — this is the real decompiled result:

Your code
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
switchBogusCases
public long factorial(int n) {
    int n2;
    long l;
    block12: while (true) {
        l = 1L;
        switch (new Object() instanceof String ? 1 : 0) {
            case 579412292: {
            }
            default: {
                break block12;
            }
            case 1894319798: {
                Integer.bitCount(42);
                continue block12;
            }
        }
        break;
    }
    block13: while (true) {
        n2 = 2;
        switch (n & ~n) {
            default: {
                break block13;
            }
            case 1081996856: {
                int cfr_ignored_0 = (new int[1])[n ^ n];
                continue block13;
            }
            case 1984262432: {
                int cfr_ignored_1 = (new int[1])[n ^ n];
            }
        }
        break;
    }
    block14: while (n2 <= n) {
        while (true) {
            l *= (long)n2;
            switch (~(~n2) - n2) {
                default: {
                    ++n2;
                    continue block14;
                }
                case 695721251: {
                    "".length();
                    continue block14;
                }
                case 30843304: 
            }
            int cfr_ignored_2 = (new int[1])[n ^ n];
        }
        break;
    }
    return l;
}

Loops

Loops are a decompiler's anchor points. These rewrite them so the anchor slips.

loopTransforms

Rewrites loops with opaque back-edges and guards — extra conditions inside the body that never alter the iteration but bloat and obscure it:

Your code
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
loopTransforms
public long factorial(int n) {
    int n2 = 0;
    int n3 = a0;
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
        if (((n2 += 4) ^ n2 ^ (n2 ^ n2) | Integer.highestOneBit(0)) == 0) continue;
    }
    return l;
}

fakeLoops

Wraps straight-line code in loops that run exactly once, gated by opaque predicates — so initialization now looks like a conditional iteration:

Your code
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
fakeLoops
public long factorial(int n) {
    long l;
    int n2;
    do {
        l = 1L;
        n2 = a0;
    } while ((n2 & ~n2) != 0);
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}

irreducibleFlow

Builds control flow that can't be expressed as clean nested loops (an irreducible graph). Java has no syntax for it, so the decompiler falls back to raw jumps or an unreachable loop it can't remove:

Your code
public String label(int n) {
    switch (n) {
        case 1: {
            return "start";
        }
        case 2: {
            return "stop";
        }
        case 3: {
            return "pause";
        }
    }
    return "unknown";
}
irreducibleFlow
public String label(int n) {
    switch (n) {
        case 1: {
            return "start";
        }
        case 2: {
            return "stop";
        }
        case 3: {
            return "pause";
        }
    }
    int n2 = a0;
    if ((n2 | ~n2) < 0) {
        return "unknown";
    }
    while (true) {
        if (!false) continue;
    }
}

Exceptions

Exception handlers carry control flow that isn't visible in the normal statement order — perfect cover, and a well-known way to confuse decompilers.

tryCatchAbuse

Wraps code in extra try/catch layers whose handlers are never really taken, threading real control through exception tables:

Your code
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}
tryCatchAbuse
public int toInt(String string) {
    try {
        try {
            return Integer.parseInt(string);
        }
        catch (NumberFormatException numberFormatException) {
            return -1;
        }
    }
    catch (UnsupportedOperationException unsupportedOperationException) {
        int n = a0;
        Integer.bitCount(n ^ n);
        throw unsupportedOperationException;
    }
}

exceptionFlow

Routes ordinary control flow through thrown-and-caught exceptions, turning a plain return path into a nested handler:

Your code
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}
exceptionFlow
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        try {
            "".length();
            return -1;
        }
        catch (StringIndexOutOfBoundsException stringIndexOutOfBoundsException) {
            stringIndexOutOfBoundsException.getMessage();
            throw stringIndexOutOfBoundsException;
        }
    }
}

exceptionOverlap

Deliberately overlaps exception ranges in a way Java's syntax can't represent. This one doesn't just make a decompiler struggle — it defeats it: both CFR and Vineflower crash on the method rather than produce any Java. Because no tool can reverse it, the "after" below is an honest reconstruction built from the real bytecode exception table (and labeled as such), not decompiler output:

Your code
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
exceptionOverlap — reconstruction (decompilers fail)
// This method cannot be decompiled. CFR and Vineflower -- two independent,
// modern decompilers -- both crash on it with an irreducible-graph error.
// The transform installs two DELIBERATELY OVERLAPPING exception ranges over
// the same code, which Java's try/catch syntax cannot represent. That overlap
// is exactly what breaks the tools. The real bytecode exception table is:
//
//     from  to  target  type
//        1  16      25   InternalError        -> re-throw
//        2  17      25   ArithmeticException  -> re-throw   (the ranges overlap)
//
// Reconstruction below (illustrative -- NOT decompiler output, since no
// decompiler can produce one) of what that bytecode actually does:

public long factorial(int n) {
    long acc = 1L;
    /* guard A (bytes 1..16):  catch InternalError       -> re-throw */
    /* guard B (bytes 2..17):  catch ArithmeticException -> re-throw */
    /*   the two ranges overlap and share one handler -- no valid Java */
    for (int i = 2; i <= n; i++) {
        acc *= i;
    }
    return acc;
}

Constants & data

Even when the structure is readable, the values don't have to be. These replace literals with expressions that only resolve at runtime, so a reader never sees the number — only the computation that produces it.

arithmeticMBA

Rewrites arithmetic as mixed boolean-arithmetic (MBA): identities built from ^ & |and + - that compute the same result through a wall of bit-twiddling. A one-lineprice * qty + 100 becomes this:

Your code
public int total(int n, int n2) {
    return n * n2 + 100;
}
arithmeticMBA
public int total(int n, int n2) {
    int n3 = 0;
    int n4 = 0;
    long l = 0L;
    long l2 = 0L;
    int n5 = 0;
    int n6 = 0;
    long l3 = 0L;
    long l4 = 0L;
    n6 = 100;
    n5 = n * n2;
    n4 = n6;
    n3 = n5;
    int n7 = ((n3 ^ n4) + (n3 & n4)) * 2;
    n4 = n6;
    n3 = n5;
    n4 = n3 + n4 - (n3 & n4) * 2;
    n3 = n7;
    return n3 + ~n4 + 1;
}

constantExpression

Replaces a constant with an expression that evaluates to it. The 100 is gone — in its place, something that computes 100 at runtime and nowhere states it:

Your code
public int total(int n, int n2) {
    return n * n2 + 100;
}
constantExpression
public int total(int n, int n2) {
    return n * n2 + (Integer.bitCount(-1) ^ 0x44);
}

integerSplitting

Splits integer constants into arithmetic over other values. The threshold 500becomes a pair of constants XORed together — you have to run it to learn what it is:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
integerSplitting
public String tier(int n) {
    if (n >= (0xC3805FC1 ^ 0xC3805E35)) {
        return "gold";
    }
    return "standard";
}

Indirection

The last group hides what talks to what — the calls, the field accesses, the method boundaries a reader uses to map a program.

invokeDynamic

Routes calls through an invokedynamic bootstrap so the class file records no direct target — just an opaque bootstrap call with encrypted descriptors (the show characters toggle flips them between escapes and raw glyphs). You can't tell what's being invoked without running it:

Your code
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}
invokeDynamic
public int toInt(String string) {
    try {
        return (int)a.bootstrap("x", 6, 1, "\u75c5\ua459\u0804\u4739\u1828\u324c\u7eda\uc98a", "\u75df\ua459\u0800\u472b\u1863\u3269\u7ed5\uc990\u8a0c\ueff3\u5f0b\ubc4a\u1dfa\u923c\uc214\ud6c7\u52ce", "\u759d\ua474\u081c\u472b\u183b\u3264\u7e9b\uc992\u8a0a\uefb3\u5f25\ubc0b\u1ddd\u922d\uc201\ud6cb\u52d2\u41ea\u5c9e\ucf8a\u7b93", -641825578, 1573233734, 318776947, 310234255, string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}

reflectionObfuscation

Rewrites direct calls and field access as reflection, so the class file no longer names its targets. Here a simple field read and write become Class.forName(...).getDeclaredField(...):

Your code
public int compute(int n) {
    int n2;
    int n3 = this.square(n);
    this.total = n2 = n3 + this.total;
    return n2;
}
reflectionObfuscation
public int compute(int n) {
    int n2;
    int n3 = this.square(n);
    Report report = this;
    Field field = Class.forName("demo.Report").getDeclaredField("total");
    field.setAccessible(true);
    int n4 = n2 = n3 + field.getInt(report);
    Report report2 = this;
    Field field2 = Class.forName("demo.Report").getDeclaredField("total");
    field2.setAccessible(true);
    field2.setInt(report2, n4);
    return n2;
}
Off in every preset. Because it rewrites calls as reflection, it can break code that itself relies on reflection unless you add matching keeprules. Enable it deliberately and test.

methodOutlining

Extracts fragments of a method into synthetic helper methods, shattering one readable body across many. Here a single spread method is broken into eleven — each of which then gets its own dose of everything above:

methodOutlining — the method list
// one method in...
public void spread(int[], int, int);

// ...eleven out (each fragment its own synthetic method):
  private static void a1(int[], int);
  private static void a2(int[], int);
  private static void b3(int[], int);
  private static void b4(int[], int);
  private static void a5(int[], int, int);
  private static void b6(int[], int, int);
  private static void b7(int[], int, int);
  private static void a8(int[], int, int);
  private static void b9(int[], int, int);
  private static void b10(int[], int, int);

seedMutation

Threads a mutating synthetic seed through the method, feeding the opaque predicates the other transforms rely on — a value that changes as the code runs, so predicates can't be folded away by static analysis:

Your code
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
seedMutation
public long factorial(int n) {
    long l = 1L;
    a0 ^= Integer.reverse(n);
    int n2 = 2;
    a0 ^= Integer.reverse(a0);
    while (n2 <= n) {
        l *= (long)n2;
        a0 += Integer.bitCount(a0) << 1 | 1;
        a0 ^= Integer.reverse(++n2);
    }
    return l;
}

Safety flags

Three flags govern the transforms that touch timing and linking:

FieldWhat it does
timingSafeIndyDefault true. Holds invokedynamic back from thread-timing-sensitive code, where a one-time link cost could perturb a real-time thread race. Most Bukkit/Spigot plugins have no such races and can set it false for fuller coverage — validate on a real server first.
seedThreadingExperimental. Context-sensitive opaque predicates threaded through methods. Off by default.
cfgFlatteningExperimental, graph-based flattening. Off by default.
It all still runs. Every transform on this page preserves behavior exactly — each build is re-checked against the JVM's own verification rules before it's handed back. Stack several together and the result is far denser than any one shown here. Test the protected build once before shipping.