Ir al contenido

FundamentosLección 04

Criptografía para desarrolladores

Cifrado simétrico y asimétrico, MAC y firmas digitales, TLS y gestión de claves. Lo que necesitas para usar la criptografía bien, sin inventarla.

Intermedio17 min de lectura

Al terminar sabrás

  • Elegir entre cifrado simétrico, asimétrico, MAC y firma según el problema.
  • Cifrar y descifrar con AES-GCM en Java sin errores clásicos (ECB, nonce repetido).
  • Entender qué garantiza TLS y qué papel juegan los certificados.
  • Aplicar reglas básicas de gestión de claves y aleatoriedad.

La primera regla de la criptografía aplicada es no inventar criptografía. La segunda es saber qué pieza ya hecha usar para cada problema. Esta lección es un mapa: qué hace cada primitiva, cuándo usarla y cómo no romperla al usarla.

  • Simétrica (AES, ChaCha20): rapidísima, ideal para cifrar datos. El problema es compartir la clave.
  • Asimétrica (RSA, curvas elípticas como ECDH/ECDSA/Ed25519): un par de claves. La pública se puede publicar; la privada nunca sale de su dueño. Es lenta, así que se usa para acordar claves y firmar, no para cifrar grandes volúmenes.

En la práctica se combinan: la asimétrica sirve para acordar una clave simétrica de sesión, y la simétrica cifra todo lo demás. Eso es exactamente lo que hace TLS.

Integridad y autenticidad: hash, MAC y firma

Sección titulada «Integridad y autenticidad: hash, MAC y firma»

Cifrar oculta un mensaje, pero no impide que alguien lo modifique. Para detectar cambios hay tres herramientas, y la diferencia está en quién puede generar la prueba y quién puede verificarla:

HerramientaQuién la generaQuién la verificaPruebaUso típico
Hash (SHA-256)CualquieraCualquieraQue el contenido no cambió si el hash viaja por un canal fiableChecksums de descargas
MAC (HMAC-SHA256)Quien tiene la claveQuien tiene la misma claveIntegridad + que lo generó alguien con la claveWebhooks, JWT HS256, cookies firmadas
Firma (RSA, ECDSA, Ed25519)Solo el dueño de la privadaCualquiera con la públicaIntegridad + autoría (no repudio)JWT RS256, certificados, actualizaciones de software

Hoy el modo por defecto es AES-GCM, un cifrado autenticado (AEAD): cifra y, a la vez, añade un tag que detecta cualquier modificación. Si alguien cambia un solo bit, descifrar falla en lugar de devolver basura.

AesGcm.java
private static final SecureRandom RANDOM = new SecureRandom();
static byte[] encrypt(SecretKey key, byte[] plaintext) throws GeneralSecurityException {
byte[] iv = new byte[12]; // nonce de 96 bits
RANDOM.nextBytes(iv); // ¡nuevo en CADA cifrado!
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] ciphertext = cipher.doFinal(plaintext); // incluye el tag de 128 bits
return ByteBuffer.allocate(iv.length + ciphertext.length).put(iv).put(ciphertext).array();
}
static byte[] decrypt(SecretKey key, byte[] message) throws GeneralSecurityException {
ByteBuffer buf = ByteBuffer.wrap(message);
byte[] iv = new byte[12];
buf.get(iv);
byte[] ciphertext = new byte[buf.remaining()];
buf.get(ciphertext);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
return cipher.doFinal(ciphertext); // lanza AEADBadTagException si se manipuló
}

Y la clave se genera, nunca se inventa:

KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(256);
SecretKey key = generator.generateKey();
Firma.java
KeyPair pair = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(pair.getPrivate());
signer.update(document);
byte[] signature = signer.sign();
Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(pair.getPublic());
verifier.update(document);
boolean valid = verifier.verify(signature); // false si cambió un solo byte

TLS crea un canal cifrado y autenticado entre el cliente y el servidor. Un resumen de TLS 1.3:

NavegadorclienteServidorapi.example.com1ClientHelloversiones, cifrados, su parte de ECDHE2ServerHello + certificadosu parte de ECDHE, firmado con su clave privadaEl navegador verifica la cadena de certificados hasta una CA de confianza y que el dominio coincide3Finishedambos derivan la misma clave de sesión4GET /api/notes (cifrado)5200 OK (cifrado)
TLS 1.3 simplificado: el certificado prueba la identidad del servidor y ECDHE acuerda claves que nunca viajan por la red.

TLS garantiza confidencialidad e integridad en tránsito y autentica al servidor (gracias al certificado). Lo que no hace:

  • No protege los datos en reposo (en tu base de datos o en tus logs).
  • No autentica al usuario: para eso están la contraseña, el token o la sesión. (Salvo que uses mTLS, donde el cliente también presenta certificado; es común entre microservicios.)
  • No te protege si el propio cliente está comprometido, ni de lo que tu aplicación haga mal.

Java 24 y 25 añaden al JDK la derivación de claves con HKDF (KDF), los algoritmos post-cuánticos ML-KEM y ML-DSA y, como preview, la lectura de claves PEM: lo verás en Java 25 y la seguridad.

La criptografía traslada el problema de proteger los datos al de proteger las claves:

  • Nunca en el código ni en el repositorio. Variables de entorno como mínimo; mejor un gestor de secretos o un KMS (AWS KMS, OCI Vault, HashiCorp Vault, Azure Key Vault).
  • Rotación planificada: identifica cada clave (kid) para poder publicar la nueva antes de retirar la vieja.
  • Separación: una clave por propósito (firmar tokens, cifrar datos, firmar cookies).
  • Aleatoriedad criptográfica: tokens, sales, IVs y claves salen de SecureRandom (Java) o crypto.getRandomValues (navegador). Nunca de Random ni de Math.random, que son predecibles.

Evita

// Predecible: un atacante puede reconstruir la secuencia
String resetToken = Long.toHexString(new Random().nextLong());

Prefiere

byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
String resetToken = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);

Mini lab · 15 min

Manipula un mensaje cifrado

Objetivo: Comprobar que AES-GCM detecta cualquier modificación y elegir la primitiva correcta para cada caso.

  1. Copia encrypt y decrypt de arriba. Cifra el texto "saldo=100", cambia un bit del resultado (por ejemplo message[20] ^= 1) e intenta descifrarlo. ¿Qué pasa?
  2. Cifra dos veces el mismo texto con la misma clave. ¿Los resultados son iguales? ¿Por qué es importante?
  3. Elige la primitiva para cada caso: (a) guardar el IBAN de un cliente para cobrarle cada mes; (b) que tus microservicios verifiquen tokens emitidos por un servidor central; (c) verificar que un webhook viene de tu proveedor de pagos, con quien compartes un secreto; (d) guardar contraseñas.
Pista

Para el punto 3, pregúntate en cada caso: ¿necesito recuperar el dato original? ¿Quién debe poder generar la prueba y quién solo verificarla?

Mostrar respuestaOcultar respuesta
  1. doFinal lanza AEADBadTagException: el tag de autenticación ya no cuadra. GCM no devuelve un texto alterado; se niega a descifrar. Eso es lo que significa “cifrado autenticado”.
  2. Son distintos, porque cada cifrado usa un IV aleatorio. Si fueran iguales, un observador sabría cuándo se repite un mensaje (por ejemplo, “saldo=100” otra vez) sin necesidad de descifrarlo.
  3. (a) Cifrado simétrico (AES-GCM) con la clave en un KMS: necesitas recuperar el dato. (b) Firma asimétrica (RS256/ES256/EdDSA): los servicios verifican con la clave pública y no pueden fabricar tokens. (c) HMAC con el secreto compartido y comparación en tiempo constante. (d) Hash de contraseñas (Argon2id/bcrypt): nunca necesitas recuperarlas.