rymga ← Volver a Lock Master

Desacoplamiento de tipos (CHF)

Cifrado de clases es la protección más potente de Lock Master, pero por sí sola únicamente puede sellar clases a las que ningún otro elemento haga referencia por su tipo concreto. El desacoplamiento de tipos elimina esa limitación: reescribe el código para acceder a esas clases mediante interfaces en claro generadas automáticamente, de modo que casi toda la lógica puede sellarse. Es el mecanismo que permite ampliar ClassGuard desde unas pocas clases hasta la mayor parte del proyecto.

Qué hace

Durante la carga, la JVM comprueba que existan todas las clases a las que una clase cargable haga referencia . Por eso, una clase en claro que contenga un campo de tipo SecretVault, ejecute new SecretVault()o realice una conversión a ese tipo no puede coexistir con una clase sellada SecretVault : no habría bytecode que resolver. Por eso, el cifrado de clases por sí solo únicamente puede sellar clases que ya estén ocultas detrás de una interfaz.

El desacoplamiento de tipos —la familia Class Hierarchy Flattening del artículo, o CHF— lo soluciona automáticamente. Para cada clase que pueda transformar de forma segura:

  • extrae una interfaz en claro a partir de la API pública de la clase;
  • reescribe todas las referencias externas para usar esa interfaz en vez del tipo concreto;
  • canaliza la creación (new X(args)) mediante una factoría generada;
  • convierte el acceso a campos entre clases en llamadas a métodos de acceso.

Ahora ningún elemento en claro hace referencia a la clase concreta, solo a su interfaz, por lo que ClassGuard puede sellarla. No tienes que escribir nada: basta una opción.

{
  "classGuard":     { "enabled": true, "encrypt": ["*"] },
  "typeDecoupling": { "enabled": true, "exclude": [], "protectHotPaths": true }
}

Por qué existe: un antes y después real

Este es el mismo JAR: un pequeño motor de licencias cuya clase Main crea y llama por nombre a sus clases internas, como cualquier programa normal. A la izquierda, solo el cifrado de clases; a la derecha, la misma configuración con el desacoplamiento de tipos activado:

Solo ClassGuard
# classGuard alone, encrypt ["*"], NO type decoupling
> obfuscate demo.jar

ClassGuard: cleartext class 'com.rymga.demo.Main' a call passes/returns an
encrypted type (encrypted class 'SecretVault'), which can't be bridged.
Either add 'com.rymga.demo.Main' to the encrypt set too, or access
'SecretVault' through a cleartext interface + no-arg construction.

# build refused — a broken jar is never produced
+ desacoplamiento de tipos
# same config + typeDecoupling.enabled = true
> obfuscate demo.jar

Type decoupling analysis:
  Classes requested for protection: 11
  Classes decoupled: 9
  Classes encrypted: 10
  Classes left cleartext: 5
JAR written: demo-obf.jar        # runs identically

Sin desacoplamiento, tendrías que escribir a mano una interfaz para cada clase que quisieras sellar y canalizar todo tu código a través de ella; en un proyecto real no sería viable. Al activarlo, el motor realiza exactamente esa transformación: nueve de once clases se desacoplan y se sellan dentro del conjunto cifrado, el JAR se comporta exactamente igual y las dos clases que no pueden transformarse con seguridad permanecen en claro en vez de romper la compilación.

Activarlo

Tú indicas la intención —qué paquetes quieres proteger— y el motor decide qué puede transformar con seguridad. La interfaz pública completa consta de tres campos:

CampoQué hace
enabledActiva el desacoplamiento de tipos. Al hacerlo, ClassGuard sella las clases que el desacoplamiento haya podido preparar con seguridad.
excludeNombres internos o prefijos de paquetes —en formato JVM, con barras— que deben quedar fuera del desacoplamiento; por ejemplo, una ruta crítica de rendimiento que prefieras dejar como acceso directo. Utiliza el mismo patrón de coincidencia que el resto de opciones.
protectHotPathsEvita los enlaces más costosos —acceso estático mediante reflexión— en clases sensibles al rendimiento. Está activado de forma predeterminada.

El motor decide por sí solo todos los detalles internos: cómo extraer interfaces, cómo crear factorías y cuándo fusionar o aplanar una clase. No hay opciones internas que combinar; la complejidad reside en la herramienta, no en la configuración.

Funciona junto con tu encrypt lista. El desacoplamiento de tipos no cambia lo que eliges sellar, sino que permite que una mayor parte de esa selección sea posible. Combínalo con classGuard.encrypt de la forma habitual: el conjunto sellado es la intersección entre las clases solicitadas y las que el desacoplamiento haya hecho seguras.

Cómo funciona con el cifrado de clases

El desacoplamiento y ClassGuard se ejecutan como un único proceso:

  1. El planificador analiza cada clase que has pedido proteger y la clasifica: puede desacoplarse con seguridad o debe seguir siendo concreta.
  2. Para el conjunto seguro genera las interfaces en claro y la factoría, y reescribe todas las referencias.
  3. Después, ClassGuard sella exactamente ese conjunto desacoplado dentro del conjunto cifrado.
  4. Antes de escribir el JAR, el resultado se analiza, se comprueban sus frames y se enlaza en una JVM limpia.

La compilación muestra un breve informe para que puedas ver el reparto de un vistazo:

Type decoupling analysis:
  Classes requested for protection: 184
  Classes decoupled: 137
  Classes encrypted: 137
  Classes left cleartext: 47
  Exclusion reasons:
    21 lambda / invokedynamic constraints
    14 external hierarchy constraints
     8 unresolved optional dependencies

Qué permanece en claro automáticamente

Algunas clases no deben sellarse ni renombrarse porque el entorno las carga por nombre. Lock Master las detecta y mantiene en claro de forma automática; no hace falta configurarlas:

  • la clase Main-Class del manifiesto y el campo main;
  • ServiceLoader y los proveedores SPI enumerados en META-INF/services/;
  • el cargador guardián y su propia infraestructura.

Los callbacks de los frameworks también siguen funcionando: un método onEnable, un manejador de eventos o cualquier elemento que sobrescriba un tipo de una biblioteca se resuelve contra esa biblioteca, por lo que su nombre se conserva de forma independiente. Tú seleccionas los paquetes que quieres proteger; el motor protege los elementos realmente internos y mantiene cargables los puntos de entrada.

Diseñado para no generar resultados inválidos

Una clase que no pueda transformarse con seguridad —porque se accede a ella mediante una lambda, una dependencia opcional sin resolver, una superclase externa o reflexión— se excluye automáticamente y permanece en claro; nunca se fuerza la transformación. Además, antes de escribir el JAR se aplican tres controles: detección de métodos duplicados, comprobación de enlaces internos del JAR y verificación completa de la JVM. Si alguna transformación produjera bytecode imposible de cargar, se revierte toda la fase de desacoplamiento y se genera un JAR válido, aunque menos desacoplado. Nunca se genera un JAR roto.

Proporciona las bibliotecas. El desacoplamiento analiza la jerarquía de clases, por lo que, al igual que el resto del proceso, necesita las dependencias de compilación y ejecución en la ruta libraries para resolver tipos externos. Sin ellas adopta un enfoque conservador y desacopla menos clases.

El límite

El desacoplamiento de tipos permite que el cifrado de clases llegue más lejos; el cifrado es la protección real porque elimina el bytecode del disco. Sin embargo, ningún ofuscador es invulnerable y no fingiremos lo contrario. La JVM debe ejecutar el código, así que una clase sellada sí existe en claro en memoria durante la ejecución en una máquina controlada por el atacante, y puede capturarse mediante un agente o un entorno instrumentado. Anti-dump eleva enormemente el coste de capturar clases durante la ejecución: la presencia de un depurador o agente provoca una denegación ante fallos y las clases nunca se descifran bajo observación. Aun así, no puede vencer a un atacante con control total y programable del entorno. La protección obtenida es concreta: impide descompilar el artefacto en reposo, detiene la ingeniería inversa casual y automatizada y encarece mucho un análisis manual decidido. No protege frente a quien controla por completo el entorno de ejecución; es un límite común a todos los ofuscadores de bytecode. Diseña tu estrategia teniendo esto en cuenta.