En este artículo vamos a ver como utilizar OpenSSL para poder cifrar un mensaje para un grupo de usuarios de forma que solo ellos, utilizando sus claves privadas puedan acceder al mensaje en texto plano.
La técnica es la misma que la utilizada para la encriptación E2E en chats de grupo y se basa en el uso de una clave de sesión y utilizar un método de cifrado híbrido. Realmente esta es la solución que se utiliza cuando se quiere cifrar un mensaje muy largo con un sistema de cifrado asimétrico. Así que matamos tres pájaros de un tiro.
¿Cómo funciona?
El método de funcionamiento es muy sencillo. E involucra los siguientes pasos:
- Elegimos un algoritmo de cifrado simétrico como por ejemplo AES-256-CBC. Este es el ejemplo que usaremos en el artículo, pero podéis elegir el algoritmo que queráis.
- A continuación elegimos los datos necesarios para poder usar ese algoritmo. Como mínimo una clave de cifrado y, dependiendo del algoritmo usado, un vector de inicialización. Esta será la clave de sesión.
- Ahora ciframos la clave de sesión, utilizando la clave pública de cada una de las persona que queremos que puedan leer el mensaje.
- Finalmente ciframos el mensaje usando el algoritmo de cifrado simétrico y la clave de sesión que hemos generado.
Con este esquema, además del mensaje cifrado debemos transmitir la clave de sesión cifrada con la clave pública de cada destinatario y cualquier otra información extra, como por ejemplo en vector de inicialización que no se considera información sensible y se puede enviar en plano.
Generando las claves
Lo primero que vamos a hacer es generar un conjunto de claves con las que comprobar que nuestro programa de cifrado para múltiples destinatarios funciona correctamente:
$ openssl genrsa -out clave_privadaALICE.pem 2048 $ openssl genrsa -out clave_privadaBOB.pem 2048 $ openssl genrsa -out clave_privadaCAROL.pem 2048
La clave privada solo la conocerán los usuarios en cuestión: Alice, Bob y Carol. Pero nosotros necesitamos la clave pública para poder cifrar mensajes para este grupo de personas. Así que extraemos las claves públicas usando los siguientes comandos.
$ openssl rsa -in clave_privadaALICE.pem -pubout > clave_publicaALICE.pem $ openssl rsa -in clave_privadaBOB.pem -pubout > clave_publicaBOB.pem $ openssl rsa -in clave_privadaCAROL.pem -pubout > clave_publicaCAROL.pem
Podéis echar un ojo a este artículo para refrescar todo esto de las claves, como generarlas y como manipularlas.
Antes de empezar a programar, necesitamos una cosa más. Un formato para nuestros mensajes.
Generando un formato para nuestros mensajes
Las funciones de OpenSSL que vamos a utilizar nos van a facilitar el proceso que hemos descrito al principio del artículo, pero necesitamos poner toda la información junta en un solo bloque que podamos llamar mensaje cifrado para transmitirlo y recibirlo.
Una posibilidad es utilizar CMS o Cryptographic Message Syntax (Sintaxis de Mensaje Cryptográfico). Este es un formato que nos permitiría generar nuestro mensaje criptográfico de forma estándar. Otra posibilidad es utilizar el formato OpenPGP recogido en el RFC 4880, sin embargo ambos formatos son bastante complejos y se nos iría el artículo en discutir los detalles básicos para comprender como utilizarlos (cosas como certificados X.509 de los que todavía no hemos hablado), así que por el momento vamos a crearnos un formato propio muy sencillo con el que poder jugar con la idea de cifrado para múltiples destinatarios.
Lo que vamos a hacer es simplemente almacenar toda la información que necesitamos junto con el mensaje. Además vamos a considerar que utilizaremos claves RSA 2048 y AES-256 para el cifrado simétrico, de forma que no tengamos que almacenar información sobre algoritmos y longitud de claves…. Vamos más fácil no se puede.
El mensaje constará de los siguientes campos:
uint16_t version : Version del formato (1.0 o 0x0100) uint8_t iv[256] : Vector de inicialización uint8_t n : Número de destinatarios
A continuación de la cabecera incluiremos la lista de destinatarios que almacenaremos en bloques como el que sigue:
unsigned char id[16] : Identificador del destinatario uint8_t key[256] : Clave simétrica cifrada con RSA
Para simplificar la cosa y no tener que lidiar con campos de longitud variables, los cuales solo harían el código más complejo y largo, hemos decidido identificar a los posibles destinatarios con nombres de 16 caracteres. En un sistema real, deberíamos almacenar algo más diferenciador, como una dirección de correo o similar.
Tras esta metainformación añadiremos una marca de separación para poder identificar fácilmente donde empieza el mensaje cuando miremos a los volcados hexadecimal (esto lo hemos tenido que hacer un par de veces mientras escribíamos el programa) y el tamaño del mensaje original.
Así que, para implementar este formato de fichero necesitaremos las
siguientes estructuras de datos que pondremos en un fichero llamado
mr_msg.h:
#ifndef MR_MSG_H
#define MR_MSG_H
#include <stdint.h>
/* FORMATO FICHER
* MR_MSG_HDR -> Cabecera
* MR_MSG_DEST -> Lista de destinatarios segun valor de n en cabecera
* ...
* MR_MSG_DEST
* ---------------- (Marca 16 bytes)
* Size (off_t)
* Mensaje cifrado
*/
#ifdef __cplusplus
extern "C" {
#endif
#define N 32 // Número máximo de destinatarios.
typedef struct dest_t {
unsigned char id[16]; // Identificador de destinatario
uint8_t key[256]; // Clave privada para ese destinatario
} MR_MSG_DEST;
typedef struct mr_msg_hdr_t {
uint16_t version; // Version del fichero
uint8_t iv[16]; // Vector de inicialización para AES-256
uint8_t n; // Número de destinatarios
} MR_MSG_HDR;
#ifdef __cplusplus
}
#endif
#endifAhora que ya tenemos nuestro formato de fichero definido, vamos a obtener la información que vamos a necesitar para poder cifrar el mensaje.
Obteniendo la información para el cifrado
Antes de poder cifrar nuestro mensaje tenemos que recabar toda la información necesaria e inicializar unas cuantas estructuras. En concreto tendremos que hacer lo siguiente:
- Crear el cifrador simétrico a utilizar (AES-256 en este caso).
- Leer las claves públicas de todos los destinatarios.
- Reservar memoria para almacenar la clave de sesión cifrada con RSA para cada destinatario.
Crear el cifrador simétrico es muy fácil. Ya lo hemos hecho en entregas anteriores de esta sección:
const EVP_CIPHER *type;
type = EVP_aes_256_cbc();Esto lo podríamos hacer más tarde, de hecho no lo necesitamos hasta que empiece el proceso de cifrado. Esa es una sencilla modificación que podéis incorporar para hacer el código más eficiente.
A continuación tenemos que leer las claves públicas y para poder usar
el formato de mensaje que hemos definido, el identificador de usuario
asociado a esa clave pública. Así que, como identificador de usuario
vamos a usar el nombre que dimos al crear las claves. Así,
clave_publicaALICE.pem sería la clave pública del usuario
ALICE.
Una vez más, para mantener el código sencillo y centrarnos en el uso
de OpenSSL, vamos a crear un array estático de N elementos
(definido en mr_msg.h como 32) para almacenar los nombres y
claves, en lugar de crearlos dinámicamente según los argumentos
recibidos a través de la línea de comandos. Hablando de la línea de
comandos, vamos a asumir que el programa espera los siguientes
parámetros:
programa fichero_entrada fichero_salida ID1 ID2 ...
Es decir, recibimos como primer parámetro el fichero con los datos a cifrar. En el segundo el nombre del fichero donde almacenaremos el mensaje cifrado y a continuación una lista de identificadores de usuarios para los que cifrar el mensaje.
Con todo esto, el siguiente fragmento de código nos permite leer las claves privadas e identificadores de todos los usuarios indicados en el línea de comandos:
unsigned char *id[N];
EVP_PKEY *pubk[N]; //Claves publicas
int npubk; // Número de claves públicas
OSSL_DECODER_CTX *dctx = NULL;
...
// Leemos claves públicas
npubk = 0; // Contendrá el número de claves públicas leidas
for (int i = 0; i < argc - 3; i++) {
char fname[1024];
dctx = OSSL_DECODER_CTX_new_for_pkey (&pubk[i], "PEM", NULL, "RSA",
EVP_PKEY_PUBLIC_KEY,libctx, NULL);
// Lee identificador y clave publica
id[i] = strndup (argv[3 + i], 16);
snprintf (fname, 1024, "clave_publica%s.pem", argv[4 + i]);
printf ("Leyendo clave publica (%s): '%s'\n", argv[4 + i], fname);
FILE *f = fopen (fname, "rb");
OSSL_DECODER_from_fp(dctx, f);
fclose (f);
OSSL_DECODER_CTX_free (dctx);
npubk++;
}El código ya lo hemos visto antes cuando aprendimos a cifrar información con RSA, simplemente observad que todo esta movido 3 posiciones para poder saltar los tres primeros elementos de la línea de comando: programa, argumento1 y argumento2.
Para terminar la inicialización solo nos faltaría reservar memoria para almacenar las claves cifradas para cada usuario. Esto es un requisito de las funciones OpenSSL que vamos a usar y la que veremos en la siguiente sección. El código no tiene mucho misterio. Reservamos memoria para un array de claves y luego reservamos memoria para almacenar cada clave. Puesto que estamos usando RSA 2048, el tamaño del mensaje resultante del cifrado asimétrico será 256 bytes.
ek = malloc (sizeof (unsigned char*) * npubk);
for (int i = 0; i < npubk; i++) {
int size = EVP_PKEY_get_size(pubk[i]);
ek[i] = malloc (size);
memset (ek[i], 0, size);
}Super fácil. Continuemos inicializando la cabecera de nuestro mensaje.
Preparando la cabecera del Mensaje
En este punto ya tenemos toda la información necesaria para generar la cabecera del mensaje cifrado. Esto lo podemos hacer con el siguiente fragmento de código:
MR_MSG_HDR *msg;
// Genera cabecera del mensaje
msg = malloc (sizeof(MR_MSG_HDR) + npubk*sizeof(MR_MSG_DEST));
msg->version = 0x0100;
msg->n = npubk;
memset (msg->iv, 0, 16);Para ello reservamos un bloque de memoria en el que poder almacenar la cabecera y la lista de destinatarios y tras ello inicializamos los campos que ya conocemos como la versión del formato y el número de destinatarios que vamos a utilizar.
Aprovechamos para inicializar el vector de inicialización. Esta operación no es realmente necesaria pero nos vino bien durante el desarrollo para asegurarnos que se estaba generando un vector de inicialización aleatorio cada vez que ejecutábamos el programa. La verdad es que la documentación de OpenSSL es un poco difícil de digerir y no era obvio como funcionaba todo esto así que tuve que hacer algunas pruebas… No os voy a aburrir con los detalles, solo para justificar porque inicializamos el IV… aunque hubiese sido más fácil borrar la línea del bloque de código anterior :).
Tanto el vector de inicialización como las claves públicas cifradas con RSA las genera la función que vamos a utilizar en un segundo.
Sellando mensajes
Para el cifrado de mensaje vamos a utilizar un conjunto de funciones
de OpenSSL para el sellado de mensajes las cuales, mira tu que
casualidad, hacen exactamente lo que nosotros queremos. Estas funciones
forman parte de la librería EVP que hemos utilizado varias
veces ya.
Esta sería la forma de llamar a la función de inicialización del proceso de sellado.
EVP_CIPHER_CTX *ctx; // Contexto de cifrador
ctx = EVP_CIPHER_CTX_new();
int r = EVP_SealInit (ctx, type,
ek, &ekl, msg->iv,
pubk, npubk);Como sucede con todas las funciones EVP_ de OpenSSL, lo
primero que debemos pasarle es un contexto. En este caso un contexto de
cifrado. A continuación le pasamos la cifrador simétrico que deseamos
usar. En este caso lo inicializamos al principio del programa y no ha
sido necesario realizar ninguna configuración adicional, así que
podríamos sustituir type por una llamada directa a
EVP_aes_256_cbc() como segundo argumento a la función.
A continuación le pasamos el array donde queremos que se generen las
claves de sesión cifradas con la clave pública de cada destinatario. La
función realizará este proceso y almacenará el resultado en el array
ek, actualizando ekl con el número de claves
generadas.
La función también genera un vector de inicialización adecuado para el cifrador utilizado, para ello es necesario pasarle un buffer en el que almacenar dicho vector de inicialización. En este caso le pasamos directamente el campo en la cabecera de nuestro mensaje.
Por último, la función necesita la lista de claves públicas con las que cifrar la clave de sesión que se utilizará, finalmente, para cifrar el mensaje. Esta clave de sesión se genera de forma aleatoria dentro de la función y se cifra usando RSA y las claves públicas proporcionadas.
Como podéis ver, esta función hace prácticamente todo por nosotros, desde generar la clave y el IV a cifrarla.
Ahora que tenemos todas las claves cifradas, podremos añadirlas a la sección de metadatos de nuestro mensaje:
// Incluye claves secretas e identidades
MR_MSG_DEST *d = (MR_MSG_DEST*)((unsigned char*)msg + sizeof (MR_MSG_HDR));
for (int i = 0; i < npubk; i++) {
memcpy (d->id, id[i], 16);
memcpy (d->key, ek[i], 256);
d++;
}Para ello, calculamos la dirección en la que empezaría la zona de
destinatarios de nuestro mensaje, es decir, justo tras la cabecera, y
simplemente copiamos los datos de identidad y las claves generadas en
esa región de memoria. Observad que nos aprovechamos de la aritmética de
punteros para simplemente incrementar d para apuntar a la
siguiente entrada del array de destinatarios en el mensaje.
Leyendo el mensaje
Ahora solo nos falta leer el mensaje y cifrarlo. Esto se parece mucho
a todo lo que hemos estado haciendo durante toda esta serie, pero
utilizando la función EVP_SealUpdate. El código sería algo
así:
unsigned char in[1024];
unsigned char out[1024];
int inl, outl;
struct stat st;
stat (argv[2], &st);
FILE *f = fopen (argv[2], "rb");
FILE *f1 = fopen (argv[3], "wb");
// Escribimos Cabecera y metadatos
fwrite (msg, 1, sizeof(MR_MSG_HDR) + npubk*sizeof(MR_MSG_DEST), f1);
fprintf (f1, "----------------");
printf ("Fichero de entrada 0x%x bytes\n", st.st_size);
fwrite (&st.st_size, 1, sizeof (off_t), f1);
int out_len = 0;
while (!feof (f)) {
if ((inl = fread (in, 1, 1024, f)) <= 0) break;
EVP_SealUpdate (ctx, out, &outl, in, inl);
fwrite (out, 1, outl, f1);
out_len += outl;
}
// Escribiendo datos finales
EVP_SealFinal (ctx, out + out_len, &outl);
fwrite (out, 1, outl, f1);
fclose (f);
fclose (f1);Lo primero que hacemos es abrir los dos ficheros, el que contiene el
mensaje de entrada y en el que almacenaremos el mensaje cifrado
resultante. Obtenemos el tamaño del fichero de entrada y lo grabamos en
el fichero de salida tras la cabecera, que ya habíamos generado en
memoria, y una marca de 16 - para identificar donde empieza
el mensaje cifrado. Podéis eliminar esta marca si queréis, yo solo la
necesité para la depuración del programa.
Una vez que los dos ficheros están preparados simplemente leemos
datos de la entrada, los pasamos por EVP_SealUpdate y el
resultado lo grabamos en la salida. Como siempre hacemos una llama extra
a EVP_SealFinal para volcar los últimos datos y lidiar con
el padding si fuera necesario.
Observad que estas dos funciones, realmente están haciendo un cifrado
simétrico usando AES-256-CBC con una clave generada aleatoriamente por
EVP_SealInit.
Descifrando
El proceso de descifrado es bastante más sencillo, necesitaremos hacer lo siguiente.
- Obtener del usuario el identificador de usuario y su clave privada.
Utilizaremos el mismo esquema que para el cifrado, el usuario
IDalmacena su clave privada enclave_privadaID.pem. - Localizar el identificador de usuario en el mensaje
- Descifrar el mensaje usando la clave y el IV incluido en el fichero.
Veamos cada una de estas tareas por separado.
Leyendo Identidad y Clave
El programa de desencriptado recibirá como parámetros la identidad a utilizar para leer el mensaje y el fichero conteniendo el mensaje. Así que lo primero que hacemos es almacenar la identidad y leer la clave privada asociada a esa identidad:
OSSL_LIB_CTX *libctx = NULL;
OSSL_DECODER_CTX *dctx = NULL;
unsigned char id[16];
EVP_PKEY *priv_key;
strcpy (id, argv[1]);
printf ("Usando identidad: %s\n", id);
dctx = OSSL_DECODER_CTX_new_for_pkey (&priv_key, "PEM", NULL, "RSA",
EVP_PKEY_KEYPAIR,libctx, NULL);
// Lee clave privada
char fname[1024];
snprintf (fname, 1024, "clave_privada%s.pem", id);
FILE *f = fopen (fname, "rb");
OSSL_DECODER_from_fp(dctx, f);
fclose (f);
OSSL_DECODER_CTX_free (dctx);Este código es idéntico al usado por el programa de cifrado, pero leyendo la clave privada en lugar de la pública.
Localizando identidad
Ahora debemos localizar la identidad proporcionada a través de la
línea de comandos en el fichero de mensaje, para ver si el usuario
identificado con ese id puede leer el mensaje. Para ello
debemos abrir el fichero de mensaje y recoger la lista de identidades
almacenadas en él. O si lo preferís, buscar la clave asociada a dicha
identidad.
Para hacer esto, vamos a leer todo el mensaje en memoria y trabajar desde ahí. Para el caso más general en el que el mensaje cifrado puede ser muy grande quizás habría que leer los datos en bloques.
Primero leemos todo el mensaje en memoria:
unsigned char *buf;
struct stat st;
stat (argv[2], &st);
FILE *fmsg = fopen (argv[2], "rb");
buf = malloc (st.st_size);
fread (buf, 1, st.st_size, fmsg);
fclose (fmsg);Y ahora podemos buscar la identidad en cuestión en memoria de forma muy sencilla.
MR_MSG_HDR *msg;
msg = (MR_MSG_HDR*) buf;
MR_MSG_DEST *d = (MR_MSG_DEST*)(buf + sizeof (MR_MSG_HDR));
MR_MSG_DEST *target = NULL;
for (i = 0; i < msg->n; i++) {
printf ("+ Procesando Destinatario: %s\n", d->id);
if (!strncmp (d->id, id, 16)) {
target = d;
break;
}
d++;
}Lo primero que haremos será convertir el buffer que contiene el
mensaje en una estructura del tipo MR_MSG_HDR para obtener
el número de destinatarios incluidos en el mensaje. Luego definimos un
puntero de tipo MR_MSG_DEST que apunte justo después de la
cabecera, donde comienza la lista de destinatarios y recorremos la lista
buscando el identificador de usuario que obtuvimos a través de la línea
de comandos.
Al terminar el bucle, target contendrá un puntero al
destinatario que nos interesa o NULL si el destinatario
indicado en la línea de comandos no está autorizado a leer este
mensaje.
NOTA:Sería mejor leer la clave privada después de determinar si la identidad es valida, puesto que en el caso de que no lo sea la clave no sirve para nada.
Descifrado
Ahora que tenemos toda la información disponible podemos decifrar el mensaje. Como ocurría con el proceso de cifrado (o sellado como lo llama OpenSSL), excepto por la función de inicialización, el resto de llamadas son equivalente a realizar un desencriptado usando un algoritmo simétrico.
La función de inicialización por su parte, es la encargada de descifrar la clave de sessión asociada al usuario en cuestión usando su clave privada. Veamos que pinta tiene la llamada a esta función.
ctx = EVP_CIPHER_CTX_new();
// Desencriptar clave secreta
EVP_OpenInit (ctx, EVP_aes_256_cbc(),
target->key, 256, msg->iv,
priv_key);Como podéis ver en esta ocasión le estamos pasando el cifrador
simétrico directamente como segundo argumento, tal y como os comentamos
cuando discutimos la función de sellado. Además de eso, le pasamos la
clave simétrica de sesión cifrada con RSA target->key,
el tamaño de la clave que será 256 ya que utilizamos RSA-2048 (2048/8 =
256), el vector de inicialización que almacenamos en la cabecera
msg->iv y la clave privada de la identidad que pasamos
como parámetro.
Esta función descifrará, usando RSA, la clave de sesión almacenada en
target->key y la asociará, junto con el valor del IV al
cifrador que indicamos. A partir de este punto estaremos descifrando un
mensaje AES-256 normal y corriente.
unsigned char *crypt= buf + sizeof (MR_MSG_HDR) +
msg->n*sizeof (MR_MSG_DEST) + 16;
int plain_len, len, cryptl = *((off_t*)crypt);
unsigned char *plain = malloc (cryptl);
memset (plain, 0, cryptl);
plain_len = 0;
len = cryptl;
printf ("Tamaño Mensaje : 0x%x\n", cryptl);
crypt += sizeof (off_t);
EVP_OpenUpdate (ctx, plain, &len, crypt, cryptl);
plain_len += len;
EVP_OpenFinal (ctx, plain+plain_len, &len);
plain_len += len;
printf ("Mensaje(%d):\n'%s'\n", plain_len,plain);El código no tiene nada especial, simplemente llamamos a
EVP_OpenUpdate y EVP_OpenFinal como lo
haríamos descifrando un mensaje con clave simétrica. Observad que el
mensaje cifrado ya lo teníamos en memoria puesto que leímos todo el
fichero de una vez, así que solo necesitamos un puntero a la zona de
memoria en la que empieza. Esto es saltando la cabecera, la lista de
destinatarios, la marca y el tamaño de fichero que almacenamos justo
antes del mensaje.
Probando el sistema
Para terminar, vamos a probar nuestro sistema de cifrado de mensaje para múltiples destinatarios. Podéis encontrar el código completo en nuestro github.
Para generar un mensaje cifrado para ALICE y BOB utilizaríamos el siguiente comando:
$ ./sellado entrada.txt salida.enc ALICE BOB
Esto generaría el fichero salida.enc que podemos leer
utilizando el programa de descifrado de la siguiente forma:
$ ./descifrado BOB salida.enc Usando identidad: BOB Leyendo clave publica (BOB): 'clave_privadaBOB.pem' Mensaje : 812 bytes Formato version 100 Número de receptores: 2 + Procesando Destinatario: BOB Destinatario BOB encontrado Tamaño Mensaje : 0xd1 Mensaje(208): 'Este es un mensaje secreto cifrado con una clave simétrica que ha sido cifrada con RSA para enviar a varios destinatatios. Mola que no? El mismo mensaje esta cifrado para distintos usuarios simultaneamente'
Como podéis ver hemos añadido algunos mensajes extra a la salida del programa tanto para el proceso de depuración, como para hacerlo más güay ;).
Si echamos un ojo al fichero, veremos algo como esto:
$ xxd kk
00000000: 0001 242d a610 b4aa 092f 7ff3 4ecf b555 ..$-...../..N..U
00000010: 850e 0200 424f 4200 0500 0000 0000 0000 ....BOB.........
00000020: 0000 0000 bb34 4d60 7920 3cb4 101b 71d9 .....4M`y <...q.
00000030: cc05 5ea4 b29e 883e a6f0 6754 2fcc 6d1e ..^....>..gT/.m.
00000040: 12f4 38bf d046 f8d8 8dc7 80d0 a17e b343 ..8..F.......~.C
00000050: 9c16 15cc 562e 90dc 0cba 5a51 a793 5ec7 ....V.....ZQ..^.
00000060: 1a0b ade9 2b93 d197 3c9f ba12 eccb d82e ....+...<.......
00000070: 39c4 3304 4b97 c910 b824 05d7 6119 5199 9.3.K....$..a.Q.
00000080: 9849 03ac 083a 2435 118b ade2 3f57 9249 .I...:$5....?W.I
00000090: 05d5 881d 1bbc c445 769b 0646 bb49 8e0e .......Ev..F.I..
000000a0: 7540 7a6f 6ef9 9ddd b8eb 605e 1b49 5918 u@zon.....`^.IY.
000000b0: e466 d1bb 416d 25cf 6aa7 37df cd5a 547f .f..Am%.j.7..ZT.
000000c0: 7d0f 8c46 3dda 409b ecc9 4d3e 74fc 529b }..F=.@...M>t.R.
000000d0: 06d3 a27a b313 0f5f dede eaf3 a8cc 5f7d ...z..._......_}
000000e0: 6c73 58a0 e2c2 defa 607e 89f8 7b13 02e1 lsX.....`~..{...
000000f0: c78f 4d7c 07d2 8620 8ee2 30dc 98a6 e6ad ..M|... ..0.....
00000100: 01e4 78b7 e92f 78bb 2858 25a6 c938 b2d0 ..x../x.(X%..8..
00000110: 8f93 e783 ec09 dbf0 8d9c c935 8844 a5e7 ...........5.D..
00000120: c5ed 050d 414c 4943 4500 0000 0000 0000 ....ALICE.......
00000130: 0000 0000 37ac 3703 5ae0 3008 6577 0f1f ....7.7.Z.0.ew..
00000140: c0e7 af9c 0ac7 15bb 0dbe 59e4 c59e 41bd ..........Y...A.
00000150: 2d62 5ab4 6fc1 487c eb77 5f0a 5ca4 9b12 -bZ.o.H|.w_.\...
00000160: 8ea6 4260 a0b1 6028 c5cb 831f beb8 fc0d ..B`..`(........
00000170: f687 8202 2644 0ae4 6894 1196 ecef 24a5 ....&D..h.....$.
00000180: ba12 bb15 cf5f 2fb5 ced5 3b49 5796 cae7 ....._/...;IW...
00000190: b56a 7bcb f01d fc22 e7a3 3abb 8ed2 f0dc .j{...."..:.....
000001a0: 554e 8ec5 3f23 cbd3 2d91 990f 2c42 ee8f UN..?#..-...,B..
000001b0: 69a2 72c2 acba cf49 dc04 0a78 d8f0 4487 i.r....I...x..D.
000001c0: 6fc9 b499 f412 7ed4 4571 c036 8b5d 4256 o.....~.Eq.6.]BV
000001d0: 7a89 d32f 5fd9 e334 d638 dbec b77d 5b79 z../_..4.8...}[y
000001e0: 36b4 6b3f c6dc 3325 261c aec4 5bbf b964 6.k?..3%&...[..d
000001f0: b806 97be 586f 2eab ed96 aea2 c7ec 7ac3 ....Xo........z.
00000200: b767 08be 376b b6ac e926 d28d f5b8 c568 .g..7k...&.....h
00000210: be57 5637 aa03 9f34 32c3 b53e 87ec bf71 .WV7...42..>...q
00000220: bf26 7fed 1595 90c3 7938 6850 2716 e171 .&......y8hP'..q
00000230: 6d06 c02d 2d2d 2d2d 2d2d 2d2d 2d2d 2d2d m..-------------
00000240: 2d2d 2d2d d100 0000 0000 0000 8a54 3409 ----.........T4.
00000250: 1f87 0f4b 9b75 fc01 9d4e 9ee8 c5b0 60ce ...K.u...N....`.
00000260: 5f75 3c51 8544 9971 5f8f 17bd ad0b 143b _u<Q.D.q_......;
00000270: b21c a079 b836 51e8 4206 74ea f0b4 b9e5 ...y.6Q.B.t.....
00000280: 144d 70ea c0c8 54fd 9a71 1bbd 64b6 2135 .Mp...T..q..d.!5
00000290: 3f3f 26f0 588d 514d e699 57ec f1d3 2955 ??&.X.QM..W...)U
000002a0: 1c38 3fda 38b0 5793 490b 2460 0930 ed1a .8?.8.W.I.$`.0..
000002b0: c1af 495a a808 2b24 35b1 4798 0c3e 30fb ..IZ..+$5.G..>0.
000002c0: 2353 6056 dbae a81f 7c1f 31d6 1ced 0363 #S`V....|.1....c
000002d0: 8353 4caa bd37 d4b5 a083 f289 e1b8 6575 .SL..7........eu
000002e0: 6907 dcce fb2a 8050 3e93 b0b2 e3ef 1385 i....*.P>.......
000002f0: a4c6 91d7 fdcc a779 32cd 39f5 9d18 1288 .......y2.9.....
00000300: a7cd 957b d644 03be fd0d 08d4 6451 9c00 ...{.D......dQ..
00000310: 53a5 91cb fe0f 5ad7 da2a b833 8a54 3409 S.....Z..*.3.T4.
00000320: 1f87 0f4b 9b75 fc01 9d4e 9ee8 ...K.u...N..
En el volcado anterior podéis ver claramente los identificadores de
usuarios y la marca de los 16 - que separan el mensaje de
la cabecera… realmente debería haber puesto el tamaño del fichero antes
de la marca. Ahora sabéis porque el programa de descifrado muestra el
tamaño de mensaje en hexadecimal… para comprobarlo fácilmente en un
volcado como el de arriba. Observad que el tamaño del fichero se ha
almacenado como un long por lo que tenemos 7 ceros junto al
valor 0xd1.
La primera palabra de 16 bits del fichero es la versión
(0x0100 o 1.0 si lo preferís). Y a continuación, antes del
primer destinatario tenemos el vector IV (16 bytes) y el número de
destinatarios (2 en este caso).
Como podéis ver en este caso, necesitamos mucho más espacio para almacenar los metadatos que la información en sí. En concreto, necesitamos 272 bytes por destinatario que incluyamos más el valor de la cabecera que son unos 20 bytes.
Conclusiones
En este artículo hemos visto como cifrar datos de forma que varias personas puedan leer el mensaje usando sus respectivas claves privadas. El cifrado en si es muy sencillo usando OpenSSL, pero hemos tenido que definir un formato de fichero para poder transmitir la información entre sistemas. El programa que hemos escrito tiene algunos problemas si se pretende utilizar en el mundo real, como soporte de sistemas Little/Big Endian o alineamiento de memoria. Os dejamos esos puntos para que os entretengáis. Nos vemos en el próximo número.
■
