Leaky

31 marzo, 2014

Hace poco me he reencontrado con un término que conocí hace tiempo y que creo que tiene su gracia. Se trata de «leaky abstraction«, que vendría a traducirse como «abstracción goteante». Éste fue propuesto por Joel Spolsky  en un artículo hace más de una década (The Law of Leaky Abstractions) y la idea general es la siguiente: Como ingenieros, una de las herramientas básicas que utilizamos para simplificar la creación de arquitecturas complejas es la abstracción. Generamos diferentes capas o componentes que actúan como cajas negras con una interface, de manera que sea posible usarlas sin tener que conocer los detalles de la implementación. Por ejemplo, un sistema operativo es una abstracción del hardware donde se ejecuta, o las bibliotecas de clases contenedoras abstraen los detalles del almacenamiento de estructuras de datos en memoria, etc. Esta herramienta es muy importante de cara al mantenimiento de una arquitectura o que otros reusen nuestros componentes. Sin embargo, por muy buenas que sean nuestras intenciones, hay casos en los que intentar abstraerse totalmente del funcionamiento interno de la caja negra puede ser contraproducente. Si ocultamos totalmente este funcionamiento interno, con una abstracción al 100%, algunos resultados pueden parecer arbitrarios o incomprensibles.

En el artículo original, se usa como ejemplo la pila TCP/IP. Si un desarrollador intenta crear una capa TCP partiendo de la abstracción total de las capas inferiores IP y acceso a red, se va a encontrar con sorpresas sin saber el motivo. Desde su punto de vista, a veces una conexión irá muy rápida y otras más lenta, ya que IP puede redirigir los paquetes usando distintas rutas. A veces, súbitamente, dejará de funcionar sin motivo aparente. El servidor en el otro extremo está en marcha, pero a nivel de acceso a red alguien puede haber desenchufado el cable. Como todos estos aspectos están abstraídos, TCP no es «consciente», y no sabe prever como van a funcionar las cosas. Quizá el ejemplo no es perfecto (en el artículo original se explica más detalladamente), pero la idea general sí que tiene sentido. Por más que nos esforcemos en abstraer, algunos aspectos de implementación nos acaban afectando igualmente, independientemente de lo perfecto que sea la interfaz definida. La abstracción «gotea».

En el campo del desarrollo de aplicaciones criptográficas, que es con el que a menudo me peleo, las abstracciones resultan especialmente interesantes. Tras la mayoría de algoritmos criptográficos existe una base matemática que puede tener un cierto grado de complejidad. No siempre son conceptos intratables, sobretodo si, como ingenieros, partimos de una formación con background matemático. Pero no tiene ningún sentido que para que un desarrollador pueda proteger unos datos usando un algoritmo de cifrado, debamos conocer exactamente cómo funciona éste internamente, a nivel de permutaciones u operaciones lógicas. Esto nos ayuda bastante a concienciar al personal para crear aplicaciones seguras.

Basta con que el lenguaje de programación favorito proporcione unas bibliotecas criptográficas, o sea, una capa de abstracción, y el poder de un algoritmo de cifrado como AES «aparece» tras teclear unas pocas líneas de código. Esto no evita tener que conocer algunos detalles, como el hecho de que se trate de un algoritmo de clave simétrica de una longitud concreta. Pero no necesito saber qué caramba es un «SubBytes» o un «AddRoundKey». Solo necesito conocer el significado de los parámetros de la función o método de turno, como pasa en cualquier otra llamada de otra biblioteca, sin resultar imprescindible saber CÓMO se usa todo esto internamente.

La transformación AddRoundKey en AES

En este campo, sin embargo, también existen «abstracciones goteantes», algunas relativamente interesantes. El uso del algoritmo RSA para cifrar datos directamente es un caso con el que me he topado unas cuantas veces. Siempre me provoca una sonrisa cuando veo al compañero de turno, sin el background matemático pertinente en criptografía, maldecir sin saber qué está pasando. Dicho algoritmo de cifrado corresponde al tipo denominado de clave publica. Para usarlo se necesitan dos claves criptográficas en vez de una: la pública, que distribuyes libremente, y la privada, que como su nombre indica, guardas en secreto. Cuando alguien te quiere enviar datos seguros, los cifra con la pública. Para descifrarlos, lo haces con tu clave privada. Esta es una explicación simplificada a nivel conceptual, pero es suficiente para usarlo correctamente a través de las bibliotecas criptográficas de turno. No necesitas saber la matemática subyacente para nada… ¿o sí?

Sin entrar en profundidad en la matemática, la transformación de cifrado del RSA se basa en dos factores (pun intended). Por una parte, las entradas se tratan como valores enteros codificados en binario. Por otra parte, sobre dichos valores enteros se aplica una operación de exponenciación modular. El quid de la cuestión está en que, como su nombre ya indica, esta operación aplica un módulo sobre los datos a cifrar, lo que implica automáticamente que los datos se van a truncar si la longitud de éstos es superior a la del módulo. Concretamente, en la fórmula original, el módulo en cuestión equivale al número de bytes que se necesita para representar una clave RSA. Por ejemplo, con una clave RSA de 2048 bits el resultado jamás ocupará más de 256 bytes. Por lo tanto, esta será la longitud máxima de los datos que se pueden cifrar, el resto se perderán. Si no sabes estos detalles de la matemática tras el RSA, que quedan completamente ocultos por la capa de abstracción de la biblioteca criptográfica, lo único que verás es que los datos de salida a veces se trucan y a veces no. Como mucho, si miras con un poco de paciencia, puedes llegar a ver que este hecho depende de la longitud de los datos a cifrar y la de las claves.

Mirando el caso de Java, afortunadamente, en la últimas versiones ya produce un error si caes en esta trampa. Pero en versiones anteriores no tenía ningún problema en truncar datos alegremente, por sorpresa. De todos modos, sin saber como funciona matemáticamente RSA, que el método te diga que con una clave de 2048 bits no puedes cifrar más de 245 bytes parecerá una restricción absolutamente arbitraria. Sí, he dicho 245 bytes, ya que como nuevo detalle oculto de implementación, al valor máximo especificado por el módulo le resta 11 bytes para generar un «padding» en los datos resultantes. El caso es que la abstracción «gotea» por unos cuantos lados…

Si tenéis curiosidad, aquí os dejo un código de ejemplo de todo esto:

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import javax.crypto.Cipher;

public class LeakyRSA {

  //Longitud de la clave
  // 1024 = módulo de 128 bytes
  // 2048 = módulo de 256 bytes
  private static final int KEY_LEN = 2048;
 
  //Longitud de los datos (en vesriones actuales de Java, a partir de
  //(mòdulo - 11) dará error)

  private static final int DATA_LEN = 245;

  public static void main (String[] arges) throws Exception {

    //Generamos las dos claves
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
    keyGen.initialize(KEY_LEN);
    KeyPair keys = keyGen.genKeyPair();

    //Generamos datos al azar
    String data = "";
    for (int i = 0; i < DATA_LEN; i++) {
      data += (i%10);
    }

    //Ciframos datos
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding","SunJCE");
    cipher.init(Cipher.ENCRYPT_MODE, keys.getPublic());
    byte[] encryptedData =  cipher.doFinal(data.getBytes());

    //Desciframos datos
    cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate());
    byte[] resultData =  cipher.doFinal(encryptedData);

    //¿Qué ha pasado?
    System.out.println("ENTRADA - Longitud inicial = " + data.length());
    System.out.println(data + "n");

    String result = new String(resultData);
    System.out.println("SALIDA - Longitud final = " + result.length());
    System.out.println(result);
  }

}

Cabe decir que, para poder solventar esta limitación en la longitud de los datos de entrada, y otras que existen ligadas a la eficiencia del algoritmo, se usará la técnica llamada de «sobre digital«, basada en combinar cifrado simétrico y de clave pública.

La moraleja en esta historia es simple. Como ingenieros, no basta con quedarse en saber que las cosas funcionan. Es necesario que saber el por qué. ¡Ay!, si me dieran un euro por cada vez que he oído «Si yo sólo quiero programar ¿por qué tengo que aprender las matemáticas en las que se basa la criptografía?».

(Visited 27 times, 1 visits today)
Autor / Autora
Joan Arnedo Moreno
Comentarios
Deja un comentario