FundamentosLección 12
Java 25 y la seguridad
Lo que Java 25, la versión LTS actual, cambia para quien escribe código seguro. Scoped Values con hilos virtuales, la API de derivación de claves, criptografía post-cuántica, PEM y el fin del Security Manager.
Al terminar sabrás
- Saber por qué Java 25 es la versión recomendada y qué cambia respecto a Java 21.
- Usar Scoped Values en lugar de
ThreadLocalpara datos de la petición con hilos virtuales. - Derivar claves con la API
KDF(HKDF) y conocer las primitivas post-cuánticas del JDK. - Entender por qué ya no existe el Security Manager y qué lo sustituye.
Java 25 (septiembre de 2025) es la versión LTS (Long-Term Support) actual y la que usa esta guía. Spring Boot 4 funciona desde Java 17, pero si empiezas un proyecto hoy, Java 25 es la elección natural: soporte largo y varias novedades que tocan directamente la seguridad. Esta lección recorre las que más importan, incluidas las que llegaron en Java 22–24 y quedan consolidadas en esta LTS.
| Novedad | JEP | Desde | Para qué te sirve |
|---|---|---|---|
| Scoped Values | 506 | 25 (final) | Compartir datos de la petición (usuario, tenant) de forma inmutable y segura con hilos virtuales |
| API de derivación de claves (KDF) | 510 | 25 (final) | Derivar claves con HKDF sin librerías externas |
| ML-KEM y ML-DSA | 496, 497 | 24 | Intercambio de claves y firmas resistentes a ordenadores cuánticos |
Hilos virtuales sin pinning en synchronized | 491 | 24 | synchronized ya no bloquea el hilo portador |
| Security Manager desactivado para siempre | 486 | 24 | Ya no es una frontera de seguridad: aísla con el sistema operativo o contenedores |
Ficheros fuente compactos y main de instancia | 512 | 25 (final) | Programas de prueba de una sola clase, sin public static ni imports del java.base |
| Codificación PEM | 470 | 25 (preview) | Leer y escribir claves y certificados PEM con la API estándar |
Scoped Values: contexto seguro con hilos virtuales
Sección titulada «Scoped Values: contexto seguro con hilos virtuales»Un ThreadLocal tiene tres problemas de seguridad: es mutable (cualquier código puede cambiarlo), hay que limpiarlo a mano (si no, el siguiente uso del hilo hereda la identidad del anterior) y su vida no está acotada. Un ScopedValue es inmutable y solo existe dentro del bloque donde se asigna:
Evita
static final ThreadLocal<String> TENANT = new ThreadLocal<>();
void handle(Request req) { TENANT.set(req.tenant()); try { process(req); } finally { TENANT.remove(); // si se olvida, fuga entre peticiones }}Prefiere
static final ScopedValue<String> TENANT = ScopedValue.newInstance();
void handle(Request req) { ScopedValue.where(TENANT, req.tenant()) .run(() -> process(req)); // fuera del bloque, TENANT no existe}
void process(Request req) { String tenant = TENANT.get(); // inmutable: nadie puede cambiarlo aquí}Desde Java 24, además, un hilo virtual dentro de un bloque synchronized ya no queda “anclado” a su hilo portador (JEP 491). Código como el TokenBucket de la lección de abuso de APIs ya no penaliza el rendimiento con hilos virtuales.
Derivar claves con la API KDF
Sección titulada «Derivar claves con la API KDF»Muchas veces tienes un secreto maestro y necesitas varias claves (una para cifrar, otra para firmar, una por tenant). Usar el mismo secreto para todo es un error clásico; la solución estándar es una función de derivación de claves como HKDF. Desde Java 25 viene en el JDK:
- Secreto maestrode un KMS o un intercambio de claves
- HKDF-SHA256
KDF.getInstance(…) - Claves independientesAES para cifrarHMAC para firmar
SecretKey deriveKey(byte[] masterSecret, byte[] salt, String purpose) throws GeneralSecurityException { KDF hkdf = KDF.getInstance("HKDF-SHA256"); AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract() .addIKM(masterSecret) .addSalt(salt) .thenExpand(purpose.getBytes(StandardCharsets.UTF_8), 32); // 32 bytes = 256 bits return hkdf.deriveKey("AES", params);}
SecretKey encryptionKey = deriveKey(master, salt, "notes:encryption:v1");SecretKey signingKey = deriveKey(master, salt, "notes:signing:v1");Criptografía post-cuántica
Sección titulada «Criptografía post-cuántica»Un ordenador cuántico suficientemente grande rompería RSA y las curvas elípticas. El riesgo de hoy es “cosechar ahora, descifrar después”: alguien guarda tráfico cifrado actual para descifrarlo dentro de unos años. Java 24 incorporó los algoritmos estandarizados por el NIST:
- ML-KEM (FIPS 203): encapsulación de claves, para acordar secretos (sustituto de ECDH).
- ML-DSA (FIPS 204): firmas digitales (sustituto de RSA/ECDSA para firmar).
KeyPair pair = KeyPairGenerator.getInstance("ML-DSA").generateKeyPair(); // ML-DSA-65 por defecto
Signature signer = Signature.getInstance("ML-DSA");signer.initSign(pair.getPrivate());signer.update(document);byte[] signature = signer.sign();La API es la misma que ya conoces de la lección de criptografía: solo cambia el nombre del algoritmo. Los JWT con firmas post-cuánticas aún no son habituales en los proveedores de identidad, pero conviene saber que el JDK ya está preparado.
PEM sin librerías (preview)
Sección titulada «PEM sin librerías (preview)»Las claves y certificados suelen venir en formato PEM (-----BEGIN PUBLIC KEY-----). Java 25 incluye, como preview, PEMEncoder y PEMDecoder para leerlos y escribirlos sin Bouncy Castle ni código manual de Base64:
String pem = PEMEncoder.of().encodeToString(publicKey);PublicKey key = PEMDecoder.of().decode(pem, PublicKey.class);Al ser preview, necesitas --enable-preview y la API puede cambiar en la siguiente versión. En Spring Boot, para claves de firma de JWT, sigue siendo más cómodo public-key-location o @ConfigurationProperties con RSAPublicKey (como en la lección de JWT).
El Security Manager ya no existe
Sección titulada «El Security Manager ya no existe»Durante 25 años, Java tuvo un Security Manager pensado para ejecutar código no confiable en una “caja de arena”. Desde Java 24 está desactivado de forma permanente (JEP 486): no se puede activar.
- Si tu aplicación lo usaba para limitar plugins o scripts, ya no protege nada.
- El aislamiento hoy se hace fuera de la JVM: contenedores, usuarios sin privilegios, sistemas de ficheros de solo lectura, seccomp, políticas de red.
- Y la regla de siempre: no ejecutes código ni expresiones que vengan del usuario (SpEL, scripts, plantillas) sin una lista blanca muy estricta.
Ficheros fuente compactos para experimentar
Sección titulada «Ficheros fuente compactos para experimentar»Para probar una idea de seguridad en un minuto ya no hace falta un proyecto: en Java 25 un fichero con un main de instancia importa automáticamente todo el módulo java.base y se ejecuta con java Fichero.java. El cálculo de PKCE de la lección de OAuth 2 queda así:
void main() throws Exception { byte[] random = new byte[32]; new SecureRandom().nextBytes(random); String verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(random);
byte[] hash = MessageDigest.getInstance("SHA-256").digest(verifier.getBytes(StandardCharsets.US_ASCII)); IO.println("verifier = " + verifier); IO.println("challenge = " + Base64.getUrlEncoder().withoutPadding().encodeToString(hash));}java Pkce.javaMini lab · 20 min
Contexto y claves con Java 25
Objetivo: Practicar Scoped Values y derivación de claves, y razonar sobre lo que cambió en la plataforma.
Con un JDK 25 instalado (java -version):
- Escribe un fichero compacto que derive con HKDF dos claves desde el mismo secreto de 32 bytes aleatorios, una con
info = "notes:encryption:v1"y otra con"notes:signing:v1", e imprime ambas en Base64. Ejecútalo dos veces con el mismo secreto y la misma sal. - Declara un
ScopedValue<String> USERy, dentro deScopedValue.where(USER, "ana").run(...), llama a un método que leaUSER.get(). Después intenta leerlo fuera del bloque. - Responde: un compañero propone “como ya no hay Security Manager, ejecutamos los scripts de los usuarios en la misma JVM con un
ClassLoaderaparte”. ¿Qué le dices?
Pista
Para el paso 2, busca qué excepción lanza get() cuando el valor no está asignado, y qué método te permite comprobarlo antes.
Mostrar respuestaOcultar respuesta
- Las dos claves son distintas entre sí (cambia
info) pero idénticas entre ejecuciones con el mismo secreto y sal: HKDF es determinista. Por eso puedes recalcular las claves cuando las necesites sin guardarlas, y versionarlas cambiandoinfo(…:v2). - Dentro del bloque,
USER.get()devuelve"ana". Fuera, lanzaNoSuchElementException: el valor solo existe durante la ejecución del bloque. Puedes comprobarlo conUSER.isBound(). No hay nada que limpiar ni posibilidad de que la siguiente petición herede “ana”. - Que un
ClassLoaderno es una frontera de seguridad: el script tendría los mismos permisos que la aplicación (ficheros, red, variables de entorno con secretos). Sin Security Manager, el aislamiento tiene que hacerse fuera de la JVM: un proceso o contenedor separado, sin credenciales, con red y sistema de ficheros restringidos, límites de CPU y memoria, o directamente un motor de reglas con un lenguaje acotado en lugar de código arbitrario.