anterior índice siguiente

Cifrado simétrico

Las rutinas de encriptación proveen una interfaz a los algoritmos de encriptación de clave privada implementados en OpenSSL. Gracias a ella, podremos abstraer las particularidades de cada implementación y utilizar de forma muy similar (prácticamente idéntica), por ejemplo, un algoritmo DES y un IDEA. Para esta parte supondremos que usted comprende el funcionamiento de los algoritmos de clave simétrica, así como los modos de cifrado (CBC, ECB, CFB, OFB) de los algortimos de encriptación por bloques.

Si no lo ha hecho aún, échele un vistazo a la parte de claves simétricas del tutorial. Puesto que no se hará incapié en temas como la generación de las claves, etc. Trataremos esta parte del tutorial en tres subapartados:


La encriptación

Muy bien, lo primero de todo será enumerar las funciones de inicialización, actualización y finalización:

  • int EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, unsigned char *key, unsigned char *iv);
  • int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, unsigned char *in, int inl);
  • int EVP_EncryptFinal(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl);

Todas ellas devuelven 0 si se produce un error y un 1 en caso contrario.Vamos a por nuestro primer ejemplo:




Ejemplo 1. Inicializamos un contexto de encriptación

Este ejemplo es muy simple. Nuestro único objetivo por el momento es inicializar un contexto de encriptación. Como siempre esto lo hacemos con una función de inicialización: EVP_EncryptInit() . Ésta toma como argumentos el contexto a inicializar ( ctx ), el algoritmo que se utilizará para la encriptación ( type ), la clave (key ) y un vector de inicialización ( iv ).

Centremos la atención en el segundo parámetro. Se introduce una estructura de datos que no habíamos visto hasta ahora: la EVP_CIPHER. Esta estructura de datos se utiliza para identificar los parámetros característicos de un algoritmo simétrico: tamaño de la clave, tamaño del vector de inicialización, tamaño de bloque, el NID del algortimo, etc. Como puede intuirse no es el programador el encargado de rellenar cada uno de los campos de la estructura. Las siguientes funciones se utilizan para obtener punteros a EVP_CIPHER convenientemente inicializados. En el ejemplo 1 utilizamos la función EVP_ede_des3_cbc() para utilizar triple des con tres claves en modo cbc. La siguiente tabla muestra las funciones que podemos utilizar y el algoritmo que devuelven:

Función Algoritmo
EVP_enc_null(void) Este cifrador no hace nada
EVP_des_cbc(void), EVP_des_ecb(void), EVP_des_cfb(void), EVP_des_ofb(void) El algoritmo DES en modos CBC, ECB, CFB y OFB respectivamente.
EVP_des_ede_cbc(void), EVP_des_ede(void), EVP_des_ede_ofb(void), EVP_des_ede_cfb(void) Triple DES con dos claves en modos CBC, ECB, OFB y CFB respectivamente
EVP_des_ede3_cbc(void), EVP_des_ede3(void), EVP_des_ede3_cfb(void), EVP_des_ede3_ofb(void) Triple DES con tres claves en modos CBC, ECB, CFB y OFB respectivamente
EVP_desx_cbc(void) El algortimo DESX en modo CBC
EVP_rc4(void) El algoritmo RC4. Por defecto la longitud de clave es de 128 bits
EVP_idea_cbc(void), EVP_idea_ecb(void), EVP_idea_cfb(void), EVP_idea_ofb(void) El algoritmo IDEA en modos CBC, ECB, CFB, OFB y CBC respectivamente
EVP_bf_cbc(void), EVP_bf_ecb(void), EVP_bf_cfb(void), EVP_bf_ofb(void) El algoritmo Blowfish en modos CBC, ECB, CFB y OFB
EVP_cast5_cbc(void), EVP_cast5_ecb(void), EVP_cast5_cfb(void), EVP_cast5_ofb(void) El algoritmo CAST en modos CBC, ECB, CFB y OFB respecticamente
EVP_rc5_32_12_16_cbc(void), EVP_rc5_32_12_16(void), EVP_rc5_32_12_16_cfb(void), EVP_rc5_32_12_16_ofb(void) El algoritmo RC5 en modos CBC, ECB, CFB y OFB
EVP_rc2_cbc(void), EVP_rc2_ecb(void), EVP_rc2_cfb(void), EVP_rc2_ofb(void) El algoritmo RC2 en modos CBC, ECB, CFB y OFB. Por defecto utiliza una longitud de clave y de clave efectiva de 128 bits
Tabla 1. Algoritmos de encriptación soportados.

Aunque se pueda, no es muy aconsejable acceder directamente a los campos de una EVP_CIPHER (donde hoy pongo trigo mañana digo Rodrigo :-P). Para esta tarea disponemos de varias macros. En particular, nuestro ejemplo utiliza EVP_CIPHER_key_length(EVP_CIPHER *) y EVP_CIPHER_iv_length(EVP_CIPHER *) para obtener el tamaño en bytes de la clave y del vector de inicialización respectivamente. Según surjan necesidades las iremos viendo.

De acuerdo, vamos a complicarnos la vida (¿qué sería la vida sin complicaciones? :-O). A por otro ejemplo:




Ejemplo 2 . Encripta una cadena de caracteres.

Muy bien. La función de actualización EVP_EncryptUpdate() se utiliza para añadir los datos que queremos ir encriptando. Los parámetros que toma son: el contexto de encriptación (ctx ), los datos de entrada encriptados (out), el tamaño de out ( outl ), la información de entrada que se desea añadir ( in ) y la longitud de la información de entrada ( inl ).

EVP_EncryptUpdate() puede ser llamada en múltiples ocasiones para ir añadiendo nuevos datos a encriptar (in). Los resultados parciales se van devolviendo en out. El problema que se plantea es saber qué cantidad de memoria va a ser ocupada por el buffer out . La solución tiene relación con la forma de trabajar de los algoritmos en bloque (luego veremos que el razonamiento es igualmente aplicable a los cifradores de flujo). Si inl no es múltiplo del tamaño de bloque, la cantidad en bytes que se devolverá en out será: inl - (inl % tam_bloque). Los bytes que restan serán añadidos en subsiguientes llamadas a EVP_EncryptUpdate(). El peor de los casos se presenta cuando inl % tam_bloque = tam_bloque-1 (ver figura 1) por ese motivo para no desbordar el buffer out éste debe apuntar a una zona reservada de inl + tam_bloque - 1 ( la primera vez que se llame a EVP_EncryptUpdate() no ocurre nada, pero la siguiente podríamos desbordarlo. Reservando dicha cantidad de memoria esto no ocurrirá nunca ).

Figura 1. Representación del peor caso posible utilizando EVP_EncryptUpdate().

Lo que hemos visto es aplicable a la función EVP_EncryptUpdate. La cuestión ahora es saber qué cantidad de memoria es necesario reservar si quisiéramos obtener toda la encriptación. Pues bien, a los sumo: tamaño_entrada + tamaño_bloque. Esto es así por el modo en que trabaja EVP_EncryptFinal. Ésta devuelve un último bloque que contiene la información que faltaba encriptada (caso de que la entrada no sea múltiplo del tamaño de bloque) más tantos bytes como falten para completar un bloque. Si faltaran n bytes para completar el último bloque se le añadirían al final tomando cada uno de ellos el valor n. Ésto es lo que se conoce como padding pkcs. El peor caso posible se presenta cuando la información de entrada es múltiplo del tamaño de bloque, en cuyo caso EVP_EncryptFinal devuelve un bloque sólo de padding. La figura 2 muestra tal situación.

Figura 2. Represetación del peor caso posible utilizando EVP_EncryptFinal().

Después de este rollo te estarás preguntando: "¿tengo que aprenderme el tamaño de bloque de cada uno de los algoritmos?" Pues la respuesta es NO. De nuevo echamos mano de la EVP_CIPHER y encontramos la macro EVP_CIPHER_block_size(EVP_CIPHER *) que devuelve el tamaño del bloque en bytes. Cuando le pasamos un cifrador de flujo, devuelve 1 byte con lo que todo el razonamiento que hicimos para la reserva de memoria del buffer encriptado es igualmente aplicable a estos cifradores (RC4 ).

Vamos a tratar de exponer con más claridad el tema este de la reserva de memoria. Modifiquemos el ejemplo 2. Esta vez leeremos datos de la entrada estándar y el resultado lo devolveremos por salida estándar (ojito con el terminal :-P).




Ejemplo 3. Encriptamos la entrada estándard

Se ha escrito una función (encriptar) para encriptar (:-P). Lo cierto es que no hay mucho que se pueda comentar ya, de modo que pasamos a la desencriptación.


La desencriptación

Bueno, al fin pondremos las cosas claras :-P Si ha entendido la parte anterior, no tendrá ningún problema con ésta. Veamos las funciones de inicialización, actualización y finalización.

  • int EVP_DecryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, unsigned char *key, unsigned char *iv);
  • int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, unsigned char *in, int inl);
  • int EVP_DecryptFinal(EVP_CIPHER_CTX *ctx, unsigned char *outm,int *outl);

El significado de los parámetros es prácticamente idéntico a las funciones homólogas de la encriptación, de modo que no nos pararemos a explicarlas de nuevo. En esta ocasión el tamaño del buffer out en la función EVP_DecryptUpdate() deberá ser el tamaño del buffer in + tamaño de bloque del algoritmo, es decir: inl + EVP_CIPHER_block_size( EVP_CIPHER * ).

El proceso de desencriptación elimina los bloques que no son de padding (tras comprobar que es correcto). Por este motivo, en el peor de los casos, el tamaño de la información desencriptada no superará los bytes_entrada - 1 bytes. Sin embargo esto sólo es cierto para algoritmos de cifrado por bloques. Los cifradores de flujo devolverán bytes_entrada.

Para poner punto y final a esta parte ampliaremos el ejemplo 3, añadiendo una función desencriptar cuyo código se muestra a continuación.




Ejemplo 4 . Añadimos la función desencriptar al ejemplo 3.


Cuestiones Avanzadas

Hemos aprendido a encriptar y desencriptar con cualquier algoritmo de clave privada que esté implementado en OpenSSL . Ha llegado el momento de plantearnos algunas cuestiones relacionadas con la modificación de determinados parámetros de los algoritmos como puedan ser longitudes de clave y el número de vueltas del RC5 y RC2. Vamos a por ello.


Establecimiento de la longitud de clave
en algoritmos de clave variable

La mayor parte de los algoritmos implementados son de clave fija, es decir, para cifrar/descrifrar siempre toman como entrada una clave de una longitud determinada. Existen algoritmos, como el RC4 , que admiten longitudes de clave variable. Estos algoritmos vienen con ciertos valores por defecto que se obtienen con la ya estudiada estructura de datos EVP_CIPHER correspondiente. Así, por ejemplo, el valor devuelto por EVP_CIPHER_key_length (EVP_rc4()) sería 16 bytes y corresponde a la longitud de clave por defecto del citado algoritmo. La modificación de los parámetros se realiza a través de la estructura de contexto: EVP_CIPHER_CTX . Veamos cómo.


Las funciones de inicialización (la de encriptación y desencriptación) pueden ser llamadas en varias ocasiones para establecer los parámetros requeridos siempre y cuando la primera llamada especifique el type y las siguientes pongan este parámetro a NULL .

Como dijimos anteriormente, la modificación de los parámetros se realizará a través del contexto. La función que utilizaremos para tal fin se describe a continuación:

  • int EVP_CIPHER_CTX_set_key_length(EVP_CIPHER_CTX *x, int keylen);

La función devuelve 0 en caso de error y 1 si no lo hay. Como argumentos toma el contexto y la nueva longitud de clave a utilizar en bytes. Todo esto se verá mejor con el siguiente ejemplo.



Ejemplo 5. Establecemos la longitud de clave del RC4.

En general, también existen macros para los contextos que permiten extraer información acerca del mismo. En el ejemplo hemos utilizado EVP_CIPHER_CTX_key_length() y EVP_CIPHER_CTX_block_size() para extraer, respectivamente, el tamaño en bytes de la clave y del tamaño de bloque (obviamente del algoritmo que le asociamos en la función de inicialización).

Queremos recalcar más. si cabe, el hecho de que el establecimiento de parámetros se hace a nivel de contexto. Por este motivo la consulta hecha con EVP_CIPHER_key_length() devolverá la longitud de clave por defecto.


Longitud de clave efectiva y número de vueltas
en el RC2 y número de vueltas en el RC5

A este respecto la sección de ejemplos de la página de manual EVP_EncryptInit(3) es bastante clara y no parece necesario hacer ninguna aclaración al respecto.


anterior índice siguiente