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:
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:
| Value | What it does |
|---|---|
| NONE | Off (the default). Literals ship in the clear. |
| PER_METHOD | Each string becomes a call to a per-class decrypt method, with the ciphertext passed as arguments. Keyed to the calling site. |
| INLINE | The decrypt loop is emitted in place at every use site — no shared helper method to set a breakpoint on. |
| POOL | Every 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:
public String tier(int n) {
if (n >= 500) {
return "gold";
}
return "standard";
}
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);
}
String 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:
public String tier(int n) {
if (n >= 500) {
return "gold";
}
return "standard";
}
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:
public String tier(int n) {
if (n >= 500) {
return "gold";
}
return "standard";
}
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:
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:
| Field | What it does |
|---|---|
| callerBinding | STACKWALKER (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. |
| cacheMode | FAST caches the decrypted string after first use. SAFE re-decrypts on every access and zeroes the char[] afterwards — more secure, slower. |
| assetPool | Where encrypted data lives: INLINE (in a helper class) or EXTERNAL (in META-INF/strings.dat, off the class files entirely). |
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.