Renombrado y estructura
Los nombres revelan más información que cualquier otra parte de un archivo de clase. getLicenceKey
lo cuenta todo a quien lo lee; ad no cuenta nada. El renombrado elimina ese significado de las clases, los métodos, los campos y el árbol de paquetes.
Renombrado
Actívalo en techniquesy cada clase, método y campo que no hayas conservado expresamente recibirá un nombre nuevo y carente de significado. Las referencias se reescriben para que coincidan, por lo que el código sigue enlazando y ejecutándose, pero deja de describirse a sí mismo.
{ "techniques": { "rename": true } }
Antes y después
Este es un resultado real: un pequeño Cart que almacena un Money subtotal, procesado por el renombrado y descompilado de nuevo a Java. Observa cómo los tipos, campos, métodos e incluso
import se reducen a nombres de dos letras mientras sus relaciones permanecen intactas:
package shop.model;
import shop.util.Money;
public class Cart {
private Money subtotal = new Money(0L);
private int itemCount = 0;
public void addItem(Money money) {
this.subtotal = this.subtotal.plus(money);
++this.itemCount;
}
public Money subtotal() {
return this.subtotal;
}
public int itemCount() {
return this.itemCount;
}
}
// Money -> aa (its own package)
package aa;
public final class aa {
public final /* synthetic */ long aa;
public /* synthetic */ aa(long l) {
this.aa = l;
}
public /* synthetic */ aa aa(aa aa2) {
return new aa(this.aa + aa2.aa);
}
public /* synthetic */ long ab() {
return this.aa;
}
}
// Cart -> ab (references to Money become aa.aa)
package ab;
import aa.aa;
public class ab {
public /* synthetic */ aa ab;
public /* synthetic */ int ac;
public /* synthetic */ ab() {
this.ab = new aa(0L);
this.ac = 0;
}
public /* synthetic */ void ac(aa aa2) {
this.ab = this.ab.aa(aa2);
++this.ac;
}
public /* synthetic */ aa ad() {
return this.ab;
}
public /* synthetic */ int ae() {
return this.ac;
}
}
El /* synthetic */ descompilador indica que incluso los metadatos de los miembros se han marcado para despistar a las herramientas. Ya no queda ningún Cart,
Money, subtotal or addItem que buscar.
Sobrecarga de métodos
El renombrado asigna un nombre nuevo a cada método. De forma predeterminada todos son distintos, pero no tienen por qué serlo. La JVM distingue dos métodos por su descriptor completo —los tipos de parámetros
y tipo de retorno—, no solo por el nombre. Una clase puede contener válidamente métodos con el mismo nombre que solo difieran en la firma. Activa rename.overloadMethods y el renombrador reutilizará un nombre corto siempre que sea seguro:
{ "rename": { "overloadMethods": true } }
El resultado es una clase repleta de métodos con el mismo nombre, incluidos varios que solo difieren por el tipo de retorno. Este último caso es bytecode válido, pero no es código fuente Java válido: ningún descompilador puede convertirlo en código que vuelva a compilar. Aquí aparece el mismo Wallet del ejemplo anterior, renombrado con la sobrecarga activada y descompilado:
public final class Wallet {
private long balance;
public long getBalance() {
return balance;
}
public void credit(long amount) {
balance += amount;
}
public void debit(long amount) {
balance -= amount;
}
public Wallet copy() {
Wallet w = new Wallet();
w.balance = balance;
return w;
}
public boolean isEmpty() {
return balance == 0;
}
}
public final class a {
private long a;
public long a() { // getBalance ()J
return this.a;
}
public void a(long var1) { // credit (J)V
this.a += var1;
}
public void b(long var1) { // debit (J)V — shares (J)V with credit, so it can't also be a(long)
this.a -= var1;
}
public a a() { // copy ()La;
a var1 = new a();
var1.a = this.a;
return var1;
}
public boolean a() { // isEmpty ()Z
return this.a == 0L;
}
}
// Three methods named a() with no parameters, differing only by return type
// (long, a, boolean). Legal in bytecode — the JVM resolves by full descriptor —
// but ILLEGAL Java: this source will not recompile. The decompiler has no choice
// but to emit it. credit/debit share the descriptor (J)V, so they must differ (a / b).
Tres métodos llamados a() sin parámetros que solo se diferencian en si devuelven long, a or boolean. Quien lea la salida debe retener toda esa información; cualquier herramienta que intente recompilarla fallará. Es seguro por construcción: dos métodos solo comparten nombre cuando ninguna clase puede resolver ambos con el mismo descriptor. Es opcional: overloadMethods tiene como valor predeterminado false.
keep frente a rename.exclude
El renombrado rompe cualquier elemento que se localice por su nombre durante la ejecución: reflexión, carga de clases mediante cadenas, la clase principal de un plugin que busca el servidor o campos vinculados desde la configuración. Hay dos formas de excluir una clase, y elegir la correcta es la causa de confusión más habitual:
rename.exclude— la opción habitual. La clase y los nombres de sus métodos y campos se conservan exactamente como están escritos, por lo que todo lo que la resuelva por nombre seguirá funcionando, pero la ofuscación del flujo y el cifrado de cadenas siguen protegiendo los cuerpos. Esto es lo apropiado para puntos de entrada, API públicas y objetivos de reflexión: conserva los nombres y protege el interior. También puedes conservar solo una parte de la identidad; consulta las exclusiones granulares más abajo. Un punto de entrada normalmente solo necesita conservar su nombre de clase , por lo que sus campos y métodos todavía pueden ofuscarse.keep— exclusión absoluta y último recurso. Una clase incluida en keep no se modifica en absoluto: no se renombra, no se ofusca el flujo, no se cifran las cadenas ni se sella. Se distribuye totalmente legible. Úsalo solo cuando el bytecode realmente deba permanecer intacto, por ejemplo, para formatos serializados, protocolos que exigen estabilidad binaria o bibliotecas de terceros que prefieras dejar completamente intactas.
Por tanto, la clase principal de un plugin, una API pública o un listener registrado por nombre deben ir en rename.exclude, no en keep. Usa keep solo cuando quieras renunciar deliberadamente a proteger el flujo y las cadenas de esa clase.
{
"rename": {
"exclude": ["com/you/MyPlugin", "com/you/api/", "com/you/EventListener"]
},
"keep": {
"packages": ["com/you/model"],
"classes": ["com/you/wire/Protocol"],
"members": [
{ "classPattern": "com/you/**", "methodPattern": "on*" }
]
}
}
- rename.exclude — conserva los nombres de la clase y sus miembros —por nombre interno o prefijo de paquete—, pero sigue ofuscando los cuerpos.
- keep.packages — conserva árboles de paquetes completos mediante su prefijo interno, totalmente intactos.
- keep.classes — conserva clases concretas mediante su nombre interno, totalmente intactas.
- keep.members — control detallado: un
classPatternjunto con unmethodPatterno unfieldPattern, y undescriptor. Conserva solo los nombres de esos miembros; el resto de la clase se renombra. Los patrones usan*—un segmento— y**—cualquier profundidad—.
com/you/MyPlugin, nunca com.you.MyPlugin. Una barra final (com/you/api/) coincide con un paquete completo; un nombre sin más coincide con una clase y sus clases internas. El mismo patrón funciona en rename.exclude,
flow.exclude, stringEncryptionExclude y
classGuard.encrypt.Exclusiones granulares: conserva solo lo imprescindible
De forma predeterminada, excluir una clase conserva toda su identidad: el nombre de la clase, los nombres de los campos y los nombres de los métodos. A menudo es más de lo necesario, y cada nombre conservado es una pista que se puede seguir. La clase principal de un plugin solo necesita conservar su nombre de clase para que el servidor pueda cargarla; sus campos —licenseService, apiClient…— y sus métodos pueden y deben seguir ofuscándose. Conservarlos solo para mantener cargable la clase entrega gratis un mapa a quien la revierta.
So rename.exclude las entradas admiten un ámbito opcional sufijo después de
# : cualquier combinación de name, fields,
methods:
{
"rename": {
"exclude": [
"com/you/MyPlugin#name",
"com/you/api/#name,methods",
"com/you/Config#fields",
"com/you/Legacy"
]
}
}
| Sufijo | Conserva | Úsalo para |
|---|---|---|
#name | solo el nombre de la clase | la clase principal de un plugin o el Main-Classde un JAR ejecutable, o una clase cargada mediante reflexión: se carga por nombre, pero todo su interior se ofusca |
#name,methods | nombres de la clase y de los métodos | una API pública a la que otro código llama por el nombre del método |
#fields | solo los nombres de los campos | una clase cuyos campos se vinculan por nombre —configuración o serialización—, pero cuyos métodos quieres ocultar |
| (ninguno) | todo | compatible con versiones anteriores: una entrada sin sufijo conserva el nombre, los campos y los métodos |
#nameen la clase principal sigue conservando onEnable, onDisable y cualquier método que sobrescriba un tipo de la plataforma. Se fijan automáticamente porque el entorno los resuelve contra la biblioteca. Solo necesitas conservar el nombre; el motor se ocupa de los callbacks.Estrategias de paquetes
El renombrado no solo cambia nombres: puede reconstruir todo el árbol de paquetes y destruir el mapa arquitectónico que revela su estructura. Aquí se muestra el mismo JAR de seis clases con cada valor de packageStrategy. En todos los casos es el listado real del JAR; el shop/App punto de entrada, incluido en rename.exclude, permanece en su sitio):
demo/Heavy.class
demo/Report.class
demo/Sample.class
shop/App.class
shop/model/Cart.class
shop/util/Money.class
ROOT: sin paquetes
Todo se traslada al paquete predeterminado. El resultado es plano y anónimo, y dificulta el trabajo de las herramientas que presuponen una estructura de paquetes:
aa.class
ab.class
ac.class
ad.class
ae.class
shop/App.class
FLAT: un único paquete compartido
Todas las clases acaban en un único paquete generado, por lo que no queda rastro de la agrupación original:
aa/aa.class
aa/ab.class
aa/ac.class
aa/ad.class
aa/ae.class
shop/App.class
WRAPPER: un paquete por clase —valor predeterminado—
Cada clase recibe su propio paquete de un solo segmento. Es un valor predeterminado ordenado que mantiene operativas las herramientas basadas en una clase por archivo sin revelar información:
aa/aa.class
ab/ab.class
ac/ac.class
ad/ad.class
ae/ae.class
shop/App.class
RANDOM: distribución en un conjunto de paquetes
Las clases se distribuyen al azar entre paquetes generados —cuyo número define
packagePool—, por lo que clases sin relación comparten paquete y otras relacionadas quedan separadas:
aa/aa.class
aa/ab.class
aa/ac.class
ac/ae.class
ad/ad.class
shop/App.class
MIRROR: misma estructura, nombres nuevos
Conserva la profundidad de anidamiento original, pero renombra cada segmento. Las dos últimas clases siguen teniendo dos niveles, como los shop/model y shop/util de los que proceden; resulta útil si algún componente espera una profundidad determinada:
aa/aa.class
ab/ab.class
ac/ac.class
ad/ae/ad.class
af/ag/ae.class
shop/App.class
KEEP: renombra las clases y conserva su paquete
La opción más conservadora: todas las clases permanecen en su paquete original y solo cambia su nombre simple. El árbol de paquetes queda intacto: compáralo con la estructura original; es el mismo árbol con las hojas renombradas:
demo/aa.class
demo/ab.class
demo/ac.class
shop/App.class
shop/model/ad.class
shop/util/ae.class
protected y package-private desde el mismo paquete. Las demás estrategias trasladan las clases, por lo que dos clases antes vecinas pueden quedar separadas y una llamada que dependía del paquete compartido fallará IllegalAccessError durante la ejecución. Si tu plugin usa acceso protected o package-private entre sus clases y aparecen errores después de ofuscarlo, cambia a KEEP. FLAT —todo en un paquete— también lo corrige y oculta más, pero KEEP es la opción más segura porque no altera la estructura. A cambio, conservar los paquetes revela la estructura del autor o del plugin —com/yourname/yourplugin/…—, justo el mapa que eliminan las demás estrategias. Úsalo solo cuando la semántica de acceso lo exija.protectedo package-private, enumera sus prefijos en
preservePackages : conservarán la ruta original y el resto seguirá tu packageStrategy. Así corriges el acceso revelando la mínima estructura. La coincidencia se hace por prefijo en formato JVM —con barras—, por ejemplo,
com/you/internal. No es lo mismo que
keep.packages, que conserva todos los nombres intactos; aquí las clases sí se renombran, pero no se trasladan.Paquete de las clases generadas
Lock Master crea algunas clases auxiliares de ejecución para funciones como el cifrado de cadenas, el flujo invokedynamic, el conjunto externo de recursos, ClassGuard y anti-debug. De forma predeterminada, sus paquetes se aleatorizan para integrarse en el JAR protegido. Si un entorno de pruebas, una política antivirus o una lista de permitidos necesita una ubicación predecible, activa rename.generatedPackage:
{
"rename": {
"generatedPackage": {
"enabled": true,
"name": "com.you.internal"
}
}
}
The package name accepts normal dotted Java form (com.you.internal) or JVM form (com/you/internal). It must be a valid, non-empty package. This setting only places classes generated by Lock Master; your original classes still follow packageStrategy.
{
"rename": {
"generatedPackage": {
"enabled": false,
"name": "com.you.internal"
}
}
}
enabled to false restores randomized packages; the saved name is ignored, so you can toggle the option without deleting it.Clase principal y descriptores de plugins
La clase de entrada del plugin se nombra dos veces: como .classy como
main referencia dentro del descriptor de la plataforma. Renombrar solo la clase dejaría el descriptor apuntando a un nombre inexistente. Lock Master mantiene ambos sincronizados y reescribe la
main referencia a la clase renombrada en todos los descriptores que encuentra:
plugin.yml— Bukkit, Spigot y Paperpaper-plugin.yml— Paper modernobungee.yml— BungeeCord y Waterfallvelocity-plugin.json— Velocity
De este modo, un JAR multiplataforma puede tener su clase de entrada renombrada en todas las plataformas a la vez y ya no necesitas
keep la clase principal solo para que el JAR cargue. Está activado de forma predeterminada; configura
rewritePluginDescriptors: false para no modificar los descriptores.
main referencia, pero no una clase que el entorno cargue por nombre de cualquier otra forma: un
ServiceLoader provider, una clase cargada mediante reflexión o el Main-Class
de un JAR ejecutable no tienen descriptor que sincronizar. Inclúyelos en
rename.exclude. Tampoco selles nunca una clase así con
ClassGuard : debe permanecer en claro con su nombre real. La ofuscación del flujo y el cifrado de cadenas siguen protegiéndola.velocity-plugin.json, que se reescribe aquí. Si la compilación conecta Velocity únicamente mediante la @Plugin anotación sin descriptor JSON, comprueba que el módulo siga cargando después de ofuscarlo y avísanos si falla.Campos de rename
El rename ajusta cómo se generan los nombres. Los valores predeterminados sirven en la mayoría de los casos:
| Campo | Qué hace |
|---|---|
| exclude | Lista de clases y paquetes que no se renombrarán. Puedes combinarlos libremente: un nombre sin más (com/you/Main) representa una clase y sus clases internas; un pkg/ prefijo, un árbol de paquetes completo. Se conservan los nombres de la clase y de sus miembros, por lo que todo lo que se resuelva por nombre seguirá funcionando mientras se mantienen la ofuscación del flujo y el cifrado de cadenas. Es la exclusión específica del renombrado, además de keep. Añade un sufijo de ámbito (#name, #fields, #methods) para conservar solo parte de la identidad. Por ejemplo, com/you/Main#name conserva solo el nombre de la clase y ofusca sus miembros. Así mantienes cargable un punto de entrada o una API pública sin desprotegerlos. |
| chars | Conjunto de caracteres para los nombres nuevos —valor predeterminado: a–z—. Usa glifos confundibles o no latinos para dificultar su lectura. |
| deep | Longitud mínima de los nombres generados; solo la superan cuando se agota el conjunto de nombres cortos. |
| packageStrategy | Cómo se reconstruye el árbol de paquetes: ROOT, FLAT, WRAPPER —valor predeterminado—, RANDOM, MIRROR, KEEP , mostrados anteriormente. |
| packageDepth | Profundidad de la estructura de paquetes generada. |
| preservePackages | Prefijos —en formato JVM— cuyas clases conservan su paquete original mientras el resto sigue packageStrategy : equivale a un KEEPpor paquete. Las clases siguen renombrándose; consulta la explicación anterior. |
| packagePool | Para RANDOM: número de paquetes entre los que se reparten las clases. |
| generatedPackage | Optional fixed package for generated helper and infrastructure classes: {"enabled":true,"name":"com/you/internal"}. Dots or JVM slashes are accepted. Disabled by default; disabling restores randomized packages. |
| rewritePluginDescriptors | Mantiene la main referencia de los descriptores sincronizada con la clase principal renombrada —plugin.yml, paper-plugin.yml, bungee.yml, velocity-plugin.json—. Valor predeterminado: true ; consulta la explicación anterior. |
El renombrado es determinista cuando defines una seed : una misma entrada produce compilaciones idénticas byte por byte. Sin embargo, no fija los nombres entre versiones: los nombres ofuscados se asignan por orden y cambian cuando cambia el código. Si otro código se compila contra una API pública, keep esa API para que sus nombres nunca cambien.