Be a happy coder

You are here

Cifrado y descifrado con AES-128 entre PHP y .NET

05 Feb 2014

Recientemente me he encontrado con la necesidad de descifrar unos parámetros que me llegaban a través de la url, como parámetros GET. Aquí muestro un ejemplo de como obtener el dato enviado y como tratarlo para su posterior uso dentro de Drupal.

En primer lugar, AES (Advanced Encryption Standard), también conocido como Rijndael, es un esquema de cifrado por bloques. El algoritmo puede utilizar claves de longitud 128, 192 y 256 bits y una longitud de bloque de 128 bits. Además el algoritmo dispone de varios modos de operación de cifrado de bloque para lo cual requiere una secuencia binaria única conocida como Vector de inicialización (IV).

En .NET disponemos de la clase AesCryptoServiceProvider con la que podemos cifrar datos utilizando AES. Utilizando la siguiente función vamos a cifrar datos utilizando una clave de 128 y el modo de cifrado CBC:

public static string AESEncrypt(string text, string aesKey, string aesIV)
{
    // AesCryptoServiceProvider
    AesCryptoServiceProvider aes = new AesCryptoServiceProvider();
    aes.BlockSize = 128;
    aes.KeySize = 128;
    aes.IV = Encoding.UTF8.GetBytes(aesIV);
    aes.Key = Encoding.UTF8.GetBytes(aesKey);
    aes.Mode = CipherMode.CBC;
    aes.Padding = PaddingMode.PKCS7;

    // Convert string to byte array
    byte[] src = Encoding.UTF8.GetBytes(text);

    // encryption
    using (ICryptoTransform encrypt = aes.CreateEncryptor())
    {
    byte[] dest = encrypt.TransformFinalBlock(src, 0, src.Length);

    // Convert byte array to Base64 strings
    return HttpUtility.UrlEncode(Convert.ToBase64String(dest));
    }
}

Un detalle adicional es el "padding" o esquema de relleno. En un cifrado por bloques resuelve el hecho de que el tamaño del mensaje en claro no sea múltiplo del tamaño de bloque. El sistema de rellenado permite conseguir que el texto en claro "rellenado" sea múltiplo del tamaño del bloque. Habitualmente hay dos clases de padding: "zero/null byte padding" o PKCS#7. El null padding rellena el mensaje con caracteres nulos '\0' y el PKCS#7 añade el número total de bytes de padding. Por ejemplo, con PKCS#7 si nuestro mensaje es "ABC", quedaría con el siguiente relleno: 0x41 0x42 0x43 0x05 0x05 0x05 0x05 0x05.

Vamos con la parte Drupal. Supongamos que tenemos una entrada de menú "mypath" que admite un parametro "param" en el que nos llegará la información cifrada. Sabemos que la url que recibiremos será algo tal que así:

http://server.localhost/mypath?param=NMPszi%2fLjbhRmo3Q6Bo7gJK0ZZLDtw4cB...

El valor de param ha sido cifrado utilizando la functión anterior y posteriormente enviado a través de en la url, por lo que ahora debemos descifrarlo para poder usarlo.

En primer lugar tenemos que recoger y sanear lo que nos llega en la url, así que puesto que sólo esperamos texto plano utilizamos check_plain() y luego decodificamos la url:

function mymodule_page() {
  // Sanear y decodificar
  $param = rawurldecode(check_plain($_GET['param']));
  $decoded_param = mymodule_decrypt_value($param);
 
  [...]

El motivo de utilizar rawurldecode() en lugar de urldecode es porque una cadena encryptada es una secuencia de bytes que no se podrían representar bien por pantalla y que por tanto hay que codificar en base 64, pero esta codificación incluye signos especiales como la suma (+) y que urldecode() nos convertiría en un espacio al decodificar la url, con lo que el mensaje se perdería y no podríamos decodificarlo bien.

Echemos un vistazo a la función para decodificar:

function mymodule_decrypt_value($string, $method = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC) {
  // Si es una cadena vacía no hay nada que hacer
  if (empty($string)) {
    return FALSE;
  }

  $key = variable_get('mymodule_aes_key', '');
  $iv  = variable_get('mymodule_aes_iv', '');

  if (empty($key)) {
    watchdog('mymodule', 'No hay una clave para descifrar.', array(), WATCHDOG_ERROR);
    return FALSE;
  }

  if (empty($iv)) {
    watchdog('mymodule', 'No hay un vector de inicialización para descifrar.', array(), WATCHDOG_ERROR);
    return FALSE;
  }

  $plaintext_dec = mcrypt_decrypt($method, $key, base64_decode($string), $mode, $iv);

  // Eliminar el padding
  return mymodule_unpadPKCS7($plaintext_dec);
}

Tengo un formulario de configuración através del cual he almacenado la clave y el IV para el descifrado. Esta clave y el vector deben ser exactamente los mismos utilizados para cifrar, claro ;)

Utilizando la librería mcrypt que ya proporciona una implementación del algoritmo AES, le indicamos los parámetros adecuados y deshacemos el camino andado.

Por último, quitamos el padding que .NET ha añadido y ya tenemos nuestra cadena:

function mymodule_unpadPKCS7($data) {
  $last = substr($data, -1);
  return substr($data, 0, strlen($data) - ord($last));
}

Si queremos probar nuestra función de descifrado sin tener que usar la versión .NET, aquí tenemos la versión de cifrado en PHP:

function mymodule_encrypt_value($string, $method = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC) {
  if (empty($string)) {
    return FALSE;
  }

  $key = variable_get('mymodule_aes_key', '');
  $iv  = variable_get('mymodule_aes_iv', '');

  $block_size = mcrypt_get_block_size($method, $mode);
  $string = mymodule_padPKCS7($string, $block_size);

  $enc_text = base64_encode(mcrypt_encrypt($method, $key, $string, $mode, $iv));

  return $enc_text;
}

function mymodule_padPKCS7($data, $block_size) {
  $pad = $block_size - (strlen($data) % $block_size);
  $data .= str_repeat(chr($pad), $pad);
  return $data;
}