rymga ← Volver a Lock Master

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:

Tu código
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;
    }
}
Después: rename
// 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:

Tu código
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;
    }
}
Después: rename + sobrecarga
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 classPattern junto con un methodPattern o un fieldPattern, y un descriptor. Conserva solo los nombres de esos miembros; el resto de la clase se renombra. Los patrones usan * —un segmento— y ** —cualquier profundidad—.
Los nombres son nombres internos. Usa el formato de la JVM, con barras en vez de puntos: 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"
    ]
  }
}
SufijoConservaÚsalo para
#namesolo el nombre de la clasela 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,methodsnombres de la clase y de los métodosuna API pública a la que otro código llama por el nombre del método
#fieldssolo los nombres de los camposuna clase cuyos campos se vinculan por nombre —configuración o serialización—, pero cuyos métodos quieres ocultar
(ninguno)todocompatible con versiones anteriores: una entrada sin sufijo conserva el nombre, los campos y los métodos
Los callbacks del framework son seguros en cualquier caso. A #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):

Estructura original
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:

packageStrategy: ROOT
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:

packageStrategy: FLAT
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:

packageStrategy: WRAPPER (default)
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:

packageStrategy: RANDOM
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:

packageStrategy: MIRROR
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:

packageStrategy: KEEP
demo/aa.class
demo/ab.class
demo/ac.class
shop/App.class
shop/model/ad.class
shop/util/ae.class
Cuándo necesitas KEEP. Java permite acceder a los miembros 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.
¿Solo algunos paquetes? Casi nunca necesitas KEEP para todo el JAR. Si solo uno o dos paquetes dependen del acceso 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"
    }
  }
}
Úsalo para las pruebas de compatibilidad y desactívalo antes de publicar. A fixed package makes antivirus rules and diagnostics easier, but also makes Lock Master's support classes easier to identify. Setting 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 Paper
  • paper-plugin.yml — Paper moderno
  • bungee.yml — BungeeCord y Waterfall
  • velocity-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.

No renombres una clase que el entorno de ejecución resuelva por nombre. La reescritura del descriptor corrige esa 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.
Plugins de Velocity basados en anotaciones. Velocity resuelve la clase de entrada a partir del 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:

CampoQué hace
excludeLista 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.
charsConjunto de caracteres para los nombres nuevos —valor predeterminado: a–z—. Usa glifos confundibles o no latinos para dificultar su lectura.
deepLongitud mínima de los nombres generados; solo la superan cuando se agota el conjunto de nombres cortos.
packageStrategyCómo se reconstruye el árbol de paquetes: ROOT, FLAT, WRAPPER —valor predeterminado—, RANDOM, MIRROR, KEEP , mostrados anteriormente.
packageDepthProfundidad de la estructura de paquetes generada.
preservePackagesPrefijos —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.
packagePoolPara RANDOM: número de paquetes entre los que se reparten las clases.
generatedPackageOptional 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.
rewritePluginDescriptorsMantiene 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.