47. Seguridad y criptografía

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.

47.1 Introducción

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:

  • ¿Existe una relación invertible entre mensaje y criptograma? (Cifrado simétrico)
  • ¿Existe una relación de uno a uno entre clave pública y privada? (Cifrado asimétrico)
  • ¿Existe una función cuyo inverso es computacionalmente intratable? (One-way functions)
  • ¿Cómo verificar que el mensaje no fue modificado? (Autenticación)

Cada respuesta depende de propiedades de relaciones discretas. Este tema explora cómo.

47.2 Cifrado de Sustitución y Cifrado Simétrico

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.

Principio: La relación debe ser invertible (biyección). Si f es la función de cifrado, existe f⁻¹ tal que f⁻¹(f(m)) = m para todo mensaje m.
// 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));

47.3 Aritmética Modular y Números Primos

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));

47.4 Cifrado RSA (Asimétrico)

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.

Relación fundamental: e · d ≡ 1 (mod φ(n)), donde n = p·q (dos primos grandes). Calcular e y d es fácil; recuperar p y q de n es imposible.
// 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}`);

47.5 Funciones Hash Criptográficas

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

47.6 Firmas Digitales

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()));

47.7 Intercambio de Claves y Diffie-Hellman

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}`);

47.8 Resumen: Criptografía y Relaciones Discretas

Conceptos Fundamentales
  • Cifrado simétrico: Relación invertible (biyección) entre mensaje y criptograma
  • Números primos y factorización: Fácil multiplicar, imposible factorizar (RSA)
  • Aritmética modular: Congruencias y exponenciación rápida
  • Funciones one-way: Fácil calcular f(x), imposible invertir
  • Hashing: Muchos-a-uno, no invertible, efecto avalancha
  • Firmas digitales: Autenticación e integridad con claves asimétricas
  • Logaritmo discreto: Base del intercambio de claves (Diffie-Hellman)

Qué debes recordar de este tema

Puntos Clave
  • La criptografía explota asimetrías computacionales en relaciones matemáticas
  • RSA se basa en la dificultad de factorizar números grandes
  • Las funciones hash son one-way: imposible revertir, pero deterministas
  • Diffie-Hellman usa el problema del logaritmo discreto para intercambio de claves
  • Las firmas digitales combinan hash + cifrado asimétrico para autenticación
  • La seguridad criptográfica descansa en relaciones discretas matemáticamente probadas
  • No implementes tu propio cifrado: usa librerías probadas (TweetNaCl, libsodium)

Conclusión

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:

  • Cifrados simétricos explotan biyecciones que son fáciles de calcular pero imposibles de invertir sin la clave
  • RSA explota que multiplicar primos es fácil pero factorizar es imposible
  • Hashing explota funciones one-way basadas en congruencias
  • Diffie-Hellman explota que el logaritmo discreto es imposible de calcular
  • Firmas digitales combinan estas relaciones para autenticación

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.