Los programadores saludables desayunan cereales criptográficos todas las mañanas

Gonzalo Álvarez de Marañón    26 noviembre, 2019
Los programadores saludables desayunan cereales criptográficos todas las mañanas

¿Cuál es la peor pesadilla de un programador? La criptografía.

Según los autores del estudio Why does cryptographic software fail?:

«Nuestro estudio cubre 269 vulnerabilidades criptográficas reportadas en la base de datos CVE desde enero de 2011 hasta mayo de 2014. Los resultados muestran que sólo el 17% de los errores se encuentran en bibliotecas criptográficas (que a menudo tienen consecuencias devastadoras), y el 83% restante son usos indebidos de bibliotecas criptográficas por parte de aplicaciones individuales. Observamos que la prevención de errores en diferentes partes de un sistema requiere técnicas diferentes, y que no existen técnicas efectivas para tratar ciertas clases de errores, como la generación de claves débiles.»

Por otro lado, ¿y si nos adentramos en el mundo Android? En An Empirical Study of Cryptographic Misuse in Android Applications los autores nos cuentan:

«Desarrollamos técnicas de análisis de programas para comprobar automáticamente los programas en el mercado de Google Play, y encontramos que 10.327 de las 11.748 aplicaciones que utilizan APIs criptográficas, 88% en total, cometen al menos un error.»

Lo que vienen a confirmar estos estudios es que rara vez la criptografía es el eslabón débil de la seguridad. La criptografía no solo existe en los libros de matemáticas. Ha de corporeizarse, para que la criptografía funcione, necesita estar escrita en software, integrada en un sistema de software más grande, administrada por un sistema operativo, ejecutada en hardware, conectada a una red y configurada y operada por los usuarios. Cada uno de estos pasos trae consigo dificultades y vulnerabilidades.

Son los fallos cometidos por los desarrolladores al implementar la criptografía los que abren la puerta al desastre. Este artículo recoge los errores más típicos que todo desarrollador debe evitar para crear aplicaciones criptográficamente sólidas.

Si reinventar la rueda ya es mala idea, en criptografía es el caos

Resulta edificante la lectura del artículo Great Crypto Failures, firmado por dos investigadores de Check Point Software Technologies, donde reseñan varios fallos épicos de implementaciones criptográficas en ransomware. Si quieren que su código sea ligero y único, los creadores de ransomware se enfrentan a la necesidad de escribir sus propias implementaciones de los algoritmos criptográficos como AES o RSA, en lugar de recurrir a bibliotecas criptográficas establecidas. Pero tú no compartes ninguna de sus restricciones, en lugar de escribir tu propia versión de un algoritmo, deberías recurrir a bibliotecas criptográficas creadas por expertos en criptografía que saben muy bien lo que se traen entre manos.

Muchos lenguajes de programación cuentan con una o varias APIs para criptografía. Por ejemplo, Java ofrece la Java Cryptographic Extensions (JCE), mientras que .NET proporciona las clases del espacio de nombres System.Security.Cryptography. Además de las primitivas criptográficas ofrecidas por los propios lenguajes de programación, existen bibliotecas criptográficas independientes, como OpenSSL, Bouncy Castle, o NaCl, entre las más populares y robustas.

Todas estas APIs criptográficas proporcionan servicios criptográficos como cifrado, generación de claves secretas, código de autenticación de mensajes, acuerdo de claves, gestión de certificados, firma digital, etc. ¡Bingo! Usas una biblioteca y ¡problema resuelto!

No tan rápido. En realidad, tus problemas no han hecho más que empezar. Es cierto que estas APIs están concebidas para permitir a los desarrolladores de aplicaciones aplicar fácilmente la criptografía. Aíslan al desarrollador de los arcanos detalles criptográficos necesarios para implementar los distintos algoritmos. Así, el programador se dedica a escribir su código y se olvida de los herméticos entresijos de la criptografía. La codificación de los algoritmos se delega en los proveedores de las APIs, los auténticos expertos, de manera que al desarrollador le basta con invocar a las funciones expuestas por las APIs. ¿Qué podría salir mal?

Ay, la criptografía es más difícil de lo que parece. Para confusión de desarrolladores, las APIs ofrecen una amplia variedad de algoritmos diferentes que, a su vez, admiten una multitud de modos y opciones de configuración. Además, cada proveedor puede admitir algoritmos adicionales o, peor aún, proporcionar diferentes valores predeterminados para la misma llamada a la API. Como resultado, los desarrolladores se enfrentan a la tarea de utilizar y orquestar correctamente todos estos componentes de la API, a menudo sin entender a fondo su funcionamiento.

Hmmmm. A mí ya me está llegando el aroma de una suculenta receta para el desastre.

Entren si se atreven en el túnel del terror criptográfico

Imagina que eres un programador de Android. Sí, eres el Rey de la Programación, pero nunca te enseñaron criptografía en la escuela. Un día, necesitas algo aparentemente tan trivial como cifrar una cadena de texto. ¿Cómo lo harías?

El primer paso consiste en consultar al oráculo de Google para ver si ya hay algo por ahí que puedas reutilizar. Después de todo, no necesitas más que cifrar una triste cadena de texto. En una reciente charla, Maximilian Blochberger, creador de la biblioteca criptográfica Tafelsalz, buscó en Google:

Android encryption example

Y este es el resultado que apareció en primer lugar y que a mí también me sigue apareciendo a día de hoy:

private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {

SecretKeySpec skeySpec = new SecretKeySpec(raw, ”AES”);
Cipher cipher = Cipher.getInstance(”AES”);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
[…]
byte[] keyStart = ”this is a key”.getBytes();
KeyGenerator kgen = KeyGenerator.getInstance(”AES”);
SecureRandom sr = SecureRandom.getInstance(”SHA1PRNG”);
sr.setSeed(keyStart);
kgen.init(128, sr); // 192 and 256 bits may not be available
SecretKey skey = kgen.generateKey();
byte[] key = skey.getEncoded();
byte[] encryptedData = encrypt(key,b);
byte[] decryptedData = decrypt(key,encryptedData);

Vamos a diseccionar el código anterior, línea por línea:

encrypt(byte[] raw, byte[] clear)

No se valida la longitud de la entrada ni de salida. Todos sabemos lo peligroso que resulta trabajar con buffers sin ningún tipo de validación. Entre otros problemas, pueden producirse desbordamientos, leer más allá del tamaño del array, y esos problemillas.

SecretKeySpec skeySpec = new SecretKeySpec(raw,
”AES”);

Un desarrollador no tiene por qué saber de criptografía. Él solo quiere cifrar una cadena de texto. ¿Con qué algoritmo? ¡Pues ni idea! Mientras sea seguro, le vale. Todas las bibliotecas ofrecen una gran variedad de algoritmos. ¿Son todos seguros? Tristemente, no. En un artículo previo listé los algoritmos que deben dejar de usarse. Si lo consultas, encontrarás a AES entre los seguros. Entonces, todo bien, ¿no?

Pues no, porque en JCE, “AES” es la abreviatura de “AES/ECB/PKCS5PADDING”, que significa que se utilizará AES en modo ECB de encadenamiento de bloques y con el algoritmo de relleno descrito en PKCS#5. Si te has leído el artículo sobre criptografía insegura, recordarás que el modo ECB es inseguro. Por desgracia, las propias bibliotecas criptográficas pueden no estar proporcionando opciones seguras por defecto o pueden proporcionar algoritmos obsoletos por cuestiones de compatibilidad con aplicaciones legadas.

SecureRandom sr = SecureRandom.getInstance(”SHA1PRNG”);

Se utiliza el algoritmo SHA1, hoy considerado inseguro.

byte[] keyStart = ”this is a key”.getBytes();

Inicializa la clave de cifrado con una contraseña leída de un parámetro estático. Es una idea mala, mala, mala. Tan mala, que a nadie que copie este código se le ocurrirá hacerlo así, ¿verdad? ¿O no tan verdad?

De hecho, todo el asunto de la derivación de la clave de cifrado AES a partir de una contraseña está mal hecho. Existen protocolos mucho más seguros, como PBKDF2.

byte[] encryptedData = encrypt(key,b);

Se cifran los datos de entrada con la clave recién generada. Si se cifran los mismos datos con la misma clave, se obtendrá el mismo resultado. ¡Falta diversificación! En un buen protocolo criptográfico, por muchas veces que cifres un mismo mensaje con la misma clave, siempre obtendrás un resultado distinto. Puede conseguirse gracias a añadir aleatoriedad cada vez, como un vector de inicialización aleatorio, o bien añadiendo un contador, como en el caso del modo CTR. Técnicamente, se busca que el cifrado posea la propiedad IND-CPA.

byte[] decryptedData = decrypt(key,encryptedData);

Y ya, para terminar, redoble de tambores…, el cifrado no está autenticado, por lo que se garantiza la confidencialidad, pero no la autenticidad. Por ejemplo, AES-GCM proporciona ambos atributos.

No está mal para un pequeño código de ejemplo que aparece en primer lugar en la página de resultados de Google cuando se busca Android encryption example. ¡Menudo ejemplo! No es de extrañar que el 88% de las apps de Android tuvieran errores criptográficos.

Pero los problemas no acaban aquí. Incluso si resuelves todos los inconvenientes que he señalado, empiezan tus problemas verdaderamente gordos. ¿De dónde leerá el código la contraseña del usuario? ¿Dónde guardará la clave de cifrado recién derivada? ¿Cómo se gestiona la clave en memoria durante el cifrado? ¿De qué manera se cifrará a su vez para que esté protegida? ¿Quién tiene acceso a esas claves? ¿Cuántas veces se reutilizan por el mismo usuario o para el mismo fin? ¿Por cuánto tiempo se almacenan? Si pueden salir del sistema, ¿cómo se protegen? Y una larga ristra de preguntas similares, que obtienen respuesta en lo que se conoce como gestión de claves, una disciplina complicada de la criptografía.

Así que, ¿qué puedes hacer?

Algunos mandamientos cuando uses cripto en tus programas

Seguir las siguientes reglas no te garantizará la seguridad total. Pero no seguirlas te conducirá al desastre.

1) Nunca te inventes algoritmos criptográficos. Usa los existentes considerados seguros.

En el artículo “La criptografía insegura que deberías dejar de usar” encontrarás un listado de algoritmos considerados hoy inseguros y cuáles usar en su lugar. Por fácil que parezca inventar un algoritmo criptográfico, ¡no lo es! Las raras veces en que se ha podido vulnerar un sistema por culpa de un algoritmo débil, la causa fue:

  • Se usó un algoritmo obsoleto.
  • Se usó un algoritmo cocinado en casa.

¡No lo hagas!

2) Nunca codifiques algoritmos criptográficos. Usa bibliotecas de confianza.

Hay tantos detalles sutiles, que si no has entendido perfectamente el funcionamiento del algoritmo y no usas bibliotecas de enteros grandes cuyo funcionamiento entiendas también perfectamente, lo más probable es que algo salga mal. Personas con más conocimiento ya se han pegado con esos detalles. ¡Aprovéchate! Utiliza una API criptográfica de confianza.

3) Entiende cómo funcionan todas las funciones y parámetros de la biblioteca criptográfica que elijas.             

Ni la mejor biblioteca servirá de nada si no sabes usarla. Necesitarás tomar decisiones como:

  • El tipo de encadenamiento de bloques: nunca uses ECB. Usa otros modos con vectores de inicialización aleatorios y distintos cada vez.
  • La longitud de la clave: 128 bits o más para algoritmos de cifrado simétrico, 3072 bits o más para RSA y 256 bits o más para ECC.
  • Utilizar o no salts: desde luego, y que sean aleatorios y distintos en cada llamada.
  • Qué tipo de padding utilizar.
  • Etc.

Existen multitud de bibliotecas. Algunas, como OpenSSL, son muy crípticas y cuesta usarlas incluso a quienes comprenden la criptografía. Dan acceso a primitivas criptográficas de bajo nivel, por lo que para hacer tareas complejas, como por ejemplo autenticar y cifrar un mensaje, es necesario invocar a cerca de una docena de funciones. ¡Cuántas oportunidades para cometer algún error!

Por este motivo, criptógrafos benévolos que aman a la Humanidad se han esforzado en crear bibliotecas intuitivas y fáciles de usar, con funciones del nivel más alto posible. En lugar de exponer funciones básicas, estas bibliotecas dan acceso a tareas, como por ejemplo “autenticar y cifrar un mensaje” o “firmar un documento”. Es la propia API la que se encarga de los detalles farragosos implicados en las tareas.

Algunas bibliotecas a considerar creadas con esta filosofía son NaCl, Tafelsalz o Libsodium. Su objetivo es que te resulte casi imposible cometer errores de seguridad al usarlas.

4) Gestiona tus claves.

Utiliza claves generadas aleatoriamente. No uses semillas estáticas ni fácilmente adivinables, como la hora. Necesitas funciones criptográficamente seguras. No te preocupes, tu API las tendrá. Y recuerda: la función random()no sirve.

No uses contraseñas como claves. Usa funciones de derivación de claves a partir de contraseñas. No sufras, tu API las tendrá.

Nunca uses claves constantes.

Si usas criptografía asimétrica:

  • No copies las claves privadas, ni las almacenes en claro, ni las escribas (hard-code) en tus programas.
  • Evita usar la misma clave pública para operaciones diferentes: cifrado, autenticación, firma, etc.

La mala programación vuelve insegura la mejor criptogrfía

Por muy a prueba de fallos que esté diseñada una biblioteca criptográfica, tendrás que comprender, aunque sea superficialmente, lo que estás haciendo. La realidad es que muchos desarrolladores carecen de una comprensión formal de la aplicación de la criptografía en su software, a pesar de que son expertos en el desarrollo de software en sí mismo. A ver, no necesitas convertirte en un Caballero Criptógrafo de la Orden de la Cifra, pero tampoco te hará daño leer algún buen tutorial sobre criptografía o hacerte algún curso online de calidad. Y si quieres poner a prueba tus conocimientos sobre criptografía desarrollando código, atrévete con los Cryptopals challenges.

¡Desayuna tus cereales criptográficos todas las mañanas!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *