{"id":94,"date":"2026-01-11T12:23:45","date_gmt":"2026-01-11T16:23:45","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=94"},"modified":"2026-01-18T09:39:09","modified_gmt":"2026-01-18T13:39:09","slug":"crea-contrasenas-realmente-fuertes-en-php-generador-seguro-con-aleatoriedad-criptografica-y-sin-sesgos","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/01\/crea-contrasenas-realmente-fuertes-en-php-generador-seguro-con-aleatoriedad-criptografica-y-sin-sesgos\/","title":{"rendered":"Crea contrase\u00f1as realmente fuertes en PHP: Generador seguro con aleatoriedad criptogr\u00e1fica y sin sesgos"},"content":{"rendered":"\n<p>En el fondo todos sabemos que \u201c123456\u201d, \u201cpassword\u201d, el nombre del perro o el a\u00f1o de nacimiento no son precisamente contrase\u00f1as seguras\u2026 y sin embargo seguimos us\u00e1ndolas.<\/p>\n\n\n\n<p>El dato que m\u00e1s asusta: seg\u00fan diferentes estudios de los \u00faltimos a\u00f1os, entre el 76% y el 81% de las brechas de seguridad involucran credenciales d\u00e9biles o reutilizadas.<\/p>\n\n\n\n<p>Por eso hoy vamos a construir juntos, paso a paso, un generador de contrase\u00f1as realmente robusto en <a href=\"https:\/\/juredev.com\/blog\/tag\/php\/\" data-type=\"link\" data-id=\"https:\/\/juredev.com\/blog\/tag\/php\/\">PHP<\/a>, utilizando aleatoriedad criptogr\u00e1fica y evitando los sesgos que tienen muchas implementaciones caseras.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\u00bfQu\u00e9 diferencia hay entre \u201caleatorio\u201d y \u201ccriptogr\u00e1ficamente seguro\u201d?<\/h2>\n\n\n\n<p>Cuando usamos <strong><a href=\"https:\/\/www.php.net\/manual\/en\/function.rand.php\">rand()<\/a><\/strong>, <strong><a href=\"https:\/\/www.php.net\/manual\/en\/function.mt-rand.php\">mt_rand()<\/a><\/strong> o incluso el famoso <strong>rand(1000000,9999999)<\/strong> para generar c\u00f3digos o contrase\u00f1as, estamos trabajando con generadores pseudoaleatorios.<\/p>\n\n\n\n<p>Son muy \u00fatiles para juegos, animaciones, simulaciones\u2026 pero no para seguridad.<\/p>\n\n\n\n<p>La raz\u00f3n principal es que si un atacante llega a conocer:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>el algoritmo<\/li>\n\n\n\n<li>la semilla (seed) inicial<\/li>\n\n\n\n<li>o unas pocas salidas consecutivas<\/li>\n<\/ul>\n\n\n\n<p>\u2026 en muchos casos puede reconstruir toda la secuencia futura.<\/p>\n\n\n\n<p>Para contrase\u00f1as (y cualquier secreto criptogr\u00e1fico) necesitamos algo mucho m\u00e1s fuerte: <strong>aleatoriedad criptogr\u00e1ficamente segura<\/strong> (CSPRNG).<\/p>\n\n\n\n<p>En PHP moderno (\u22657.0) la forma recomendada y m\u00e1s sencilla es usar <strong>random_bytes()<\/strong>.<\/p>\n\n\n\n<p>Esta funci\u00f3n aprovecha las fuentes de entrop\u00eda del sistema operativo (interrupciones de hardware, eventos del kernel, etc.), por lo que el resultado es impredecible incluso para alguien que conozca todo el estado interno previo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El c\u00f3digo \u2013 Funci\u00f3n generateSecurePassword()<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\n\/**\n * Genera una contrase\u00f1a fuerte usando aleatoriedad criptogr\u00e1fica sin sesgos\n * \n * @param int    $length        Longitud deseada (m\u00ednimo 8)\n * @param bool   $useNumbers    Incluir n\u00fameros\n * @param bool   $useLowercase  Incluir min\u00fasculas\n * @param bool   $useUppercase  Incluir may\u00fasculas\n * @param bool   $useSymbols    Incluir s\u00edmbolos\n * @param bool   $useSpaces     Incluir espacios (\u00a1cuidado con esta opci\u00f3n!)\n * @param string $customSymbols S\u00edmbolos personalizados (si se env\u00eda, reemplaza los predeterminados)\n * \n * @return string Contrase\u00f1a generada\n *\/\nfunction generateSecurePassword(\n    int $length = 12,\n    bool $useNumbers = true,\n    bool $useLowercase = true,\n    bool $useUppercase = true,\n    bool $useSymbols = true,\n    bool $useSpaces = false,\n    string $customSymbols = ''\n): string {\n    \/\/ Protecci\u00f3n b\u00e1sica contra longitudes peligrosamente cortas\n    $length = max(8, $length);\n\n    \/\/ Conjuntos de caracteres\n    $numbers   = '0123456789';\n    $lowercase = 'abcdefghijklmnopqrstuvwxyz';\n    $uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n    $symbols   = $customSymbols !== '' \n        ? $customSymbols \n        : '!@#$%^&amp;*()-_=+&#91;]{}|;:,.&lt;&gt;?\/~`';\n\n    $spaces = ' ';\n\n    \/\/ Construimos el pool completo seg\u00fan las opciones elegidas\n    $chars = '';\n    if ($useNumbers)    $chars .= $numbers;\n    if ($useLowercase)  $chars .= $lowercase;\n    if ($useUppercase)  $chars .= $uppercase;\n    if ($useSymbols)    $chars .= $symbols;\n    if ($useSpaces)     $chars .= $spaces;\n\n    \/\/ Caso extremo: el usuario desactiv\u00f3 TODO \u2192 fallback razonable\n    if ($chars === '') {\n        $chars = $lowercase . $uppercase . $numbers;\n    }\n\n    $charsLength = strlen($chars);\n\n    \/\/ Generador de \u00edndice aleatorio sin sesgo (rejection sampling)\n    $getUnbiasedIndex = function (int $max) use (&amp;$random_bytes): int {\n        $maxSafe = (floor(255 \/ $max)) * $max;\n\n        do {\n            $byte = random_bytes(1);\n            $value = ord($byte);\n        } while ($value &gt;= $maxSafe);\n\n        return $value % $max;\n    };\n\n    \/\/ Generamos la contrase\u00f1a base\n    $password = '';\n    for ($i = 0; $i &lt; $length; $i++) {\n        $index = $getUnbiasedIndex($charsLength);\n        $password .= $chars&#91;$index];\n    }\n\n    \/\/ Garantizamos al menos un car\u00e1cter de cada tipo solicitado\n    $required = &#91;];\n    if ($useNumbers)    $required&#91;] = $numbers&#91;random_int(0, 9)];\n    if ($useLowercase)  $required&#91;] = $lowercase&#91;random_int(0, 25)];\n    if ($useUppercase)  $required&#91;] = $uppercase&#91;random_int(0, 25)];\n    if ($useSymbols)    $required&#91;] = $symbols&#91;random_int(0, strlen($symbols) - 1)];\n    if ($useSpaces)     $required&#91;] = ' ';\n\n    \/\/ Mezclamos las posiciones donde insertaremos los caracteres obligatorios\n    $positions = range(0, $length - 1);\n    shuffle($positions); \/\/ \u2190 \u00a1shuffle usa Mersenne Twister, pero solo para posiciones!\n\n    foreach ($required as $i =&gt; $char) {\n        if (isset($positions&#91;$i])) {\n            $password&#91;$positions&#91;$i]] = $char;\n        }\n    }\n\n    return $password;\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Puntos clave que hacen diferente (y m\u00e1s seguro) este generador<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Usa <strong>random_bytes()<\/strong> \u2192 fuente criptogr\u00e1ficamente segura<\/li>\n\n\n\n<li><strong>Rejection sampling<\/strong> para evitar sesgo modular (muy importante cuando el pool no es potencia de 2)<\/li>\n\n\n\n<li><strong>Garantiza al menos un car\u00e1cter de cada tipo<\/strong> sin sacrificar la aleatoriedad global<\/li>\n\n\n\n<li><strong>Permite mucha flexibilidad<\/strong> (longitud, conjuntos de caracteres, s\u00edmbolos personalizados)<\/li>\n\n\n\n<li><strong>Fallback <\/strong>razonable si el usuario desactiva todas las opciones<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Ejemplo de uso r\u00e1pido<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>$pass = generateSecurePassword(\n    length: 16,\n    useNumbers: true,\n    useLowercase: true,\n    useUppercase: true,\n    useSymbols: true,\n    customSymbols: '!@#*_$%'\n);\n\necho \"Contrase\u00f1a generada:  \" . $pass;\n\/\/ Ejemplo de salida:  kP$9mW#vL2qR*8nT<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Recomendaciones finales r\u00e1pidas<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Longitudes \u226514\u201316 caracteres son el est\u00e1ndar recomendado en 2025\u20132026 para la mayor\u00eda de aplicaciones<\/li>\n\n\n\n<li>Nunca guardes contrase\u00f1as en texto plano \u2192 usa siempre <strong>password_hash()<\/strong><\/li>\n\n\n\n<li>Considera ofrecer tambi\u00e9n la opci\u00f3n de passphrase (estilo 4\u20136 palabras) en interfaces de usuario<\/li>\n\n\n\n<li>Si vas a generar muchas contrase\u00f1as en batch \u2192 piensa en <strong>random_bytes()<\/strong> en bloque (m\u00e1s eficiente)<\/li>\n<\/ul>\n\n\n\n<p>La seguridad de las contrase\u00f1as no se trata solo de la longitud, sino de la calidad de la aleatoriedad. Con <code><strong>random_bytes()<\/strong><\/code> y zero sesgos, este generador es imbatible para apps cotidianas. Podr\u00edas extenderlo con passphrases o exclusi\u00f3n de caracteres ambiguos (como &#8216;O&#8217; vs &#8216;0&#8217;) en versiones futuras.<\/p>\n\n\n\n<p>Implementa esta funci\u00f3n hoy mismo y eleva la <a href=\"https:\/\/juredev.com\/blog\/tag\/seguridad\/\" data-type=\"link\" data-id=\"https:\/\/juredev.com\/blog\/tag\/seguridad\/\">seguridad<\/a> de tu app. \u00a1Tus usuarios te lo agradecer\u00e1n! \u00bfEst\u00e1s listo para probarla? \u00a1Explora las profundidades de mejorar la generaci\u00f3n de claves en tu aplicaci\u00f3n! Sigamos codificando.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>En el fondo todos sabemos que \u201c123456\u201d, \u201cpassword\u201d, el nombre del perro o el a\u00f1o de nacimiento no son precisamente contrase\u00f1as seguras\u2026 y sin embargo seguimos us\u00e1ndolas. El dato que m\u00e1s asusta: seg\u00fan diferentes estudios de los \u00faltimos a\u00f1os, entre el 76% y el 81% de las brechas de seguridad involucran credenciales d\u00e9biles o reutilizadas. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[15,28],"class_list":["post-94","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-php","tag-seguridad"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/94","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=94"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/94\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=94"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=94"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=94"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}