r Lock MasterAccount

String encryption

Plain string literals are the fastest way into a program — search the class file for a message and you're standing on the logic that uses it. Lock Master removes every readable literal and rebuilds it, encrypted, only when it's actually needed.

Why it matters

Compiled Java keeps every string literal in the clear inside the constant pool. A license message, an API endpoint, a permission node, an error string — all of it is one strings command away, and each one is a signpost to the code around it. Encrypting them doesn't just hide the text; it removes the map an attacker uses to navigate.

You turn it on with a single field in techniques:

{ "techniques": { "stringEncryption": "POOL" } }

Every example below is real engine output — one small method, encrypted with each mode, then decompiled back to Java with a standard decompiler so you can see exactly what ships. The original:

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

The four modes

stringEncryption takes one of four values. They differ in where the encrypted data and the decrypt routine live — which is what changes how hard it is to hook or lift:

ValueWhat it does
NONEOff (the default). Literals ship in the clear.
PER_METHODEach string becomes a call to a per-class decrypt method, with the ciphertext passed as arguments. Keyed to the calling site.
INLINEThe decrypt loop is emitted in place at every use site — no shared helper method to set a breakpoint on.
POOLEvery string is moved out into a shared, keyed pool class and fetched by index. Smallest footprint, and the literals leave the class entirely.

All three encrypted modes share one property: the key is derived from the runtime caller(see callerBinding), so copying an encrypted string and its call into a different class makes it decrypt to garbage. There is no single "decrypt everything" routine to run.

PER_METHOD

The literal disappears; in its place is a call to a generated decrypt method (Sample.ahere) that takes the ciphertext and per-string parameters. The readable text is gone from the constant pool — what remains is an opaque payload of characters, shown here as \uXXXX escapes. Use theshow characters toggle on the block to flip between the escapes and the raw glyphs:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
After — PER_METHOD
public String tier(int n) {
    if (n >= 500) {
        return Sample.a("_", "\u23e4\u23e5\u23fd\u23fc", 9, 2, 6);
    }
    return Sample.a("_", "\u2300\u230d\u2306\u2303\u233f\u2320\u233d\u2351", 10, 6, 29);
}
Why do the characters look like arrows and symbols? The ciphertext isn't text in any language — it's arbitrary 16-bit values straight out of the cipher. A JavaString is UTF-16, so every char (0x0000–0xFFFF) is a valid character regardless of whether it spells anything, which is why this is legal Java. Random values land all over Unicode — arrows, technical and dingbat symbols, CJK, Mongolian, combining marks — so it reads as a jumble rather than one script. They're not real emoji (those live in higher planes and need a pair of escapes); a few BMP symbols just happen to render in an emoji-like style. The \uXXXXform is simply how a decompiler writes a non-ASCII character inside a string literal.

INLINE

No helper method at all: the decryption — a per-string XOR schedule over the ciphertext — is written straight into the method, so there's no shared routine to hook and every use site looks different. The plain strings "gold" and "standard" are nowhere to be found:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
After — INLINE
public String tier(int n) {
    if (n >= 500) {
        char[] cArray = "\u2787\u2785\u2799\u279b".toCharArray();
        int n2 = cArray.length * 29;
        for (int i = 0; i < cArray.length; ++i) {
            int n3 = i;
            cArray[n3] = (char)(cArray[n3] ^ (18208 + 1653 ^ 0x6A01 ^ i * 10 ^ i >> 1 ^ n2));
        }
        return new String(cArray);
    }
    char[] cArray = "\u2ba8\u2ba7\u2baa\u2bad\u2b9f\u2b92\u2b99\u2b87".toCharArray();
    int n4 = cArray.length * 26;
    for (int i = 0; i < cArray.length; ++i) {
        int n5 = i;
        cArray[n5] = (char)(cArray[n5] ^ (12523 + 9927 ^ 0x7CB9 ^ i * 8 ^ i >> 6 ^ n4));
    }
    return new String(cArray);
}

POOL

The strongest for footprint and separation: literals are lifted out of your class entirely into a shared pool class (ab here) and fetched by index. Your method keeps no string data at all — just an accessor call:

Your code
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
After — POOL
public String tier(int n) {
    if (n >= 500) {
        return ab.ba("x", 0);
    }
    return ab.aa("x", 0);
}

It works through concatenations too — here a greeting built from two literals and a variable. The literals become two pool lookups, and only the variable you passed in survives:

After — POOL (a concatenation)
public String welcome(String string) {
    String string2 = string;
    return (String)((Object)ab.aa("x", 2)) + string2 + (String)((Object)ab.ab("x", 2));
}

Options

Sensible defaults cover almost everyone; these fine-tune how the decryptor behaves:

FieldWhat it does
callerBindingSTACKWALKER (default) derives each key from the JVM-enforced runtime caller class, so lifting a string into another class yields the wrong key. STACKTRACE is cheaper but the caller can be spoofed.
cacheModeFAST caches the decrypted string after first use. SAFE re-decrypts on every access and zeroes the char[] afterwards — more secure, slower.
assetPoolWhere encrypted data lives: INLINE (in a helper class) or EXTERNAL (in META-INF/strings.dat, off the class files entirely).
Which should I pick? POOL is a great default — small and clean. Reach for PER_METHOD or INLINE when you want the decrypt logic spread across the code with no single choke point to attack.