Confidex
Secciones

¿Cómo puede un servidor almacenar y sincronizar nuestros secretos sin tener las claves para leerlos? Seguiremos cómo Confidex crea sus claves, las protege en un candado y las usa para cifrar datos y verificar cambios.

Cerradura

Solo el Elegido puede abrir la puerta

Matrix Reloaded

Imagine que estamos construyendo un vault virtual para almacenar datos secretos. Debe guardar esos datos en un portátil después de que se cierre la aplicación y dejarnos abrirla en otro dispositivo. La bóveda necesitará sus propias claves. Debemos almacenarlos de modo que copiar los archivos no le dé a un extraño la posibilidad de leer o alterar su contenido.

Un lock protege las claves: un pequeño archivo cifrado. Para abrirlo se necesita una contraseña maestra y un código secreto, secret_key; ninguno de los dos está en el archivo mismo. Podríamos publicar el candado completo en el periódico de la mañana. Los lectores obtendrían una copia, pero publicarla no revelaría las claves privadas.

Supongamos que un extraño obtiene este archivo e incluso aprende la contraseña maestra. Ahora pueden repetir cada cálculo que solo necesite la contraseña, pero esos cálculos no producirán la llave que abre la cerradura: secret_key también es una entrada. El archivo no contiene ninguna verificación separada de la contraseña únicamente. Abrirlo todavía requiere el código secreto desconocido.

Ese código proviene de datos criptográficamente aleatorios; una persona no lo inventa. Mientras se desconozca secret_key, ni siquiera una contraseña maestra adivinada correctamente abre el candado: el atacante todavía carece de un secreto aleatorio. Por lo tanto, una contraseña débil por sí sola no convierte la copia del servidor en un práctico ataque de diccionario.

Si el atacante también obtiene secret_key, puede probar las conjeturas de contraseña maestra sin conexión. Esto es costoso, pero la dificultad final depende de la seguridad de la contraseña.

Claves

Caballeros, ¿qué hacen las llaves? Llaves... ¿desbloquear cosas?

Piratas del Caribe – El cofre del hombre muerto

La aplicación obtiene 32 bytes aleatorios del generador criptográfico del sistema operativo y los prepara para su uso como clave. Estos bytes preparados son la clave maestra privada, master_key.

La aplicación siempre obtiene las mismas claves de master_key: repetir los mismos cálculos en los mismos bytes da el mismo resultado. Por tanto, un ordenador portátil y un teléfono pueden recuperar claves idénticas. Para obtener master_key en un nuevo dispositivo, la aplicación primero debe abrir el candado con la contraseña maestra y secret_key.

El primer par: cifrado

Soy el maestro de llaves.

Cazafantasmas

Para almacenar datos cifrados, la aplicación calcula una clave pública de master_key: secret_key. Puede cifrar datos para la bóveda, mientras que el descifrado requiere master_key.

"Público" describe el papel de esta clave en el algoritmo, no la obligación de dársela a todos. En Confidex permanece con el usuario. También es el segundo secreto necesario para abrir la cerradura, por lo que permanece local y no se envía a través de la red. Por sí solo, secret_key permite que alguien cifre datos para la bóveda, pero no los descifre ni firme cambios en su nombre.

X25519 es el algoritmo que toma master_key y calcula secret_key. Recuperar master_key de secret_key es prácticamente inviable. Un ataque clásico a una supercomputadora imaginaria que realiza 10¹⁸ operaciones sobre la curva por segundo tardaría aproximadamente 2,3 billones de años en promedio (estimación de SafeCurves).

public bytes Random bytes32 bytes master_key secret_key public bytes Random bytes32 bytes master_key secret_key
Desde bytes aleatorios hasta las claves de cifrado.

El segundo par: firmas.

Lo único original es la firma

Atrápame si puedes

El primer par ayuda a ocultar el contenido. Ahora necesitamos una forma de confirmar que el propietario de la bóveda autorizó un cambio. El servidor también debe poder verificar las firmas, por lo que debemos enviarle una clave de verificación pública. La parte pública del primer par es secret_key, que el servidor no debe recibir. Por lo tanto, creamos un segundo par de claves permanente para las firmas.

En lugar de almacenar otro secreto independiente, derivamos una clave de firma privada de master_key y la llamamos signing_key. La aplicación lo utiliza para firmar datos y el bloqueo. Otros dispositivos y el servidor utilizan su parte pública para comprobar firmas sin leer el contenido.

Podemos recuperar este par del mismo master_key. La aplicación obtiene un valor inicial a partir de él: 32 bytes que determinan de forma única la clave de firma.

Para derivar diferentes claves para diferentes trabajos, incluimos una etiqueta fija en el cálculo: aquí, confidex/vault/ed25519/v1. El mismo master_key y la misma etiqueta siempre producen la misma clave.

La contraseña maestra no ha entrado en estos cálculos. Ambos pares dependen de los bytes aleatorios de master_key, no de una cadena inventada por una persona. Esta distinción importará más adelante: la contraseña protegerá el acceso a las claves que ya existen, por lo que podremos cambiarla sin cambiar las claves mismas.

La derivación utiliza una función de derivación de clave basada en hash: HKDF, HMAC-based Extract-and-Expand Key Derivation Function. Confidex lo usa con SHA-256, de ahí el nombre HKDF-SHA256. Los 32 bytes resultantes son el seed para el par Ed25519. La parte pública de signing_key no revela secret_key, por lo que podemos enviarla al servidor para verificar la firma.

public bytes HKDF-SHA256keymaster_keyinfo"confidex/vault/ed25519/v1" signing_key Signature verification public bytes HKDF-SHA256keymaster_keyinfo"confidex/vault/ed25519/v1" signing_key Signature verification
Una clave de firma independiente, derivada de la misma clave maestra.

¿Qué es un archivo de bloqueo y qué hace?

Abre las puertas del compartimento de las cápsulas, HAL.
Lo siento, Dave. Me temo que no puedo hacer eso.

Una odisea espacial

La cerradura almacena un master_key cifrado. Para abrirlo se requiere la contraseña maestra y secret_key. El encabezado público contiene la versión del formato, el identificador del almacén, la parte pública de signing_key, un salt y los parámetros de procesamiento de contraseñas. La versión del formato interno está cifrada junto con master_key.

Al crear el bloqueo, la aplicación ya tiene master_key, por lo que calcula secret_key antes del cifrado. Al abrirlo, el usuario proporciona secret_key por separado: no está en la copia pública del candado.

Después del descifrado, la aplicación calcula la parte pública de master_key usando X25519 y requiere que coincida con secret_key. Luego recupera signing_key y compara su parte pública con la clave en el encabezado. De este modo la cerradura se controla sola.

Para ralentizar el proceso de adivinar la contraseña maestra, cada intento debe costar tiempo y memoria. Confidex usa Argon2id con 64 MiB de memoria y tres pases. El procesamiento de contraseñas también utiliza una sal aleatoria. La sal no es un secreto: hace que la misma contraseña produzca resultados diferentes en diferentes cerraduras.

Si ya se conoce secret_key, la longitud de la contraseña cambia drásticamente la escala de un ataque. Hay alrededor de 12 millones de combinaciones de 5 letras latinas minúsculas aleatorias, alrededor de 8 mil millones de 7 y alrededor de 9,5 × 10¹⁶ de 12. Pasar de 7 a 12 letras aumenta el espacio de búsqueda casi 12 millones de veces. Si la búsqueda de 7 letras llevara un día de media en una máquina determinada, 5 tardaría unos dos minutos y 12 unos 33.000 años.

Para obtener una clave del resultado Argon2id y secret_key, usamos la función HKDF-SHA256 introducida anteriormente, con la etiqueta confidex/lock/wrap/v1. El resultado es wrapping_key, la clave que cifra la cerradura.

Debemos ocultar master_key y protegerlo de manipulaciones. XChaCha20-Poly1305 lo cifra con wrapping_key y agrega una etiqueta de autenticación. La etiqueta vincula el texto cifrado al encabezado público: una contraseña incorrecta o un cambio en los bytes protegidos hacen que la apertura falle. La etiqueta y un valor aleatorio de un solo uso, el nonce, se almacenan con el texto cifrado.

Cada vez que se crea o cifra nuevamente el bloqueo, la aplicación obtiene una sal nueva y nonce. El contenido cifrado incluye un byte de versión y los 32 bytes de master_key; junto a ellos se almacenan una etiqueta nonce de 24 bytes y una etiqueta de 16 bytes. La documentación del algoritmo describe la construcción.

Una discrepancia entre las claves recuperadas puede deberse a un bloqueo deliberadamente mal formado o a un error en el programa que lo creó. La corrupción de bytes aleatorios generalmente se detecta antes, cuando se verifica la etiqueta.

El servidor no tiene los secretos necesarios para comprobar esta etiqueta. Para transferir el bloqueo, la aplicación añade una firma realizada con signing_key. Cubre todo el archivo terminado, incluido el encabezado público y el contenido cifrado. El servidor verifica la firma y, en la primera carga, fija la parte pública completa de signing_key y el identificador de la bóveda interna. Comprueba los cambios posteriores con esta clave. El bloqueo y los datos cifrados están firmados con el mismo signing_key, recuperado de master_key.

la bóveda

Ahora podemos publicar el bloqueo. Solo alguien que conozca secret_key y el master password puede leer master_key, mientras que cualquiera puede tomar la clave de firma pública del archivo de bloqueo y verificar los datos firmados sin ver el contenido del candado ni de los datos.

Considere una contraseña, SuperPa55worD. Por ahora, solo queremos guardarlo: después de cerrar la aplicación, deberíamos poder enviar el archivo de bloqueo y la contraseña cifrada a cualquier lugar sin que nadie vea SuperPa55worD, y al mismo tiempo permitirles verificar nuestra autoría. El cifrado debe ser reversible cuando tengamos secret_key y master password. Un cifrado simétrico puede cifrar y descifrar con la misma clave. Como no almacenamos esa clave, debemos poder recuperarla usando la cerradura. Lo llamaremos message_key.

Obtendremos un message_key separado para cada nuevo cifrado. Todavía tenemos que organizar su recuperación: dejar la clave abierta junto al texto cifrado permitiría a cualquiera que tuviera una copia leerlo todo.

A partir de bytes aleatorios nuevos, la aplicación crea un par temporal X25519 solo para este cifrado: record_key. Luego toma la parte privada de record_key y la clave pública permanente de la bóveda, secret_key, y calcula un secreto compartido. Una función separada deriva message_key de ese secreto; esta clave cifra nuestra contraseña.

Al leer, la aplicación toma otro camino. Utiliza master_key y la parte pública de record_key. Estas dos claves producen el mismo secreto compartido. Matemáticamente, ambas rutas multiplican el mismo punto inicial de la curva por dos números privados, en diferente orden. El orden no cambia el resultado. Por tanto, podemos recuperar la clave de descifrado sin conservar la parte privada de record_key.

Obtener este secreto a partir de las dos claves públicas es prácticamente imposible: cualquiera de las rutas necesita la clave privada correspondiente. Los puntos públicos no nos permiten recuperar eficientemente los números privados que los produjeron. Este truco matemático sustenta la seguridad de la bóveda. Pero eso no impide que alguien nos dé una bóveda diferente con nuestro candado adjunto.

Ahora podemos almacenar el texto cifrado junto con la parte pública de record_key. La parte privada de record_key y la message_key usada ya no son necesarias para lecturas futuras. Posteriormente, la aplicación recupera la clave de descifrado de master_key y la parte pública almacenada de record_key. Por lo tanto, un secreto de bóveda permanente puede descifrar muchos valores cifrados por separado, cada uno con su propio message_key recuperado cuando sea necesario.

No basta con ocultar el texto: también debemos detectar manipulaciones al leerlo. Por lo tanto, el cifrado añade una etiqueta de autenticación, un valor breve que se comprueba junto con los datos cifrados. Si la verificación falla, la aplicación rechaza el resultado. No debe mostrar texto aleatorio o alterado como una contraseña descifrada correctamente.

public bytes Fresh random bytes record_key secret_key Shared secret Derive message key message_key Encrypted passwordpacket public bytes Fresh random bytes record_key secret_key Shared secret Derive message key message_key Encrypted passwordpacket
Un par de claves nuevas para cada cifrado.

Hemos descrito una construcción en la que un par asimétrico ayuda a establecer un secreto compartido, mientras que message_key cifra el contenido. Esto se llama Hybrid Public Key Encryption o HPKE y se especifica en RFC 9180. Confidex usa X25519 para acordar el secreto, HKDF-SHA256 para derivar claves y ChaCha20-Poly1305 para cifrado con verificación de integridad. Aquí, HKDF toma el secreto compartido acordado usando record_key y el par master_key; Este es un trabajo independiente de derivar el Ed25519 seed. RFC 5869 describe HKDF.

Para el siguiente cifrado, la aplicación crea un nuevo contexto record_key y un nuevo contexto HPKE. Reutilizar una combinación de message_key y nonce rompe la protección del cifrado. En Confidex, la clave y nonce se derivan dentro de HPKE, se usan para un mensaje y luego se descarta el contexto.

El propio ChaCha20 utiliza un message_key de 256 bits. Para tener una idea de un ataque directo, imagine una supercomputadora ficticia probando 10¹⁸ claves candidatas por segundo. Buscar las 2²⁵⁶ claves posibles llevaría aproximadamente 1,8 × 10⁵¹ años. RFC 8439 describe el algoritmo y su análisis.

Un récord

¿Estamos felices?

Ficción pulpa

Hemos guardado la contraseña, pero para iniciar sesión en un sitio web normalmente también se requiere un nombre de usuario y una dirección. A veces necesitamos una nota al lado de ellos. Si almacenamos estos valores por separado, la aplicación debe recordar de alguna manera que van juntos. Pongamos estos “papeles” en un sobre. Combinamos los campos en una colección y la llamamos record.

Un registro puede contener cualquier combinación de campos. La aplicación los reúne en un solo paquete, lo comprime y cifra el conjunto mediante la construcción que acabamos de describir. Los valores, nombres, tipos y etiquetas de los campos están todos protegidos. Cambiar la dirección del sitio web, incluso manteniendo la misma contraseña, crea una nueva versión del registro completo, cifrada con un nuevo record_key.

cadena hash

¿TU PROYECTO DEBE UTILIZAR UNA CADENA DE BLOQUE? → NO

XKCD 2267

Alguien que conozca secret_key puede preparar un registro cifrado para nuestra bóveda. Para evitar aceptarlo como nuestro, la aplicación firma cada versión del registro con signing_key. Sin esta clave, un extraño no puede crear una versión con una firma que el servidor y otros dispositivos acepten.

La versión cifrada necesita cierta información adjunta: a qué bóveda y registro pertenece, cuándo cambió y qué versiones le preceden. Esto forma el encabezado público. No contiene la contraseña, pero aún necesita protección: de lo contrario, alguien podría cambiar la propiedad y las relaciones de los objetos sin tocar el texto cifrado. La aplicación firma los bytes exactos del encabezado junto con el texto cifrado. Cambiar los datos firmados invalida la firma.

El servidor parece un almacén de paquetes sellados. Ve el encabezado, el contenido cifrado y la firma, pero no la contraseña que contiene, y sabe que el destinatario existe en la ciudad. Comprueba la firma con la parte pública fijada de signing_key y decide si acepta la nueva versión. No necesita ni la contraseña maestra ni secret_key ni ninguna clave privada. Al leer, el cliente verifica la misma firma y luego verifica nuevamente el vínculo entre el contenido y el encabezado durante el descifrado, utilizando la etiqueta de autenticación HPKE.

La firma también protege los vínculos entre versiones. Un hash SHA-256 del encabezado exacto y los bytes de texto cifrado se convierte en el identificador de la versión. El encabezado de la siguiente versión contiene el identificador de la versión anterior. Cuando cambiamos la contraseña de un sitio web, la aplicación crea y firma un nuevo objeto con este enlace. El objeto antiguo y su firma permanecen sin cambios. Por lo tanto, reemplazar el predecesor conservando la firma de la nueva versión fracasará. Si dos dispositivos realizan cambios independientes, ambas ramas sobreviven; una versión fusionada puede referirse a ambos.

¿Recuerdas la cadena de bloques? Ahí está :-).

El encabezado público nos permite comprobar los enlaces sin descifrarlos, pero parte de la información permanece visible. El servidor conoce los identificadores, los enlaces de versión, la hora de cambio proporcionada por el cliente, la cantidad de campos y el tamaño codificado de cada valor de campo. El cifrado oculta el contenido del registro, no esta información estructural.

Header andparent IDs Encrypted recordpacket Sign revision signing_key Signed revision Header andparent IDs Encrypted recordpacket Sign revision signing_key Signed revision
El encabezado y el texto cifrado están firmados juntos.

Sincronización

"No es una fuga de datos, es una copia de seguridad distribuida gratuitamente".

Meme de Internet.

Para trabajar en varios dispositivos, necesitamos transferir el historial de bloqueo y registro entre ellos. El formato del objeto no cambia: transferimos los mismos bytes cifrados y firmas que se almacenan localmente.

Para la transferencia manual, la aplicación crea un archivo ZIP que contiene el candado, todas las versiones de registros cifrados almacenadas localmente y una lista de archivos firmados. No se incluyen ni la contraseña ni secret_key. En otro dispositivo, la aplicación primero abre el candado, luego verifica las firmas, descifra los registros y restaura una copia de la bóveda local. La importación está destinada a un dispositivo donde esta bóveda aún no existe; no fusiona el historial con una copia existente.

Un servidor de sincronización intercambia cambios entre copias de trabajo. En la primera carga, recibe el bloqueo firmado, verifica la firma y calcula la dirección de la bóveda pública desde la parte pública de signing_key. Luego recuerda esta clave y el identificador de la bóveda del encabezado: esto registra la bóveda en el servidor. Los cambios posteriores deben coincidir con este identificador y llevar una firma de la misma clave.

Un dispositivo nuevo necesita la dirección de la bóveda y secret_key, por lo que la aplicación los combina en un código de transferencia con una suma de verificación. El usuario transfiere este código al dispositivo e ingresa la contraseña maestra por separado; sólo la dirección pública se envía al servidor.

Después de abrir el candado descargado, la aplicación compara las partes públicas de las claves recuperadas con secret_key y el encabezado, y compara la dirección calculada con el código de transferencia. Luego guarda la parte pública verificada de signing_key para verificar los registros.

Durante la sincronización, la aplicación recupera las versiones que faltan y carga sus cambios. El servidor comprueba firmas y enlaces a versiones anteriores, almacena los objetos aceptados y los pasa a otros dispositivos. Verificar los datos recibidos, descifrarlos y resolver conflictos se realiza en los dispositivos del usuario.

El registro no solicita nombre, dirección de correo electrónico ni contraseña separada. El servidor ve la dirección pública, la clave de verificación de firma, los encabezados públicos, los tamaños de los objetos y los enlaces de versión. También ve direcciones IP y tiempos de solicitud. La contraseña maestra, secret_key, las claves privadas y el contenido del registro de texto sin formato permanecen en los dispositivos del usuario. Cualquiera que conozca la dirección pública puede descargar los objetos cifrados, pero leerlos requiere los secretos del usuario.

Estos controles detectan objetos falsificados, pero no prueban que el servidor haya mostrado el historial completo y su último estado. Puede ocultar una rama que el cliente nunca ha visto o devolver un conjunto de datos antiguo consistente. Una firma confirma la autenticidad de un objeto individual, no la integridad de la respuesta del servidor. El usuario puede notar que faltan cambios en otro dispositivo, pero un historial antiguo consistente pasará la verificación hash-chain.

Cambiar la contraseña maestra

Entonces, ¿le importaría a Su Santidad cambiar su contraseña?

Hackers (1995)

Cambiar la contraseña maestra vuelve a cifrar solo el bloqueo; master_key por dentro permanece igual. Por lo tanto, los registros, sus firmas, secret_key y la dirección de la bóveda permanecen sin cambios. El nuevo candado está firmado con el mismo signing_key.

Todavía se abre una copia de bloqueo antigua con la contraseña anterior y el mismo secret_key; cambiar la contraseña no revoca las claves que ya han sido expuestas.

La imagen completa

El bloqueo protege a master_key, message_key cifra registros y signing_key crea firmas. Los archivos ZIP y el servidor reciben objetos cifrados con firmas.

public bytes public bytes public bytes master_key Fresh randombytes secret_key record_key Master password wrapping_key signing_key message_key lock Encrypted records Signed lockand records Signatureverification ZIP archive /sync server public bytes public bytes public bytes master_key Fresh randombytes secret_key record_key Master password wrapping_key signing_key message_key lock Encrypted records Signed lockand records Signatureverification ZIP archive /sync server
Las claves, qué protegen y qué se puede compartir.