Back to blogs

Sam Rivera Build

Shipping Per-Slot Jars from the Gradle Plugin

The HashGuard Gradle plugin wraps the engine as a plrObfuscate task. Apply it in the Minecraft client project and one command produces per-slot jars, each encrypted with its own keys.

One task, N jars

Apply fr.polargames.plr-obfuscator and configure the plrObfuscator extension. The task takes your compiled jar and an obfuscator config, then writes client_0.jar to client_N.jar and a keys-manifest.json into build/plr-obfuscated.

Applying the plugin from an included build kotlin
// settings.gradle.kts of the Minecraft client projectpluginManagement {    includeBuild("/path/to/PLR-Obfuscator")}// build.gradle.ktsplugins {    id("fr.polargames.plr-obfuscator")}plrObfuscator {    inputJar   = tasks.named<Jar>("jar").flatMap { it.archiveFile }    configFile = file("obfuscator-config.json")    slotCount  = 100   // default 1, handy for local tests}
Run it bash
./gradlew plrObfuscate

The extension options

  • inputJar is the compiled jar to obfuscate, and it is required.
  • configFile is the path to the obfuscator config JSON, and it is required.
  • slotCount is the number of jars to produce, from 1 to 100, 1 by default.
  • outputDir is the output folder, build/plr-obfuscated by default.
  1. 01 Parse config Read the ObfuscatorConfig JSON
  2. 02 Draw seeds One random 32-byte seed per slot, from SecureRandom
  3. 03 Obfuscate N slots In parallel on virtual threads, each with its own keys
  4. 04 Write output client_0.jar to client_N.jar and keys-manifest.json
What plrObfuscate does for each run.

Where the keys come from

For every slot the plugin draws a random 32-byte seed with SecureRandom and derives two keys from it with HKDF: a 16-byte AES key with info plr-classfile-aes and a 32-byte HMAC key with info plr-classfile-hmac. Each slot then runs the obfuscator with its own keys, and magicCrypter encrypts every class into the PLRG format.

Rotation without bookkeeping

Seeds are not persisted. Every ./gradlew plrObfuscate generates new keys, and that is the rotation: nothing to store, nothing to forget to rotate. The keys-manifest.json lists the derived keys in hex for every slot, and it is meant to be consumed in the same CI run that produced it.

Shape of keys-manifest.json json
{  "slot_count": 100,  "slots": [    { "slot": 0, "aes_key_hex": "...(32 hex)...", "hmac_key_hex": "...(64 hex)..." },    { "slot": 1, "aes_key_hex": "...", "hmac_key_hex": "..." }  ]}

A requirement: magicCrypter

The PLRG format only appears when magicCrypter is true in the config of the jar you build. A PLRG class replaces CAFEBABE with PLRG, followed by a 16-byte IV, an HMAC-SHA256 and the AES-128-CTR ciphertext of the zlib-compressed class. It needs the PLR JVM to load.

At runtime the slot is derived from the player’s id with sha256(user_uuid)[0:4] % slot_count, and the launcher downloads the matching jar.