Cifrado de cadenas
Los literales de texto son la entrada más rápida a un programa: busca un mensaje en el archivo de clase y llegarás directamente a la lógica que lo utiliza. Lock Master elimina todos los literales legibles y los reconstruye, cifrados, únicamente cuando hacen falta.
Por qué importa
Java compilado conserva cada literal en claro dentro del pool de constantes. Un mensaje de licencia, un endpoint, un permiso o un error están a un solo strings comando de distancia, y cada uno señala el código que lo rodea. Cifrarlos no solo oculta el texto: elimina el mapa que utiliza un atacante para orientarse.
Se activa con un único campo en techniques:
{ "techniques": { "stringEncryption": "POOL" } }
Todos los ejemplos siguientes son salida real del motor — un método pequeño cifrado con cada modo y después descompilado de nuevo a Java con una herramienta estándar, para que veas exactamente lo que se distribuye. Este es el original:
public String tier(int n) {
if (n >= 500) {
return "gold";
}
return "standard";
}
Los cuatro modos
stringEncryption admite cuatro valores. Se diferencian por el lugar donde donde residen los datos cifrados y la rutina de descifrado; eso determina lo difícil que resulta interceptarlos o extraerlos:
| Value | Qué hace |
|---|---|
| NONE | Desactivado (valor predeterminado). Los literales se distribuyen en claro. |
| PER_METHOD | Cada cadena se convierte en una llamada a un método de descifrado propio de la clase, que recibe el texto cifrado como argumento. La clave está ligada al punto de llamada. |
| INLINE | El bucle de descifrado se inserta in place en cada punto de uso: no existe un método auxiliar compartido en el que colocar un breakpoint. |
| POOL | Todas las cadenas se trasladan a una clase de pool compartida y protegida con clave, y se recuperan por índice. Es la opción más compacta y elimina por completo los literales de la clase. |
Los tres modos cifrados comparten una propiedad: la clave deriva de la runtime caller (see callerBinding); por eso, copiar una cadena cifrada y su llamada a otra clase produce basura al descifrarla. No existe una única rutina que permita «descifrarlo todo».
PER_METHOD
El literal desaparece y lo sustituye una llamada a un método de descifrado generado (Sample.a
aquí) que recibe el texto cifrado y parámetros propios de cada cadena. El texto legible desaparece del pool de constantes; queda una carga opaca de caracteres mostrada como \uXXXX escapes. Usa el
show characters selector del bloque para alternar entre los escapes y los glifos sin procesar:
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 usa UTF-16, por lo que cada char entre 0x0000 y 0xFFFF es un carácter válido aunque no forme palabras; por eso sigue siendo Java válido. Los valores aleatorios aparecen por todo Unicode —flechas, símbolos técnicos, CJK, mongol o marcas combinadas— y parecen una mezcla de escrituras. No son not emoji reales —que viven en planos superiores y necesitan dos escapes—; algunos símbolos del BMP simplemente se muestran con aspecto de emoji. La \uXXXX
es únicamente la forma en que un descompilador representa un carácter no ASCII dentro de un literal.INLINE
No existe ningún método auxiliar: el descifrado —una secuencia XOR propia de cada cadena— se escribe directamente en el método. No hay una rutina compartida que interceptar y cada punto de uso es diferente. Las cadenas en claro "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
La opción más fuerte en tamaño y separación: los literales salen por completo de la clase y pasan a un pool compartido (ab aquí), desde donde se recuperan por índice. El método no conserva datos de texto, solo una llamada de acceso:
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);
}
También funciona con concatenaciones. Aquí, un saludo formado por dos literales y una variable: los literales se convierten en dos consultas al pool y solo permanece la variable recibida:
public String welcome(String string) {
String string2 = string;
return (String)((Object)ab.aa("x", 2)) + string2 + (String)((Object)ab.ab("x", 2));
}
Opciones
Los valores predeterminados sirven para casi todos los casos; estas opciones ajustan el comportamiento del descifrador:
| Campo | Qué hace |
|---|---|
| callerBinding | STACKWALKER (predeterminado) deriva cada clave de la clase llamadora impuesta por la JVM; mover una cadena a otra clase produce una clave incorrecta. STACKTRACE es más barato, pero permite falsificar el llamador. |
| cacheMode | FAST guarda en caché la cadena descifrada tras el primer uso. SAFE vuelve a descifrarla en cada acceso y después borra el char[] afterwards — more secure, slower. |
| assetPool | Ubicación de los datos cifrados: INLINE (en una clase auxiliar) o EXTERNAL (in META-INF/strings.dat, completamente fuera de los archivos de clase). |
| stringEncryptionExclude | Clases o paquetes —con nombres internos— cuyos literales deben seguir siendo legibles; es la exclusión específica del cifrado de cadenas, además de keep. Esas clases siguen sometiéndose a renombrado y ofuscación de flujo. |
POOL es un excelente valor predeterminado: pequeño y limpio. Usa PER_METHOD or INLINE cuando quieras distribuir la lógica de descifrado por el código sin un único punto que atacar.Excluir clases del cifrado de cadenas
El cifrado de cadenas se aplica a todas las clases. Para conservar algunos literales legibles —una versión que busca otra herramienta o textos que un framework compara por valor— añade esas clases a
stringEncryptionExclude. Esas clases siguen renombrándose y ofuscando su flujo; solo se desactiva su cifrado de cadenas.
La lista admite clases y paquetes combinados libremente, sin límite práctico. Cada entrada es un
internal name: a bare name (com/you/Version) coincide con esa clase y sus clases internas; una barra final (com/you/messages/) coincide con todo el árbol del paquete. Puedes combinar varios paquetes y clases individuales:
{
"techniques": {
"stringEncryption": "POOL",
"rename": true,
"stringEncryptionExclude": [
"com/you/Version",
"com/you/messages/",
"com/you/templates/"
]
}
}
pkg/ como prefijo, combinados en una sola lista— es la que utilizan
rename.exclude,
flow.exclude,
keep and
classGuard.encrypt
all use.