tutoriales·9 min read

Generación de Contraseñas Seguras: Más Allá de los Símbolos Raros

Descubre por qué la longitud vence a la complejidad, qué es la entropía real, y cómo crear políticas de contraseñas que los usuarios no odien.

DevToolsHub Team
·
Generación de Contraseñas Seguras: Más Allá de los Símbolos Raros

La paradoja de la contraseña: segura vs memorable

Hace unos años trabajé en un proyecto donde el equipo de seguridad exigía contraseñas con:

  • Mínimo 12 caracteres
  • Al menos una mayúscula, una minúscula, un número y un símbolo
  • Cambio obligatorio cada 30 días
  • No podía repetir las últimas 12 contraseñas

El resultado predecible: los usuarios escribían sus contraseñas en post-its pegados a la pantalla. La política "segura" había creado una vulnerabilidad peor que la que intentaba prevenir.

Esto ilustra un problema fundamental en la seguridad de contraseñas: hay una tensión constante entre lo que es técnicamente seguro y lo que los humanos pueden manejar. El objetivo de este tutorial es ayudarte a navegar esa tensión correctamente.


Entropía: el verdadero lenguaje de la seguridad de contraseñas

Cuando hablamos de contraseñas seguras, lo que realmente importa es la entropía: una medida de la cantidad de incertidumbre o aleatoriedad. Se mide en bits.

¿Qué significan realmente los bits de entropía?

Imagina que estás adivinando una contraseña probando todas las combinaciones posibles (ataque de fuerza bruta). La entropía te dice cuántos intentos necesitarías en promedio:

Entropía Intentos promedio necesarios Tiempo de crack (GPU moderna)
28 bits 268 millones Segundos
40 bits 1 billón Minutos
60 bits 1 trillón Días
80 bits 1 cuatrillón Años
100 bits 1 quintillón Siglos
128 bits 1 septillón Milenios (prácticamente imposible)

La regla de oro: 80 bits es el mínimo aceptable para contraseñas importantes hoy en día. Con menos, estás apostando contra la capacidad computacional creciente.


La gran revelación: longitud vence a complejidad

Aquí está el insight más importante que puedo darte sobre contraseñas:

Una contraseña larga y simple es más segura que una corta y compleja.

Veamos los números:

Contraseña: Tr0ub4dour&3 (12 caracteres, compleja)
Entropía: ~80 bits (asumiendo aleatoriedad total)
Problema: Difícil de recordar, usuarios la escriben en post-its

Contraseña: correct-horse-battery-staple (28 caracteres, simple)
Entropía: ~100 bits (4 palabras de una lista de 2000)
Ventaja: Fácil de recordar, imposible de romper

La passphrase de palabras comunes tiene más entropía Y es más usable. ¿Por qué seguimos exigiendo símbolos raros?

La matemática detrás del poder de las passphrases

Si usas 4 palabras elegidas aleatoriamente de una lista de 2000 palabras comunes:

  • Posibilidades: 2000⁴ = 16 trillones
  • Entropía: log₂(2000⁴) ≈ 44 bits por palabra × 4 = 176 bits

Incluso si el atacante conoce tu método (usar 4 palabras de esa lista específica), sigue necesitando probar 16 trillones de combinaciones.

Con 6 palabras de una lista de 7776 (como el método Diceware):

  • Entropía: 6 × log₂(7776) ≈ 6 × 12.9 = 77.4 bits
  • Aún memorable, pero casi tan segura como una clave AES

Generando aleatoriedad real (no pseudoaleatoriedad)

El error más grave que puedes cometer al generar contraseñas es usar Math.random():

// ❌ NUNCA HAGAS ESTO EN PRODUCCIÓN
function generatePasswordInseguro(length) {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  let password = '';
  for (let i = 0; i < length; i++) {
    password += chars[Math.floor(Math.random() * chars.length)];
  }
  return password;
}

Math.random() no es criptográficamente seguro. Es un generador de números pseudoaleatorios determinista. Si alguien conoce el estado interno (o puede adivinarlo a partir del timestamp), puede predecir todas tus "aleatorias" contraseñas.

La forma correcta: Web Crypto API

function generatePasswordSeguro(length = 16, options = {}) {
  const {
    lowercase = true,
    uppercase = true,
    numbers = true,
    symbols = false  // Desactivado por defecto, veremos por qué
  } = options;

  // Construir el conjunto de caracteres disponibles
  let charset = '';
  if (lowercase) charset += 'abcdefghijklmnopqrstuvwxyz';
  if (uppercase) charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  if (numbers) charset += '0123456789';
  if (symbols) charset += '!@#$%^&*';

  // Generar bytes aleatorios criptográficamente seguros
  const randomValues = new Uint32Array(length);
  crypto.getRandomValues(randomValues);

  // Convertir a caracteres usando módulo (uniformidad casi perfecta)
  let password = '';
  for (let i = 0; i < length; i++) {
    password += charset[randomValues[i] % charset.length];
  }

  return password;
}

// Uso recomendado: 20+ caracteres, sin símbolos obligatorios
const password = generatePasswordSeguro(20, { symbols: false });
console.log(password); // "kQ7mP2vL9xR4nQ8wT3jB"

La diferencia clave es crypto.getRandomValues(), que obtiene entropía del sistema operativo (/dev/urandom en Linux, CryptGenRandom en Windows), que a su vez recoge ruido térmico del hardware y otros eventos impredecibles.

Puedes probar esta generación en nuestra herramienta Password Generator, que implementa exactamente este algoritmo.


Implementando políticas de contraseñas que no odien los usuarios

El problema con las políticas tradicionales

Las políticas clásicas de "mínimo 8 caracteres, mayúscula, minúscula, número y símbolo" surgen de NIST SP 800-63 de 2004. Pero en 2017, NIST publicó nuevas directrices que contradicen muchas de esas reglas:

NIST SP 800-63B (2017) recomienda:

  1. ✅ Mínimo 8 caracteres (subiendo a 15 es mejor)
  2. ❌ NO exigir composición de caracteres (símbolos, etc.)
  3. ❌ NO forzar cambios periódicos arbitrarios
  4. ✅ Verificar contra listas de contraseñas comprometidas
  5. ✅ Permitir todos los caracteres Unicode (emojis incluidos)

Implementación moderna en JavaScript

// Validador que sigue principios modernos
function validarPassword(password) {
  const errores = [];

  // 1. Longitud mínima (el único requisito obligatorio)
  if (password.length < 12) {
    errores.push('Mínimo 12 caracteres (15+ recomendado)');
  }

  // 2. Verificar contra lista de contraseñas comunes
  const comunes = ['password', '123456', 'qwerty', 'admin', 'letmein'];
  if (comunes.includes(password.toLowerCase())) {
    errores.push('Esta contraseña es demasiado común');
  }

  // 3. Verificar patrones obvios
  if (/^(.)\1+$/.test(password)) {
    errores.push('No uses el mismo carácter repetido');
  }

  if (/^(abc|123|qwe|asd)/i.test(password)) {
    errores.push('Evita secuencias de teclado obvias');
  }

  // 4. Opcional: verificar entropía mínima
  const entropia = calcularEntropia(password);
  if (entropia < 50) {
    errores.push('La contraseña es demasiado predecible');
  }

  return {
    valida: errores.length === 0,
    errores,
    entropia: Math.round(entropia),
    recomendacion: entropia < 60 
      ? 'Considera una frase más larga' 
      : 'Buena contraseña'
  };
}

// Cálculo aproximado de entropía
function calcularEntropia(password) {
  // Detectar el espacio de caracteres usado
  let espacio = 0;
  if (/[a-z]/.test(password)) espacio += 26;
  if (/[A-Z]/.test(password)) espacio += 26;
  if (/d/.test(password)) espacio += 10;
  if (/[^a-zA-Z0-9]/.test(password)) espacio += 32;
  
  // Si parece passphrase (espacios o palabras separadas por guiones)
  if (password.includes(' ') || password.split('-').length > 2) {
    // Estimación diferente para passphrases
    const palabras = password.split(/[s-]+/).length;
    return palabras * 12; // ~12 bits por palabra del lenguaje
  }
  
  // Entropía estándar: log2(espacio^longitud)
  return password.length * Math.log2(espacio || 26);
}

Passphrases: la solución de usabilidad + seguridad

Una passphrase (frase de contraseña) es simplemente una contraseña compuesta por múltiples palabras. La famosa viñeta de XKCD "Password Strength" lo resumió perfectamente: "correct horse battery staple" es más segura y fácil de recordar que "Tr0ub4dour&3".

Generando passphrases en JavaScript

// Lista de palabras comunes (en producción, usa una lista más grande)
const WORD_LIST = [
  'apple', 'house', 'river', 'mountain', 'guitar', 'coffee',
  'sunset', 'garden', 'window', 'puzzle', 'dragon', 'castle',
  'purple', 'silver', 'orange', 'velvet', 'thunder', 'whisper',
  'journey', 'horizon', 'crystal', 'shadow', 'cascade', 'breeze'
];

function generatePassphrase(wordCount = 4, separator = '-') {
  const words = [];
  
  for (let i = 0; i < wordCount; i++) {
    // Generar índice aleatorio seguro
    const randomValues = new Uint32Array(1);
    crypto.getRandomValues(randomValues);
    const index = randomValues[0] % WORD_LIST.length;
    
    words.push(WORD_LIST[index]);
  }
  
  return words.join(separator);
}

// Generar passphrase
const passphrase = generatePassphrase(4);
console.log(passphrase); // "river-castle-purple-thunder"
// Entropía: 4 × log₂(24) ≈ 18.4 bits... espera, eso es muy poco

Problema: Con solo 24 palabras, la entropía es demasiado baja. Necesitamos una lista más grande:

// Lista EFF (Electronic Frontier Foundation) - 7776 palabras
// Genera ~12.9 bits de entropía por palabra
const EFF_WORDLIST = [
  'abacus', 'abdomen', 'ability', 'able', 'aboard', ... // 7776 palabras
];

function generateSecurePassphrase(wordCount = 6) {
  const words = [];
  
  for (let i = 0; i < wordCount; i++) {
    const randomValues = new Uint32Array(1);
    crypto.getRandomValues(randomValues);
    const index = randomValues[0] % EFF_WORDLIST.length;
    words.push(EFF_WORDLIST[index]);
  }
  
  return words.join(' ');
}

// 6 palabras de EFF = 6 × 12.9 = 77.4 bits de entropía
// "abacus ability able aboard about above"
// Memorable y extremadamente segura

Errores que he visto en sistemas reales

1. Almacenar contraseñas en texto plano (sí, aún pasa)

// ❌ CRIMINAL - Nunca hagas esto
const user = {
  email: 'usuario@ejemplo.com',
  password: 'SuperSecret123!'  // ¡Texto plano!
};

Usa siempre funciones de hash con salt como PBKDF2, bcrypt o Argon2. Más detalles en nuestro tutorial de SHA-256 con PBKDF2.

2. Limitar la longitud máxima de contraseñas

Algunos sistemas limitan contraseñas a 16 o 20 caracteres. Esto es contraproducente: desincentiva las passphrases largas y seguras. Si necesitas un límite por razones técnicas, hazlo muy alto (128+ caracteres).

3. Mostrar requisitos complejos en el campo de password

Contraseña debe contener:
☐ 8-16 caracteres
☐ Al menos una mayúscula
☐ Al menos una minúscula
☐ Al menos un número
☐ Al menos un símbolo (!@#$%^&*)
☐ No puede contener tu nombre de usuario
☐ No puede ser igual a las últimas 12 contraseñas
☐ ...

Los usuarios ven esto y abandonan el registro. O peor, escriben la contraseña en un documento de texto para "cumplir todos los requisitos".

Mejor approach:

  • Solo requisito explícito: "Mínimo 12 caracteres"
  • Barra de fuerza visual que mejora con la longitud
  • Mensaje: "Frases largas son mejor que símbolos raros"

Conclusión: volver a lo básico

La seguridad de contraseñas es sorprendentemente simple si ignoramos el ruido:

  1. Longitud es lo único que importa realmente: 15+ caracteres para contraseñas, 6+ palabras para passphrases
  2. Aleatoriedad real: Siempre usa CSPRNG, nunca Math.random()
  3. Usabilidad importa: Las políticas que los usuarios evadan son peores que ninguna política
  4. Verificar compromisos: Usa APIs como Have I Been Pwned para rechazar contraseñas filtradas
  5. Sin rotación forzada: Cambiar contraseñas cada X meses crea contraseñas predecibles

Genera contraseñas seguras instantáneamente con nuestra herramienta Password Generator.

Recuerda: el objetivo no es crear la contraseña "perfecta", sino una que sea lo suficientemente segura para que un atacante busque objetivos más fáciles. En seguridad, ser "demasiado difícil de romper" es tan bueno como "imposible de romper".

#password#seguridad#entropía#políticas#javascript

Related articles