rymga ← Back to Lock Master

Configuration

One JSON file tells Lock Master what to protect and how hard. Every technique is opt-in — nothing happens unless you ask for it. This page is the map; each layer has its own page with real before/after code for every transform.

Names are "internal" names. Everywhere you name a class, use JVM internal form — slashes, not dots: com/you/Main, not com.you.Main. ** matches any depth, * a single segment.

Structure

The top level is a set of optional blocks. Include only the ones you need:

{
  "techniques": { ... },   // class-file protections (strings, rename, stripping)
  "keep":       { ... },   // what to never rename / touch
  "rename":     { ... },   // how renaming names things
  "flow":       { ... },   // control-flow obfuscation
  "classGuard": { ... },   // whole-class encryption (sealed classes)
  "antiDebug":  { ... },   // refuse to run under a debugger
  "libraries":  { ... },   // dependency jars so the class hierarchy resolves
  "seed": 123456           // optional: reproducible builds
}

jar and mapping exist in the engine but are handled by the platform in the cloud (your input and output come from the CLI's --in/--out). mapping is handled by the platform too — every renaming build emits a name-map file the CLI downloads for retrace; you don't set its paths. libraries you do set — see libraries below.

The four layers

Protection stacks in four layers, from cheapest to strongest. Each has a dedicated page that walks through every option with real engine output — one small method or jar, transformed, then decompiled so you see exactly what ships:

  • Renaming & layout — strip the meaning out of class, method, field and package names. Covers rename, keep rules, and the five package strategies.
  • String encryption — remove every readable literal from the class file. Covers the PER_METHOD, INLINE and POOL modes.
  • Control-flow obfuscation — rewrite how methods branch, loop and compute so no clean Java reproduces them. Covers presets and all nineteen flow techniques.
  • Class encryption — ship your classes as encrypted blobs with no bytecode to decompile. Covers classGuard, antiDebug and watermark.

techniques

The techniques block holds the class-file-level switches. The two headline ones, stringEncryption and rename, have their own pages (strings, renaming). The rest are stripping and metadata options:

FieldWhat it does
stringEncryptionEncrypt string literals — NONE / PER_METHOD / INLINE / POOL. See String encryption.
renameRename classes/methods/fields (uses rename + keep). See Renaming & layout.
stripDebugRemove debug info — line numbers and local variable names. Recommended.
stripInnerClasses / stripNestInfoDrop inner-class / nest-mate metadata.
syntheticMark members synthetic, hiding them from some tools and IDEs.
cacheModeString cache: FAST (cache after first decrypt) or SAFE (re-decrypt and wipe on every access).
callerBindingKey strings to their runtime caller: STACKWALKER (default) or STACKTRACE (cheaper, spoofable).
assetPoolWhere encrypted string data lives: INLINE (helper class) or EXTERNAL (META-INF/strings.dat).
stripKotlinMetadataDrop Kotlin @Metadata (for Kotlin code).
stripAnnotationsDrop invisible (CLASS-retention) annotations; runtime-visible ones are always kept. Use keepAnnotations for exceptions.

libraries

Library jars are the dependencies your project imports but doesn't bundle — the ones you declare in build.gradle or pom.xml (the Paper/Spigot API, a database driver, any compileOnly or provided library). They sit on your classpath at build time but aren't inside the JAR you hand us.

The obfuscator needs to see them to protect your code safely: to rename, restructure and encrypt, it walks the full class hierarchy — superclasses, interfaces, method overrides. If a type your code extends or implements lives in one of those libraries and we can't see it, we can't tell an override from a brand-new method, and the protected JAR could break at runtime. Declaring them lets us resolve every type.

List them by folder and name; the CLI bundles and uploads them alongside your JAR (paths relative to where you run it). You only need what's required to resolve your hierarchy — not your whole classpath, and never the JDK itself:

{
  "libraries": {
    "folder": "./libs",
    "jars": ["paper-api.jar", "PlaceholderAPI.jar"]
  }
}

Get them automatically

Don't collect these by hand. Add one task to your build and it copies every dependency into ./libs/ — then point libraries.folder at that folder.

Gradle — add this to build.gradle, then run ./gradlew exportLibs:

// build.gradle — export your dependencies into ./libs
tasks.register('exportLibs', Copy) {
    // runtimeClasspath covers normal deps; add compileClasspath
    // if your API is compileOnly / provided (Paper, Spigot, …)
    from configurations.runtimeClasspath
    from configurations.compileClasspath
    into 'libs'
}

// run it:  ./gradlew exportLibs

Maven — add the plugin to pom.xml; it runs on mvn package, or on demand with the command in the comment:

<!-- pom.xml -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <executions>
    <execution>
      <id>export-libs</id>
      <phase>package</phase>
      <goals><goal>copy-dependencies</goal></goals>
      <configuration>
        <outputDirectory>${project.basedir}/libs</outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugin>

<!-- run it:  mvn package
     or:      mvn dependency:copy-dependencies -DoutputDirectory=libs -->
Libraries are metered too. Tokens are charged on the combined weight of your JAR plus the library jars, since the server processes both — so export only what your hierarchy needs, and trim test-only or runtime-only jars you don't actually extend.

seed & reproducibility

Set "seed": <number> to make builds reproducible: the same input plus the same config produces byte-identical output — handy for debugging and for verifying a build. Leave it out for a fresh random build each time.

For stable names across releases, use incremental obfuscation. Obfuscated names are assigned in order, so a seed alone doesn't hold them when your code changes. Pass the CLI --mapping rymga.map and it reuses the previous build's names for unchanged symbols — see the mapping file. For an API that must never move, keep it unrenamed.
classGuard uses a secure random key by design, so turning it on makes the build non-reproducible regardless of the seed.

Full reference

Every field, with valid values, in one file — copy it and delete what you don't need. It's valid JSON as-is (no comments), so it runs, though reflectionObfuscation and classGuard are off by default because they change the most:

{
  "techniques": {
    "stringEncryption": "PER_METHOD",
    "rename": true,
    "synthetic": false,
    "stripDebug": true,
    "stripInnerClasses": false,
    "stripNestInfo": false,
    "cacheMode": "FAST",
    "callerBinding": "STACKWALKER",
    "assetPool": "INLINE",
    "stripKotlinMetadata": false,
    "stripAnnotations": false,
    "keepAnnotations": []
  },
  "keep": {
    "packages": ["com/you/api"],
    "classes":  ["com/you/Main"],
    "members": [
      { "classPattern": "com/you/**", "methodPattern": "on*", "fieldPattern": null, "descriptor": null }
    ]
  },
  "rename": {
    "deep": 3,
    "chars": ["a", "b", "c", "d"],
    "packageStrategy": "WRAPPER",
    "packageDepth": 2,
    "packagePool": 8
  },
  "flow": {
    "enabled": true,
    "preset": "custom",
    "intensity": 6,
    "timingSafeIndy": true,
    "seedThreading": false,
    "cfgFlattening": false,
    "bogusControlFlow":      { "enabled": true,  "weight": 5 },
    "deadCode":              { "enabled": true,  "weight": 5 },
    "tryCatchAbuse":         { "enabled": true,  "weight": 5 },
    "exceptionFlow":         { "enabled": true,  "weight": 5 },
    "controlFlowFlattening": { "enabled": true,  "weight": 5 },
    "loopTransforms":        { "enabled": true,  "weight": 5 },
    "gotoSpaghetti":         { "enabled": true,  "weight": 5 },
    "methodOutlining":       { "enabled": true,  "weight": 5 },
    "irreducibleFlow":       { "enabled": true,  "weight": 5 },
    "switchBogusCases":      { "enabled": true,  "weight": 5 },
    "invokeDynamic":         { "enabled": true,  "weight": 5 },
    "seedMutation":          { "enabled": true,  "weight": 5 },
    "integerSplitting":      { "enabled": true,  "weight": 5 },
    "fakeLoops":             { "enabled": true,  "weight": 5 },
    "branchAugmentation":    { "enabled": true,  "weight": 5 },
    "arithmeticMBA":         { "enabled": true,  "weight": 5 },
    "exceptionOverlap":      { "enabled": true,  "weight": 5 },
    "constantExpression":    { "enabled": true,  "weight": 5 },
    "reflectionObfuscation": { "enabled": false, "weight": 5 }
  },
  "classGuard": {
    "enabled": false,
    "mode": "STANDALONE",
    "pluginMainClass": null,
    "excludeClasses": []
  },
  "antiDebug": {
    "enabled": false,
    "action": "throw",
    "checkAgents": false
  },
  "libraries": {
    "folder": "./libs",
    "jars": []
  },
  "seed": null
}

Full examples

A Minecraft plugin, light and safe

Strings gone, renamed, cheap flow obfuscation, keeping the main class and public API. A great starting point for a first Bukkit/Spigot/Paper plugin:

{
  "techniques": {
    "stringEncryption": "POOL",
    "rename": true,
    "stripDebug": true
  },
  "flow": { "enabled": true, "preset": "light" },
  "keep": {
    "classes":  ["com/you/MyPlugin"],
    "packages": ["com/you/api"]
  }
}

A real plugin: shaded libraries & background tasks

The common real-world case. Most plugins shade libraries (bStats, a license API…) into the jar and run async scheduler tasks. This still gives strong protection — renamed, strings encrypted, control flow rewritten — while keeping the two things that would otherwise break: the main class (so plugin.yml resolves) and the packages of any shaded library that spins up its own thread or reads itself by name:

{
  "techniques": { "stringEncryption": "PER_METHOD", "rename": true, "stripDebug": true },
  "flow": { "enabled": true, "preset": "balanced" },
  "keep": {
    "classes":  ["com/you/MyPlugin"],
    "packages": ["com/you/libs/bstats", "com/vendor/licenseapi"]
  }
}
Async tasks are fine; watch one edge with self-loading shaded libs. classGuard loads its encrypted classes child-first, so async scheduler tasks and threads your own code starts resolve them correctly, and two plugins with colliding obfuscated names no longer clash on a shared server. The one case that can still miss: a shaded library that loads a class by name off a thread it started, via that thread's context class loader (some bStats builds) — list such a library in excludeClasses, or use the config above (no classGuard) for that jar. Either way, seal only after you've tested it loads on a real server.

A plugin, sealed (strongest — test it first)

Adds class encryption in PLUGIN mode and anti-debug. You only name the main in pluginMainClass — you don't keep it or exclude it; PLUGIN mode renames and encrypts your real main and ships a cleartext stub under your plugin.yml name. Best for a self-contained plugin without background threads or shaded self-loading libraries (see the warning above):

{
  "techniques": {
    "stringEncryption": "PER_METHOD",
    "rename": true,
    "stripDebug": true
  },
  "flow": { "enabled": true, "preset": "balanced", "timingSafeIndy": true },
  "classGuard": {
    "enabled": true,
    "mode": "PLUGIN",
    "pluginMainClass": "com/you/MyPlugin"
  },
  "antiDebug": { "enabled": true },
  "keep": { "classes": [] }
}

A library or API JAR

A jar other code compiles against: keep the whole public api package and all its member names so callers still link, and obfuscate everything else. MIRROR keeps the package shape so reflection over your own packages still resolves:

{
  "techniques": { "stringEncryption": "POOL", "rename": true, "stripDebug": true },
  "flow": { "enabled": true, "preset": "light" },
  "rename": { "packageStrategy": "MIRROR" },
  "keep": {
    "packages": ["com/you/api"],
    "members": [
      { "classPattern": "com/you/api/**", "methodPattern": "*" }
    ]
  }
}

A Kotlin plugin

Same as a plugin, plus stripKotlinMetadata so the @Metadata annotations that let tools rebuild your Kotlin classes, names and signatures are removed:

{
  "techniques": {
    "stringEncryption": "PER_METHOD",
    "rename": true,
    "stripDebug": true,
    "stripKotlinMetadata": true
  },
  "flow": { "enabled": true, "preset": "balanced" },
  "keep": { "classes": ["com/you/MyKotlinPlugin"] }
}

A standalone app, everything on

An executable jar (not a plugin): aggressive flow, sealed classes with a generated launcher, and anti-debug that refuses to unseal under a debugger. Keep the Main-Class:

{
  "techniques": { "stringEncryption": "PER_METHOD", "rename": true, "stripDebug": true },
  "flow": { "enabled": true, "preset": "aggressive" },
  "classGuard": { "enabled": true, "mode": "STANDALONE" },
  "antiDebug": { "enabled": true, "action": "throw", "checkAgents": false },
  "keep": { "classes": ["com/you/Main"] }
}
Always test the output. The heavier options (classGuard, aggressive flow, reflectionObfuscation) change a lot — run the protected build once before you ship it, and add keep rules for anything reflection touches.