A continuación encontrarás traducido al castellano el artículo escrito por Gabriel Ludosanu y publicado originalmente en el Blog oficial de Xojo.
El módulo Crypto cubre varios tipos de funciones relacionadas con la Criptografía. La parte difícil no suele ser encontrar una función determinada, sino saber la adecuada para el tipo de solución que quieres implementar.
¿Qué tipo de función deberías de escoger cuando tu app necesita?
- Recordar la contraseña de un usuario.
- Guardar un token de API en el archivo local de ajustes.
- Comprobar qué petición de una API no ha cambiado.
- Verificar que una actualización proviene de ti.
Los siguientes ejemplos darán respuesta a estas preguntas con algunos pequeños escenarios. Estos muestran como encaja cada una de los funciones; teniendo en cuenta que no cubren todos los aspectos relacionados con la seguridad.
Un buen punto de partida es:
- Utiliza PBKDF2 cuando necesites comprobar una contraseña.
- Utiliza AES cuando necesites ocultar datos que tu app necesite recuperar posteriormente.
- Utiliza HMAC cuando dos sistemas confiables compartan un secreto y precises detectar los mensajes que han cambiado.
- Utiliza Ed25519 cuando un lado firme datos y otros componentes sólo precisen verificarlos.
Ejemplo 1: Recordar la contraseña de un usuario con PBKDF2
Imagina una app de escritorio o web con una pantalla de acceso. Cuando el usuario cree una cuenta tendrás que guardar la información que te permita comprobar posteriormente la corrección de la contraseña introducida.
No guardes la contraseña propiamente dicha
¡Si alguien accediese a la base de datos podría obtener las contraseñas de los usuarios! Un hash SHA-256 en claro no supone una buena opción, dado que los atacantes pueden calcular los valores de hash con bastante rapidez, e incluso probar con grandes listas de contraseñas frecuentes. PBKDF2 está diseñada para este tipo de escenario. Combina la contraseña con una “sal” (dato adicional) aleatoria y repite el cálculo varias veces.
Var password As String = "correct horse battery staple" Var salt As String = EncodeHex(Crypto.GenerateRandomBytes(16)) Var iterations As Integer = 100000 Var derived As MemoryBlock = Crypto.PBKDF2(salt, password, iterations, 32, Crypto.HashAlgorithms.SHA2_512) Var storedHash As String = EncodeHex(derived)
Guarda la sal, las iteraciones, el nombre del algoritmo y el valor de storedHash en el registro del usuario. El valor de sal no precisa ser secreto. Tendrás que recuperarlo posteriormente cuando el usuario se vuelva a conectar.
Var suppliedPassword As String = "correct horse battery staple"
Var calculated As MemoryBlock = Crypto.PBKDF2(salt, suppliedPassword, iterations, 32, Crypto.HashAlgorithms.SHA2_512)
Var calculatedHash As String = EncodeHex(calculated)
If calculatedHash = storedHash Then
MessageBox("Password accepted")
Else
MessageBox("Password rejected")
End If
La app no precisa recuperar la contraseña original, si no que calcula el valor de nuevo y lo compara con el guardado.
El valor de iteraciones en este ejemplo es sólo una forma de hacerlo. Pruébalo en el ordenador más lento que vayas a soportar. Guarda el valor de iteraciones con el hash de modo que puedas incrementarlo posteriormente y actualizar los registros antiguos tras un acceso correcto.
Utiliza Crypto.GenerateRandomBytes en este caso porque la “sal” no debe de ser predecible. La misma función resulta útil cuando has de crear un token aleatorio o un vector de inicialización.
Ejemplo 2: Guardar un token API en un archivo de ajustes encriptado
Imagina ahora una app desktop que permita la conexión de un usuario a un servicio en línea. Después de que el usuario haya introducido un token de API la app debería de recordarlo sin guardarlo como texto en claro en el archivo de ajustes.
Este es un trabajo para la encriptación simétrica. La app utiliza la misma clave secreta para cifrar y desencriptar el valor.
AES acepta claves con una longitud de 16, 24 o 32 bytes. El vector de inicialización es de 16 bytes para AES. Este ejemplo utiliza una clave de 32 bytes y un vector de inicialización (IV) de 16 bytes:
Var secretKey As String = Crypto.GenerateRandomBytes(32) Var initializationVector As MemoryBlock = Crypto.GenerateRandomBytes(16) Var apiToken As String = "token-from-the-service" Var encryptedToken As MemoryBlock = Crypto.AESEncrypt(secretKey, apiToken, Crypto.BlockModes.CBC, initializationVector)
Para leer el token posteriormente:
Var decryptedToken As MemoryBlock = Crypto.AESDecrypt(secretKey, encryptedToken, Crypto.BlockModes.CBC, initializationVector) Var apiTokenAgain As String = decryptedToken
El IV no es una contraseña y no tiene por qué ser secreto. Guárdalo con el valor cifrado. Necesitarás el mismo IV para desencriptarlo.
La clave es la parte difícil. Si generas una nueva clave cada vez que se ejecute la app, entonces no podrás desencriptar los ajustes guardados el día anterior. Una aplicación real ha de almacenar la clave en un tipo de almacenamiento seguro, derivarlo de un secreto proporcionado por el usuario, o bien mantener el valor sensible en un servidor. No incluyas una clave de producción directamente en el código fuente.
El registro cifrado podría contener estos campos:
- version
- iv
- ciphertext
Guarda IV y ciphertext como Base64 o bien texto hexadecimal si el formato de ajustes no puede contener datos binarios. Guarda un valor de versión de modo que puedas cambiar el formato posteriormente.
El cifrado CBC oculta el token pero no prueba que el valor de ciphertext haya cambiado. Para datos reales, también has de autenticar el registro cifrado. Con CBC esto implica utilizar un diseño del tipo cifra-y-luego-aplica-MAC, utilizando para ello una clave MAC independiente. Otra opción consiste en el uso de una librería de cifrado autenticada o bien un servicio que proporcione un modo AEAD.
No re-utilices un IV con la misma clave, y no utilices el modo ECB para los datos de una aplicación.
Ejemplo 3: firmar peticiones API con HMAC
Supongamos que controlas tanto la aplicación cliente como servidor. El cliente envía una orden al servidor, y el servidor necesita saber si algún actor intermedio ha cambiado la petición durante la transmisión.
El cliente y el servidor pueden compartir un secreto. El cliente firma la petición usando HMAC, y el servidor calcula el mismo HMAC. Si los valores difieren, entonces el servidor omite la petición.
Var sharedSecret As String = "replace-this-with-a-real-secret"
Var requestText As String = "POST|/api/orders|1714492800|{""id"":42}"
Var requestData As MemoryBlock = requestText
Var signature As MemoryBlock = Crypto.HMAC(sharedSecret, requestData, Crypto.HashAlgorithms.SHA2_256)
Var signatureText As String = EncodeBase64(signature)
Envía requestText y signatureText al servidor. El servidor ha de crear exactamente el mismo texto antes de calcular su propia signatura. Un separador diferente, final de línea o diseño JSON distinto producen un resultado diferente.
Una firma por sí misma no evita que alguien pueda enviar de nuevo la misma petición. Incorpora una huella de tiempo (timestamp) y descarta las peticiones que sean demasiado antiguas. Para las operaciones que sólo deban de ocurrir una vez, incluye un ID de petición único y registra los IDs que el servidor ya haya procesado.
HMAC es un buen encaje para dos servicios del lado del servidor o para un diseño de tipo cliente-servidor. Es un mal encaje cuando incluyas el secreto compartido en una app móvil o de escritorio. Un secreto compilado en una app puede ser extraído eventualmente.
Ejemplo 4: verificar y actualizar con Ed25519
Un escenario diferente tiene lugar cuando se trata de distribuir actualizaciones de una aplicación. El servidor de actualizaciones debería de firmar un archivo, y la app instalada debería de verificarla. La app instalada no debería de incluir el secreto mediante el que se podrían gener firmas falsas.
Utiliza un par de clave pública y privada:
- Mantén la clave privada en el equipo de firmado.
- Incluye la clave pública en la aplicación o distribúyela mediante un canal seguro.
Var privateKey As String
Var publicKey As String
If Crypto.ED25519GenerateKeyPair(privateKey, publicKey) Then
Var updateInfo As String = "version=4.2.1|sha256=..."
Var signature As MemoryBlock = Crypto.ED25519Sign(updateInfo, privateKey)
If signature <> Nil And Crypto.ED25519VerifySignature(updateInfo, signature, publicKey) Then
MessageBox("Update information is authentic")
End If
End If
En un actualizador real, verifica la firma antes de confiar en la versión o archivo descargado. La firma no prueba por sí misma que la actualización sea segura; prueba que los datos coinciden con la clave privada asociada con la clave pública.
Mantén de forma estable los bytes exactos. Si la herramienta de firma añade finales de línea pero la app los elimina antes de realizar la verificación, entonces la firma fallará.
¿Qué función soluciona este problema?
| Problema | Uso | Guardado o enviado |
|---|---|---|
| Comprobar una contraseña | Crypto.PBKDF2 | Sal, contador de iteración, algoritmo, valor derivado |
| Crear una sal impredecible o IV | Crypto.GenerateRandomBytes | La sal o IV, cuando la operación requiera usarlas posteriormente |
| Ocultar datos que tu app deba de descifrar | Crypto.AESEncrypt y Crypto.AESDecrypt | Clave, IV, y texto cifrado, usando un diseño de almacenamiento seguro |
| Detectar cambios en una petición de secreto compartido | Crypto.HMAC | Mensaje y valor HMAC |
| Verificar datos firmados por otro sistema | Crypto.ED25519Sign y Crypto.ED25519VerifySignature | Firma y clave pública |
| Crear una huella fija de datos públicos | Crypto.Hash | El hash esperado a partir de una fuente fiable |
Empieza con el problema y no con el nombre de la función:
- ¿Necesita comprobar una contraseña? Usa PBKDF2.
- ¿Necesita leer los datos originales? Usa cifrado.
- ¿Necesita detectar un mensaje modificado y compartir un secreto con el emisor? Usa HMAC.
- ¿Ha de verificar un mensaje sin tener que crear firmas? Usa una firma de clave pública.
Una breve lista de comprobación
- Nunca guardes las contraseñas como texto plano.
- No utilices un hash rápido de propósito general como esquema de almacenamiento de contraseñas.
- No guardes secretos de producción en el código fuente o en una aplicación de escritorio.
- Genera valores de “sal” y vectores de inicialización (IV) con Crypto.GenerateRandomBytes.
- Utiliza SHA2_256 o superior para crear nuevos valores de hash y HMAC. Evita el uso de MD5 y SHA-1 en tus diseños de seguridad.
- No trates el texto cifrado AES-CBC como autenticado salvo que añadas comprobaciones de integridad.
- Mantén estables los bytes exactos a firmar.
- Comprueba las contraseñas inválidas, claves inválidas, mensajes modificados, peticiones contestadas y texto cifrado corrompido.
- Comprueba las notas de compatibilidad de Xojo para cada algoritmo de tu elección.
- Utiliza un formato establecido o bien una librería en vez de inventar un protocolo criptográfico.
El módulo Crypto te proporciona las operaciones; tu aplicación aun es la responsable de decidir donde guardar las claves, qué ha de firmarse, cómo formatear los datos y como han de sustituirse las claves. Estas decisiones determinan si el ejemplo puede crecer en algo más a lo que puedas aplicar el soporte adecuado.