La criptografía es el arte de proteger información mediante transformaciones matemáticas. Las relaciones discretas—números primos, congruencias, funciones one-way, logaritmo discreto—son el fundamento de la seguridad moderna. Sin estas relaciones matemáticas, no existiría cifrado seguro.
La criptografía protege información transformándola de modo que solo el destinatario autorizado pueda recuperarla. Se basa en relaciones matemáticas unidireccionales: fáciles de calcular en una dirección, imposibles en la otra.
Las preguntas fundamentales de la criptografía son todas relacionales:
Cada respuesta depende de propiedades de relaciones discretas. Este tema explora cómo.
El cifrado de sustitución reemplaza cada carácter por otro mediante una relación (tabla de sustitución). El cifrado simétrico usa la misma clave para cifrar y descifrar.
// Cifrado Caesar (rotación simple)
class CifroCaesar {
constructor(desplazamiento) {
this.desplazamiento = desplazamiento % 26;
}
cifrar(texto) {
return texto.split('').map(char => {
if (char.match(/[a-z]/i)) {
const codigo = char.charCodeAt(0);
const esMinuscula = codigo >= 97;
const base = esMinuscula ? 97 : 65;
const offset = (codigo - base + this.desplazamiento) % 26;
return String.fromCharCode(base + offset);
}
return char;
}).join('');
}
descifrar(texto) {
return new CifroCaesar(-this.desplazamiento).cifrar(texto);
}
}
// Análisis de frecuencia: relación entre frecuencia de letra y lenguaje
class CriptoanálisisFrecuencia {
static frecuenciaLetras(texto) {
const freq = {};
const textoLimpio = texto.toLowerCase().replace(/[^a-z]/g, '');
for (const char of textoLimpio) {
freq[char] = (freq[char] || 0) + 1;
}
// Normalizar
for (const char in freq) {
freq[char] = (freq[char] / textoLimpio.length * 100).toFixed(2);
}
return freq;
}
static indiceCoincidencia(texto) {
const freq = this.frecuenciaLetras(texto);
let ic = 0;
for (const f of Object.values(freq)) {
const p = parseFloat(f) / 100;
ic += p * p;
}
return ic;
}
static ataqueFuerza(criptograma) {
const resultados = [];
for (let shift = 1; shift < 26; shift++) {
const cipher = new CifroCaesar(shift);
const descifrado = cipher.descifrar(criptograma);
const ic = this.indiceCoincidencia(descifrado);
resultados.push({ shift, descifrado: descifrado.substring(0, 50), ic });
}
// Ordenar por IC más cercano al inglés (~0.065)
resultados.sort((a, b) => Math.abs(a.ic - 0.065) - Math.abs(b.ic - 0.065));
return resultados[0];
}
}
// Cifrado de Vernam (One-time pad) - Teóricamente seguro
class CifroVernam {
constructor() {
this.claveAleatoria = null;
}
generarClave(longitud) {
this.claveAleatoria = Array.from({length: longitud}, () =>
Math.floor(Math.random() * 256)
);
return this.claveAleatoria;
}
cifrar(texto) {
if (!this.claveAleatoria || this.claveAleatoria.length < texto.length) {
throw new Error("Clave no generada o muy corta");
}
return texto.split('').map((char, i) => {
const xor = char.charCodeAt(0) ^ this.claveAleatoria[i];
return String.fromCharCode(xor);
}).join('');
}
descifrar(criptograma) {
// XOR es su propio inverso
return this.cifrar(criptograma);
}
}
// Uso
const caesar = new CifroCaesar(3);
const mensaje = "el cifrado de caesar es inseguro";
const criptograma = caesar.cifrar(mensaje);
console.log("Original:", mensaje);
console.log("Cifrado:", criptograma);
console.log("Descifrado:", caesar.descifrar(criptograma));
console.log("\nAtaque de frecuencia:", CriptoanálisisFrecuencia.ataqueFuerza(criptograma));
La aritmética modular (congruencias) es fundamental para criptografía. Dos números son congruentes módulo n si tienen el mismo residuo al dividirse por n. Los números primos juegan un papel especial.
// Aritmética modular
class Aritmética {
// Máximo Común Divisor (Euclides)
static mcd(a, b) {
while (b !== 0) {
[a, b] = [b, a % b];
}
return a;
}
// Inverso modular (Extended Euclidean Algorithm)
static inversoModular(a, m) {
let [old_r, r] = [a, m];
let [old_s, s] = [1, 0];
while (r !== 0) {
const quotient = Math.floor(old_r / r);
[old_r, r] = [r, old_r - quotient * r];
[old_s, s] = [s, old_s - quotient * s];
}
return old_s < 0 ? old_s + m : old_s;
}
// Exponenciación modular rápida
static powMod(base, exp, mod) {
let resultado = 1;
base = base % mod;
while (exp > 0) {
if (exp % 2 === 1) {
resultado = (resultado * base) % mod;
}
exp = Math.floor(exp / 2);
base = (base * base) % mod;
}
return resultado;
}
// Test de primalidad (Miller-Rabin simplificado)
static esPrimo(n, iteraciones = 5) {
if (n < 2) return false;
if (n === 2 || n === 3) return true;
if (n % 2 === 0) return false;
// Escribir n-1 como d * 2^r
let d = n - 1;
let r = 0;
while (d % 2 === 0) {
d = Math.floor(d / 2);
r++;
}
// Pruebas aleatorias
for (let i = 0; i < iteraciones; i++) {
const a = 2 + Math.floor(Math.random() * (n - 3));
let x = this.powMod(a, d, n);
if (x === 1 || x === n - 1) continue;
let probablePrimo = false;
for (let j = 0; j < r - 1; j++) {
x = (x * x) % n;
if (x === n - 1) {
probablePrimo = true;
break;
}
}
if (!probablePrimo) return false;
}
return true;
}
// Generar número primo aleatorio
static generarPrimoAleatorio(min, max) {
let num = min + Math.floor(Math.random() * (max - min));
while (!this.esPrimo(num)) {
num++;
}
return num;
}
}
// Uso
console.log("¿6700417 es primo?", Aritmética.esPrimo(6700417)); // true
console.log("Inverso de 7 mod 26:", Aritmética.inversoModular(7, 26)); // 15
console.log("2^100 mod 1000000007:", Aritmética.powMod(2, 100, 1000000007));
RSA es un cifrado asimétrico que usa un par de claves: pública (conocida) y privada (secreta). La seguridad depende de que factorizar números grandes es computacionalmente intratable, pero calcular potencias modulares es rápido.
// Cifrado RSA simplificado
class Aritmética {
static inversoModular(a, m) {
let [oldR, r] = [a, m], [oldS, s] = [1, 0];
while (r !== 0) {
const cociente = Math.floor(oldR / r);
[oldR, r] = [r, oldR - cociente * r];
[oldS, s] = [s, oldS - cociente * s];
}
return oldS < 0 ? oldS + m : oldS;
}
static powMod(base, exponente, modulo) {
let resultado = 1;
base %= modulo;
while (exponente > 0) {
if (exponente % 2 === 1) resultado = (resultado * base) % modulo;
exponente = Math.floor(exponente / 2);
base = (base * base) % modulo;
}
return resultado;
}
}
class RSA {
constructor(p, q) {
this.p = p;
this.q = q;
this.n = p * q;
this.phi = (p - 1) * (q - 1); // φ(n)
// Seleccionar e (debe ser coprimo con φ(n))
this.e = 65537; // Valor estándar
// Calcular d tal que e·d ≡ 1 (mod φ(n))
this.d = Aritmética.inversoModular(this.e, this.phi);
}
clavePublica() {
return { e: this.e, n: this.n };
}
clavePrivada() {
return { d: this.d, n: this.n };
}
static cifrar(mensaje, clavePublica) {
// C = M^e mod n
return Aritmética.powMod(mensaje, clavePublica.e, clavePublica.n);
}
static descifrar(criptograma, clavePrivada) {
// M = C^d mod n
return Aritmética.powMod(criptograma, clavePrivada.d, clavePrivada.n);
}
static cifrarTexto(texto, clavePublica) {
return texto.split('').map(char => {
const charCode = char.charCodeAt(0);
return this.cifrar(charCode, clavePublica);
});
}
static descifrarTexto(criptogramaArray, clavePrivada) {
return criptogramaArray.map(cripto => {
const charCode = this.descifrar(cripto, clavePrivada);
return String.fromCharCode(charCode);
}).join('');
}
}
// Uso
const p = 61, q = 53; // Números primos pequeños (en producción, mucho más grandes)
const rsa = new RSA(p, q);
const pubKey = rsa.clavePublica();
const privKey = rsa.clavePrivada();
console.log("Clave pública (n, e):", pubKey);
console.log("Clave privada (n, d):", privKey);
// Cifrar y descifrar un número
const mensaje = 42;
const cifrado = RSA.cifrar(mensaje, pubKey);
const descifrado = RSA.descifrar(cifrado, privKey);
console.log(`Mensaje: ${mensaje}`);
console.log(`Cifrado: ${cifrado}`);
console.log(`Descifrado: ${descifrado}`);
Una función hash criptográfica es una relación de muchos-a-uno que es computacionalmente imposible de invertir (one-way function). Se usa para autenticación e integridad de datos.
// Hash criptográfico simple (NO SEGURO - solo educativo)
class HashSimple {
// Función hash basada en polinomio módulo primo
static hash(datos) {
const primo = 1000000007;
let resultado = 0;
let base = 1;
for (let i = 0; i < datos.length; i++) {
const codigo = datos.charCodeAt(i);
resultado = (resultado + codigo * base) % primo;
base = (base * 31) % primo;
}
return resultado;
}
static hashHexadecimal(datos) {
return HashSimple.hash(datos).toString(16);
}
}
// Propiedades de una buena función hash:
// 1. Determinismo: mismo entrada → mismo hash
// 2. Rápido: calcula hash en tiempo razonable
// 3. One-way: imposible encontrar entrada dado hash
// 4. Avalancha: pequeño cambio en entrada → completamente diferente hash
class PropiedadesHash {
static verificarDeterminismo(hashFunc, datos) {
const h1 = hashFunc(datos);
const h2 = hashFunc(datos);
return h1 === h2;
}
static verificarAvalancha(hashFunc, datos) {
const h1 = hashFunc(datos);
// Cambiar un carácter
const datosModificados = datos.slice(0, -1) +
String.fromCharCode(datos.charCodeAt(datos.length - 1) + 1);
const h2 = hashFunc(datosModificados);
// Contar bits diferentes (Hamming distance)
const xor = parseInt(h1, 16) ^ parseInt(h2, 16);
let bitsDiferentes = 0;
for (let i = 0; i < 32; i++) {
if ((xor >> i) & 1) bitsDiferentes++;
}
return bitsDiferentes > 15; // Esperamos ~50% de bits diferentes
}
static verificarResistenciaPreimagen(hashFunc, objetivo, spacioInicialSize = 10000) {
// Intento naive: buscar entrada que genere el hash
for (let i = 0; i < spacioInicialSize; i++) {
const candidato = "entrada" + i;
if (hashFunc(candidato) === objetivo) {
return { encontrado: true, candidato };
}
}
return { encontrado: false };
}
}
// Uso
const datos = "Hello World";
const hash1 = HashSimple.hashHexadecimal(datos);
console.log("Hash de 'Hello World':", hash1);
console.log("¿Determinismo?", PropiedadesHash.verificarDeterminismo(HashSimple.hashHexadecimal, datos));
console.log("¿Efecto avalancha?", PropiedadesHash.verificarAvalancha(HashSimple.hashHexadecimal, datos));
// Detección de modificaciones
class DetectorIntegridad {
constructor() {
this.archivo = "";
this.hash = null;
}
guardar(contenido) {
this.archivo = contenido;
this.hash = HashSimple.hashHexadecimal(contenido);
}
verificar(contenido) {
const hashActual = HashSimple.hashHexadecimal(contenido);
return hashActual === this.hash;
}
hashGuardado() {
return this.hash;
}
}
const detector = new DetectorIntegridad();
detector.guardar("Información importante");
console.log("\n¿Archivo sin modificar?", detector.verificar("Información importante")); // true
console.log("¿Archivo modificado?", detector.verificar("Información editada")); // false
Una firma digital demuestra que el mensaje fue creado por el remitente y no fue modificado. Usa la clave privada para firmar y la pública para verificar.
// Firma digital con RSA
class HashSimple {
static hash(datos) {
let resultado = 0, base = 1;
for (const caracter of datos) {
resultado = (resultado + caracter.charCodeAt(0) * base) % 1000000007;
base = (base * 31) % 1000000007;
}
return resultado;
}
}
class Aritmética {
static inversoModular(a, m) {
let [oldR, r] = [a, m], [oldS, s] = [1, 0];
while (r !== 0) {
const cociente = Math.floor(oldR / r);
[oldR, r] = [r, oldR - cociente * r];
[oldS, s] = [s, oldS - cociente * s];
}
return oldS < 0 ? oldS + m : oldS;
}
static powMod(base, exponente, modulo) {
let resultado = 1;
base %= modulo;
while (exponente > 0) {
if (exponente % 2 === 1) resultado = (resultado * base) % modulo;
exponente = Math.floor(exponente / 2);
base = (base * base) % modulo;
}
return resultado;
}
}
class RSA {
constructor(p, q) {
this.n = p * q;
this.phi = (p - 1) * (q - 1);
this.e = 65537;
this.d = Aritmética.inversoModular(this.e, this.phi);
}
clavePublica() { return { e: this.e, n: this.n }; }
clavePrivada() { return { d: this.d, n: this.n }; }
static cifrar(mensaje, clave) { return Aritmética.powMod(mensaje, clave.e, clave.n); }
static descifrar(cifrado, clave) { return Aritmética.powMod(cifrado, clave.d, clave.n); }
}
class FirmaDigital {
static firmar(mensaje, clavePrivada) {
// Calcular hash del mensaje
const hash = HashSimple.hash(mensaje) % clavePrivada.n;
// Firmar el hash con clave privada: S = hash^d mod n
return RSA.descifrar(hash, clavePrivada);
}
static verificar(mensaje, firma, clavePublica) {
// Calcular hash del mensaje
const hash = HashSimple.hash(mensaje) % clavePublica.n;
// Verificar firma: recuperar hash con clave pública: hash' = firma^e mod n
const hashRecuperado = RSA.cifrar(firma, clavePublica);
return hash === hashRecuperado;
}
}
// Uso
const p = 61, q = 53;
const rsa = new RSA(p, q);
const pubKey = rsa.clavePublica();
const privKey = rsa.clavePrivada();
const mensaje = "Transferencia de 1000 euros a Juan";
const firma = FirmaDigital.firmar(mensaje, privKey);
console.log("Mensaje:", mensaje);
console.log("Firma:", firma);
console.log("¿Firma válida?", FirmaDigital.verificar(mensaje, firma, pubKey)); // true
// Intento de modificación
const mensajeModificado = "Transferencia de 1000000 euros a Juan";
console.log("¿Firma válida para mensaje modificado?", FirmaDigital.verificar(mensajeModificado, firma, pubKey)); // false
// Certificado digital
class Certificado {
constructor(propietario, clavePublica, autoridad) {
this.propietario = propietario;
this.clavePublica = clavePublica;
this.autoridad = autoridad;
this.fechaExpiracion = new Date();
this.fechaExpiracion.setFullYear(this.fechaExpiracion.getFullYear() + 1);
this.firmaAutoridad = null;
}
firmarPorAutoridad(clavePrivadaAutoridad) {
const contenido = JSON.stringify({
propietario: this.propietario,
clavePublica: this.clavePublica,
fechaExpiracion: this.fechaExpiracion
});
this.firmaAutoridad = FirmaDigital.firmar(contenido, clavePrivadaAutoridad);
}
esValido(clavePublicaAutoridad) {
const contenido = JSON.stringify({
propietario: this.propietario,
clavePublica: this.clavePublica,
fechaExpiracion: this.fechaExpiracion
});
const hoyEsAntes = new Date() < this.fechaExpiracion;
const firmaEsValida = FirmaDigital.verificar(contenido, this.firmaAutoridad, clavePublicaAutoridad);
return hoyEsAntes && firmaEsValida;
}
}
// Uso
const autoridad = new RSA(p, q);
const cert = new Certificado("Juan Pérez", pubKey, "CA España");
cert.firmarPorAutoridad(autoridad.clavePrivada());
console.log("\n¿Certificado válido?", cert.esValido(autoridad.clavePublica()));
El problema del intercambio de claves es: ¿cómo dos partes establecen una clave secreta compartida en un canal público? Diffie-Hellman lo resuelve explotando el problema del logaritmo discreto.
// Protocolo Diffie-Hellman
class Aritmética {
static powMod(base, exponente, modulo) {
let resultado = 1;
base %= modulo;
while (exponente > 0) {
if (exponente % 2 === 1) resultado = (resultado * base) % modulo;
exponente = Math.floor(exponente / 2);
base = (base * base) % modulo;
}
return resultado;
}
}
class DiffieHellman {
// p: primo grande, g: generador
static parametrosPublicos(p = 23, g = 5) {
return { p, g };
}
static generarClavePrivada(p) {
return 2 + Math.floor(Math.random() * (p - 3));
}
static calcularClavePublica(p, g, clavePrivada) {
// Clave pública = g^privada mod p
return Aritmética.powMod(g, clavePrivada, p);
}
static derivarSecretoCompartido(p, clavePublicaOtra, clavePrivadaPropia) {
// Secreto = clavePublicaOtra^clavePrivadaPropia mod p
return Aritmética.powMod(clavePublicaOtra, clavePrivadaPropia, p);
}
}
// Simulación de protocolo DH
console.log("=== Protocolo Diffie-Hellman ===");
const { p, g } = DiffieHellman.parametrosPublicos();
console.log(`Parámetros públicos: p=${p}, g=${g}`);
// Alicia genera su clave privada
const alicePrivada = DiffieHellman.generarClavePrivada(p);
const alicePublica = DiffieHellman.calcularClavePublica(p, g, alicePrivada);
console.log(`Alicia: privada=${alicePrivada}, pública=${alicePublica}`);
// Bob genera su clave privada
const bobPrivada = DiffieHellman.generarClavePrivada(p);
const bobPublica = DiffieHellman.calcularClavePublica(p, g, bobPrivada);
console.log(`Bob: privada=${bobPrivada}, pública=${bobPublica}`);
// Intercambian claves públicas (por canal inseguro)
// Alicia calcula secreto compartido
const secretoAlicia = DiffieHellman.derivarSecretoCompartido(p, bobPublica, alicePrivada);
console.log(`Secreto calculado por Alicia: ${secretoAlicia}`);
// Bob calcula secreto compartido
const secretoBob = DiffieHellman.derivarSecretoCompartido(p, alicePublica, bobPrivada);
console.log(`Secreto calculado por Bob: ${secretoBob}`);
console.log(`¿Secretos iguales? ${secretoAlicia === secretoBob}`);
La criptografía moderna es la aplicación más crítica de las relaciones discretas. Sin ella, no habría seguridad en internet, no habría transacciones bancarias, no habría privacidad digital.
Cada algoritmo criptográfico que hemos explorado explota una relación matemática específica:
La seguridad de un sistema no es mejor que la de su relación matemática subyacente. Cuando entiendes que RSA depende de la dificultad de factorizar, que Diffie-Hellman depende del logaritmo discreto, que hashing depende de funciones one-way, comprendes por qué la criptografía funciona.
El legado de este curso es que las relaciones discretas no son abstracciones lejanas: son el fundamento de la seguridad digital moderna. Dominarlas significa comprender cómo se protege la información en el mundo real.
Relaciones discretas = poder de proteger información. Es tan simple y tan profundo como eso.