Ocho cifras para un cifrado de 1977
Quien entra en el registro electrónico de Red.es con su certificado carga, antes de nada, un fichero de 166.701 bytes. Dentro hay un programa completo de cifrado DES, el algoritmo que Estados Unidos aprobó en 1977 y retiró en 2005, y una clave para usarlo que es un número de ocho cifras.
- Dominio
- sede.red.gob.es
- Guardado
- 31 AGO 2026
- Comprobado en vivo
- 15 SEP 2026
- Citas
- 10 en 2 ficheros guardados
- Lectura
- unos 4 min

La página es la de acceso al registro electrónico: un recuadro titulado «Autenticación», un texto que avisa de que hará falta un certificado electrónico o el DNI electrónico, y dos botones. Antes de todo eso, el HTML carga tres ficheros de programa, y el primero viene del propio dominio. El servidor dice que ese fichero no se toca desde el 18 de mayo de 2022. Lo capturamos el 31 de agosto de 2026 y lo volvimos a pedir el 15 de septiembre. Los mismos 166.701 bytes, la misma huella.

- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 22,6 kB
- sha256
- b7ba4731e98c…
| 301 | window. |
| 302 | } |
| 303 | </script> |
| 304 | |
| 305 | |
| 306 | |
| 307 | |
| 308 | |
| 309 | <body> |
| 310 | |
| 311 | <script type="text/javascript" src=" |
| 312 | <script type="text/javascript" src="/ |
| 313 | <script type="text/javascript" src="/ |
| 314 | |
| 315 | <script type="text/javascript" language="javascript"> |
| 316 | /** |
| 317 | * Función que muestra un mensage de error |
| 318 | * |
| 319 | * @param {String} htmlMsg Texto con el mensage |
| 320 | */ |
| 321 | function fjsFirma |
| 322 | var theDiv = document. |
Lo primero que hace el cliente al arrancar es decidir cómo va a hablar con AutoFirma. Tiene tres caminos: una conexión local con el programa, un puerto del propio ordenador, o un servidor intermedio al que la página sube los datos y del que la aplicación los descarga. El tercero es el que se lleva esta pieza. La condición que lo elige está escrita en la primera línea del reparto: si se ha forzado ese modo, o si el sistema es iOS, o si es Android. El teléfono, vamos.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
| 553 | // abortamos la carga en ese caso |
| 554 | if (severeTimeDelay) { |
| 555 | if (console && console.log) { |
| 556 | console.log("Se ha detectado un desfase grave entre la hora del sistema y la del servidor. " + |
| 557 | "Se cancela la carga del cliente de firma"); |
| 558 | } |
| 559 | return; |
| 560 | } |
| 561 | |
| 562 | // Si se fuerza el uso de servidor intermedio o estamos en un dispositivo movil, |
| 563 | // usamos servidor intermedio |
| 564 | if (forceWSMode || Platform.isIOS() || Platform.isAndroid()) { |
| 565 | clienteFirma = new AppAfirma |
| 566 | } |
| 567 | // Si podemos utilizar un WebSocket local y no estamos en Internet Explorer |
| 568 | // (en el que no podemos asegurar el funcionamiento si se encuentra habilitada |
| 569 | // una opcion de red concreta), usamos WebSockets |
| 570 | else if (isWebSocketsSupported() && !Platform. |
| 571 | clienteFirma = new AppAfirma |
| 572 | } |
| 573 | // Si no se esta en una version antigua de Internet Explorer o Safari |
| 574 | else if (!Platform. |
| 575 | clienteFirma = new AppAfirma |
Por ese camino los datos del trámite pasan por un servidor, así que el cliente los cifra antes de soltarlos. Para eso necesita una clave, y la función que la fabrica se llama generateCipherKey. Pide un número aleatorio al navegador, se queda con el resto de dividirlo entre cien millones y lo rellena con ceros a la izquierda hasta ocho dígitos. La clave es un número entre 00000000 y 99999999, escrito como ocho caracteres de texto. Son cien millones de claves posibles, pocas más que matrículas españolas, que son ochenta millones.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
| 3078 | } |
| 3079 | |
| 3080 | return random; |
| 3081 | } |
| 3082 | |
| 3083 | /* Genera un numero aleatorio para utilizar como clave de cifrado. */ |
| 3084 | function generateCipherKey() { |
| 3085 | var random; |
| 3086 | if (typeof window.crypto != "undefined" && typeof window. |
| 3087 | var randomInts = new Uint32Array(1); |
| 3088 | window. |
| 3089 | random = zeroFill(randomInts[0] % 100000000, 8); |
| 3090 | } |
| 3091 | else { |
| 3092 | random = zeroFill(Math.floor(((rnd() * MAX_NUMBER) + 1) % 100000000), 8); |
| 3093 | } |
| 3094 | |
| 3095 | return random; |
| 3096 | } |
| 3097 | |
| 3098 | /* Completa un numero con ceros a la izquierda. */ |
| 3099 | function zeroFill(number, width) { |
| 3100 | width -= number.toString().length; |
Hay un segundo camino, para cuando el navegador no tiene window.crypto, la fuente de números aleatorios que traen todos los navegadores desde 2011. Entonces la clave sale de una función propia que se declara “con una distribución homogénea”: multiplica una semilla por 9301, le suma 49297 y se queda con el resto de dividir entre 233280. Es el generador que circulaba en los manuales de los años ochenta. Para arrancar multiplica los milisegundos del reloj por mil y por otro número aleatorio.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
| 3105 | return number + ""; |
| 3106 | } |
| 3107 | |
| 3108 | /** |
| 3109 | * Genera numeros aleatorios con una distribucion homogenea |
| 3110 | */ |
| 3111 | var seed; |
| 3112 | function rnd () { |
| 3113 | if (seed == undefined) { |
| 3114 | seed = new Date().getMilliseconds() * 1000 * Math.random(); |
| 3115 | } |
| 3116 | seed = (seed * 9301 + 49297) % 233280; |
| 3117 | return seed / 233280; |
| 3118 | } |
| 3119 | |
| 3120 | /** |
| 3121 | * Envia los datos al servidor intermedio y luego invoca a la |
| 3122 | * aplicacion nativa para que los descargue y opere con ellos. |
| 3123 | * @param idSession Identificador de la operacion con el que se espera recuperar el resultado. |
| 3124 | * @param cipherKey Clave de cifrado. Si no se indica, no se cifra. |
| 3125 | * @param storageServletAddress URL del servlet que almacena. |
| 3126 | * @param retrieverServletAddress URL del servlet que recupera. |
| 3127 | * @param op Identificador del tipo de operacion (firma, cofirma, guardado,...). |
Con esa clave se cifra la operación entera. El cliente arma un pequeño XML con los parámetros (el formato de firma, el algoritmo, el documento en base 64) y lo manda al servidor de depósito con un envío cuyo cuerpo empieza por «op=put». Esto ocurre cuando la llamada no cabe en una dirección web, y el listón está en 2.000 caracteres. Cualquier documento de verdad lo pasa. El resultado, el certificado del ciudadano y la firma, vuelve por el mismo sitio y se descifra con la misma clave.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
| 3163 | httpRequest.onerror = function(e) { |
| 3164 | console.log("Error al enviar los datos al servidor intermedio (HTTP Status: " + httpRequest.status + "): " + e.message); |
| 3165 | errorCallback(" |
| 3166 | } |
| 3167 | } |
| 3168 | catch (e) { |
| 3169 | // Vacio |
| 3170 | } |
| 3171 | |
| 3172 | var requestData = |
| 3173 | "op=put&v=1_0&id=" + fileId + "&dat=" + |
| 3174 | cipher(buildXML(op, params), cipherKey); |
| 3175 | |
| 3176 | try { |
| 3177 | httpRequest. |
| 3178 | } |
| 3179 | catch(e) { |
| 3180 | errorCallback(" |
| 3181 | } |
| 3182 | } |
| 3183 | |
| 3184 | /** |
| 3185 | * Invoca un Intent con la operacion seleccionada, la configuraci\u00F3n establecida y las campos del |
La aplicación instalada tiene que conocer esa clave para descifrar lo que la página depositó. Se la pasa la propia dirección con la que se la llama. El cliente construye una dirección que empieza por «afirma://», o por «intent://» si es Chrome en Android, y le añade un parámetro llamado key con las ocho cifras dentro. Esa dirección es la que el navegador abre para despertar a AutoFirma, y ahí va la clave, escrita.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
| 3260 | * @returns URL para la llamada a la app con los datos necesarios para que descargue |
| 3261 | * la configuración de la operación a realizar. |
| 3262 | */ |
| 3263 | function buildUrlWithoutData (op, id, rtServlet, cipherKey) { |
| 3264 | var j = 0; |
| 3265 | var newParams = new Array(); |
| 3266 | newParams[j++] = {key:"fileid", value:id}; |
| 3267 | if (rtServlet != null || rtServlet != undefined) { |
| 3268 | newParams[j++] = {key:"rtservlet", value:rtServlet}; |
| 3269 | } |
| 3270 | if (cipherKey != null || cipherKey != undefined) { |
| 3271 | newParams[j++] = {key:"key", value:cipherKey}; |
| 3272 | } |
| 3273 | return buildUrl(op, newParams); |
| 3274 | }; |
| 3275 | |
| 3276 | /** |
| 3277 | * Ejecuta el metodo de error si el html recuperado es tal o el metodo de exito si no lo es, |
| 3278 | * en cuyo caso previamente descifrara el resultado. Si el valor recuperado no es ni el |
| 3279 | * de exito ni el de error, indicara si se debe seguir esperando por un resultado. En caso |
| 3280 | * de indicar false, no se esperara mas; en caso de true, se seguira con la espera; y si se |
| 3281 | * de vuelve "reset" se debera reiniciar la espera. |
| 3282 | * @param html Resultado obtenido. |
Queda el algoritmo. El comentario que encabeza la función de cifrado lo dice con todas las letras: «Algoritmo DES, modo CBC, sin Padding». Doce líneas más abajo está la llamada de verdad, con la clave, los datos y tres números: un 1, un 0 y un nulo. El 1 quiere decir cifrar. El 0 es el modo. Y del relleno que el comentario niega se encarga el propio código, que cuenta a mano los bytes que faltan para llegar a múltiplo de ocho y antepone esa cuenta al resultado, separada por un punto.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
| 3604 | */ |
| 3605 | function decipher(cipheredData, key, intermediate) { |
| 3606 | |
| 3607 | var dotPos = cipheredData.indexOf('.'); |
| 3608 | var padding = cipheredData.substr(0, dotPos); |
| 3609 | |
| 3610 | var deciphered = Cipher.des(key, Cipher. |
| 3611 | return Cipher. |
| 3612 | } |
| 3613 | |
| 3614 | /** |
| 3615 | * Realiza un cifrado DES compatible con Java (Algoritmo DES, modo CBC, sin Padding). |
| 3616 | * @param dataB64 Cadena de texto base 64. |
| 3617 | * @param key Clave de cifrado. |
| 3618 | * @return Base 64 cifrado. |
| 3619 | */ |
| 3620 | function cipher(dataB64, key) { |
| 3621 | |
| 3622 | var data = Cipher. |
| 3623 | var padding = (8 - (data.length % 8)) % 8; |
| 3624 | |
| 3625 | // Los datos cifrados los pasamos a base 64 y, antes de devolverlos le anteponemos el padding que |
| 3626 | // le habra agregado el metodo de cifrado separados por un punto ('.'). |
| 3616 | * @param dataB64 Cadena de texto base 64. |
| 3617 | * @param key Clave de cifrado. |
| 3618 | * @return Base 64 cifrado. |
| 3619 | */ |
| 3620 | function cipher(dataB64, key) { |
| 3621 | |
| 3622 | var data = Cipher. |
| 3623 | var padding = (8 - (data.length % 8)) % 8; |
| 3624 | |
| 3625 | // Los datos cifrados los pasamos a base 64 y, antes de devolverlos le anteponemos el padding que |
| 3626 | // le habra agregado el metodo de cifrado separados por un punto ('.'). |
| 3627 | return padding + "." + Cipher. |
| 3628 | } |
| 3629 | |
| 3630 | /** |
| 3631 | * Convierte de Base64 URL Save a Base64 normal. |
| 3632 | */ |
| 3633 | function from |
| 3634 | if (!base64UrlSave) { |
| 3635 | return base64UrlSave; |
| 3636 | } |
| 3637 | return base64UrlSave. |
| 3638 | } |
Casi cuatrocientas líneas más abajo, en la librería que hace el trabajo, se ve qué significa ese número. El modo 1 es CBC: cada bloque se mezcla con el anterior antes de cifrarse, y por eso hace falta un vector inicial. El 0 es el otro, ECB: cada bloque de ocho bytes se cifra por su cuenta, sin encadenar nada, de modo que dos bloques iguales del documento dan dos bloques iguales del resultado. El comentario anuncia el primero; la llamada pide el segundo.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
| 3990 | else {looping = encrypt ? new Array (0, 32, 2, 62, 30, -2, 64, 96, 2) : new Array (94, 62, -2, 32, 64, 2, 30, -2, -2);} |
| 3991 | |
| 3992 | //pad the message depending on the padding parameter |
| 3993 | if (padding == 2) message += " "; //pad the message with spaces |
| 3994 | else if (padding == 1) {temp = 8-(len%8); message += String.fromCharCode (temp, |
| 3995 | else if (!padding) message += "\0\0\0\0\0\0\0\0"; //pad the message out with null bytes |
| 3996 | |
| 3997 | //store the result here |
| 3998 | var result = ""; |
| 3999 | var tempresult = ""; |
| 4000 | |
| 4001 | if (mode == 1) { //CBC mode |
| 4002 | cbcleft = (iv.charCodeAt(m++) << 24) | (iv.charCodeAt(m++) << 16) | (iv.charCodeAt(m++) << 8) | iv.charCodeAt(m++); |
| 4003 | cbcright = (iv.charCodeAt(m++) << 24) | (iv.charCodeAt(m++) << 16) | (iv.charCodeAt(m++) << 8) | iv.charCodeAt(m++); |
| 4004 | m=0; |
| 4005 | } |
| 4006 | |
| 4007 | //loop through each 64 bit chunk of the message |
| 4008 | while (m < len) { |
| 4009 | left = (message.charCodeAt(m++) << 24) | (message.charCodeAt(m++) << 16) | (message.charCodeAt(m++) << 8) | message.charCodeAt(m++); |
| 4010 | right = (message.charCodeAt(m++) << 24) | (message.charCodeAt(m++) << 16) | (message.charCodeAt(m++) << 8) | message.charCodeAt(m++); |
| 4011 | |
| 4012 | //for Cipher Block Chaining mode, xor the message with the previous result |
La misma librería sabe hacer Triple DES, que es DES aplicado tres veces y lo que se usó durante años cuando una pasada se quedó corta. Lo decide sola, mirando la longitud de la clave: con nueve caracteres o más, tres pasadas; con ocho o menos, una. La clave que fabrica esta página tiene ocho caracteres exactos. Le falta uno para que la propia librería, sin que nadie se lo pidiera, hubiera hecho el trabajo tres veces. Una pasada, pues.
- Copia
- archivo
- Guardado
- 31 ago 2026
- Peso
- 166,7 kB
- sha256
- e1774d2f3ef4…
- Tapado
- 1 tramos ▒
| 4076 | var pc2bytes5 = new Array (0, |
| 4077 | var pc2bytes6 = new Array (0, |
| 4078 | var pc2bytes7 = new Array (0, |
| 4079 | var pc2bytes8 = new Array (0, |
| 4080 | var pc2bytes9 = new Array (0, |
| 4081 | var pc2bytes10 = new Array (0, |
| 4082 | var pc2bytes11 = new Array (0, |
| 4083 | var pc2bytes12 = new Array (0, |
| 4084 | var pc2bytes13 = new Array (0, |
| 4085 | |
| 4086 | //how many iterations (1 for des, 3 for triple des) |
| 4087 | var iterations = key.length > 8 ? 3 : 1; //changed by ▒▒▒▒ 16/6/2007 to use Triple DES for 9+ byte keys |
| 4088 | //stores the return keys |
| 4089 | var keys = new Array (32 * iterations); |
| 4090 | //now define the left shifts which need to be done |
| 4091 | var shifts = new Array (0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0); |
| 4092 | //other variables |
| 4093 | var lefttemp, righttemp, m=0, n=0, temp; |
| 4094 | |
| 4095 | for (var j=0; j<iterations; j++) { //either 1 or 3 iterations |
| 4096 | var left = (key.charCodeAt(m++) << 24) | (key.charCodeAt(m++) << 16) | (key.charCodeAt(m++) << 8) | key.charCodeAt(m++); |
| 4097 | var right = (key.charCodeAt(m++) << 24) | (key.charCodeAt(m++) << 16) | (key.charCodeAt(m++) << 8) | key.charCodeAt(m++); |
DES es de 1977. Ese año el Gobierno de Estados Unidos lo aprobó como norma federal de cifrado, con clave de 56 bits, y en 2005 lo retiró. La librería que lo implementa aquí lleva en su cabecera la fecha de julio de 2001 y un parche fechado el 16 de junio de 2007. La cabecera entera trae los nombres de sus dos autores y sus páginas personales, y por eso no la reproducimos. Es el código que se pone en marcha cuando alguien intenta firmar un trámite desde el móvil.
El comentario promete CBC. La llamada pide el otro modo. La clave son ocho cifras, y todo esto se pone en marcha cuando alguien saca el móvil para firmar.
- El fichero que sede.red.gob.es servía el 15 de septiembre de 2026 contiene una implementación de DES en JavaScript.
- La clave que genera para el modo de servidor intermedio es un número de ocho cifras, y viaja como parámetro en la dirección afirma:// con la que se llama a la aplicación de firma.
- La llamada de cifrado pasa un 0 donde la librería reconoce el 1 como CBC, y pide una sola pasada porque la clave no llega a nueve caracteres.
- Ese camino se toma cuando el trámite se abre en
iOSo en Android, o cuando la página fuerza el modo de servidor intermedio.
- No decimos que nadie haya leído ni alterado ningún trámite: no lo sabemos y no hemos probado nada contra ese servidor.
- No decimos que una firma se pueda falsificar; la firma la hace la aplicación instalada, con el certificado del ciudadano, fuera del navegador.
- No decimos que esto sea cosa de este organismo: el fichero es el cliente estándar de @firma y aparece igual en otros 42 hosts del archivo.
- No decimos qué versión de ese cliente es la vigente hoy, ni si alguna la ha cambiado.
De dónde sale cada cita
Cada trozo de código de esta ficha está en un fichero guardado tal como lo entregó el servidor, con su huella (sha256) y su hora. Las citas entre «» se comprueban por programa contra ese fichero antes de publicar.
- https://sede.red.gob.es/eRegistro/accesoFirma/autenticacionAppletFirma.action?expedienteSeleccionado=QS-00131 de agosto de 2026, 16:16archivosha256 b7ba4731e98c089e7af1383c3d9fad6048191cf14f3b0310e4673e99468e5055
- https://sede.red.gob.es/afirma/js/autoscript.js31 de agosto de 2026, 16:16archivosha256 e1774d2f3ef4c3e6cae90f038f9120d4e1e8a2ef9c4caffc4448f17c9897bd37
- https://sede.red.gob.es/afirma/js/autoscript.js15 de septiembre de 2026, 01:35en vivo · byte a byte igualsha256 e1774d2f3ef4c3e6cae90f038f9120d4e1e8a2ef9c4caffc4448f17c9897bd37