Desmitificando la Encriptación (ex MTJ.NET)

Aclaración

El siguiente contenido apareció publicado en la revista MTJ.NET en MSDN en español en el año 2005, en partes I y II. Debido a que la revista MTJ.NET no está al aire hoy, he optado por lanzarlo al aire nuevamente desde mi blog en un sólo documento.

A los suscriptores RSS les pido disculpas de antemano por haber publicado diferentes versiones.

Código fuente disponible aquí.

Introducción

Cuando es necesario darle seguridad a ciertos datos de nuestra aplicación, y buscamos información en el Web que nos permita lograrlo, es normal que hablemos (o leamos) de encriptación, y que irremediablemente salgan a la luz palabras tales como algoritmos simétricos, asimétricos y Hash. Estos términos pueden generar temor o dar la sensación de ser complejos. La motivación de este documento es mostrar qué es la encriptación sin necesidad de profundizar en cómo funciona un algoritmo internamente; y también indicar las malas prácticas que se realizan popularmente. Se revisará además la forma correcta de utilizar los algoritmos de encriptación, dónde se deben usar y dónde no.

Debido a lo extenso del tema en cuestión y a que es preferible realizar todas las explicaciones correspondientes, fue necesario dividir el documento en dos partes. Lo que contendrá cada parte está detallado en el siguiente índice.

Agradecimientos

Antes de comenzar deseo agradecer tanto el apoyo de Mike Giagnocavo como el de la información disponible en su blog (http://www.atrevido.net/). Sin sus consejos y correcciones, los ejemplos de este documento y la demostración no habrían quedado de excelente calidad y listos para utilizar.

Introducción, alcance y preguntas iniciales

La seguridad de los datos de cualquier sistema es algo muy importante, y no siempre recibe la atención y dedicación necesarias. Podemos formular las siguientes preguntas básicas y es probable que encontremos falencias de seguridad que pueden comprometer a nuestro sistema:

  • ¿Se está almacenando la información crítica de forma segura?
  • ¿Qué entendemos por seguro?
  • Si se está encriptando, ¿Qué algoritmo se está utilizando?
  • ¿Se permite la desencriptación de datos que no debieran poder desencriptarse?
  • ¿Cómo está realizándose la encriptación?

Adicionalmente, podemos complementar con preguntas más complejas:

  • ¿Dónde están almacenadas la o las llaves?
  • ¿Con qué frecuencia se cambian las llaves?
  • ¿Cómo se enfrenta el problema de cambiar las llaves con tanta frecuencia?
  • ¿Con qué criterio se definen las nuevas llaves?

Es probable que algunas de las preguntas básicas no sean respondidas adecuadamente. Si esto es así, sería recomendable hacer una revisión de los criterios y metodologías que se están utilizando, y determinar si es necesario hacer modificaciones o rehacer esas funcionalidades. Si el pensamiento es que esto no genera utilidad, cabe hacerse las siguientes preguntas: ¿Qué le sucedería al negocio si alguien accediese a cualquiera de los datos privados del sistema? ¿Se verá comprometida la seguridad con hechos como éste? De seguro, en ese momento el pensamiento sería que valdría la pena. Entonces, manos a la obra.

El alcance de este documento es dar a conocer los distintos métodos que existen para proteger tanto el almacenamiento como el envío de la información. Este documento NO tiene como intención ser un manual de seguridad. Esto es lo mínimo que debe tener una organización para no estar al descubierto en sus procesos. Se debe entender que existen libros dedicados a la seguridad. Aquí se está danto el puntapié inicial para salir del desconocimiento que existe sobre este tema. Es responsabilidad tanto de los administradores  de sistemas operativos como del arquitecto de soluciones definir las reglas, dónde almacenar sus llaves, los criterios de cambio de llaves y también los lugares donde almacenará la información.

Habiendo introducido el tema y definido el alcance, comencemos con la revisión de los algoritmos y los distintos tipos que existen.

Definición de algoritmos

Como se mencionó con anterioridad, existen distintos tipos de algoritmos de encriptación, pero algo que no se ha mencionado hasta ahora son los “algoritmos” que se cree que encriptan, pero que no hacen nada más que transformar texto (o información). El objetivo de hacer esta distinción es mostrar algunas malas prácticas que se cometen, pensando que cualquier algoritmo que cambia de cierta forma un texto, está realizando encriptación de datos. Estos algoritmos podremos llamarlos simplemente “Transformaciones”.

Como se está hablando de encriptación, se revisarán también los algoritmos que han sido construidos con distintos fines y que están agrupados en distintas categorías. ¿Cuál es la funcionalidad de los algoritmos dentro de cada uno de estos grupos? Cada uno tiene un objetivo y un problema que atacar, y por lo mismo hay casos en los que debe utilizarse uno y sólo uno de estos algoritmos, como también casos en los que debe utilizarse algoritmos de más de un grupo.

Antes de revisar los escenarios, se debe aclarar el concepto de llave. La llave de encriptación es una serie de carácteres, de determinado largo, que se utiliza para encriptar y desencriptar la información que se quiere proteger. El largo de la llave depende del algoritmo. Existen llaves privadas y públicas, que serán detalladas más adelante. Veamos los siguientes escenarios.

  • Escenario 1: Necesito almacenar información crítica que deberá poder descifrarse, y seré yo el único que haga todo el proceso. Nadie más tendrá acceso a la llave con que se encriptará y desencriptará la información.
  • Escenario 2: Necesito que me envíen información crítica que yo desencriptaré posteriormente, pero necesito que las personas que me van a enviar información pueden encriptarla libremente, pero no desencriptarla. En este caso, se deja disponible una llave pública para que ellos encripten y yo tendré mi llave privada de encriptación en forma segura.
  • Escenario 3: Necesito almacenar o enviar información crítica de forma segura, pero que no requerirá ser desencriptada para su validación, o que es extremadamente importante verificar que no haya sido modificada en el camino.

Estos son los tres escenarios más comunes. Es probable que te preguntes para qué puede haber casos como el 3. Lo veremos más adelante con ejemplos, destacando ventajas y desventajas. Los tres escenarios calzan con los algoritmos listados a continuación:

  • Escenario 1: Encriptación simétrica
  • Escenario 2: Encriptación asimétrica
  • Escenario 3: Hash de información

Antes de comenzar a revisar cada uno de estos algoritmos, revisemos una muy mala práctica que se utiliza más de lo que debiera, que corresponde a la utilización de “algoritmos” o mejor dicho, transformaciones del contenido.

Malas prácticas o transformaciones

Antes mencionamos que es común escuchar o ver que se utilizan ciertas transformaciones como métodos de protección. Como la comparación se hace con el ojo humano, si se tiene una cadena de texto y se decide transformar aplicándole un XOR o desplazando los bytes, el resultado de esa transformación nos produce el efecto de que está encriptado, por que no lo podemos entender (nuestro cerebro no lo entiende). Debido a esto, no vamos a hacer el ejemplo transformando texto sino que lo haremos transformando una imagen. Se aplicarán los dos efectos mencionados recién, XOR y Desplazamiento, y se verá que es efectivamente lo que hacen. El motivo de utilizar una imagen es para que nuestro ojo no se engañe con las apariencias.

Nuestro ejemplo nos presenta una pantalla como la que se muestra en la Figura 1, donde tenemos dos botones; uno para cargar la imagen, otro para aplicarle las transformaciones XOR, y otro para desplazar los bytes:



Figura 1.

Si se presiona Transformar, el resultado será el que se muestra en la figura 2:


Figura 2: Resultado de las transformaciones.

Como se puede apreciar, no se produce una encriptación sino sólo una transformación de los datos (más adelante verás cómo queda la imagen realmente encriptada). Este tipo de transformaciones no protege nuestra información. Hagamos ahora las siguientes preguntas: ¿Cuánta seguridad existe con estas transformaciones? ¿Cuánto demorará un hacker en retransformar esto? El hacker obtiene la respuesta en menos de lo que te toma a ti leer sólo esta línea. Hacerle modificaciones a este “algoritmo” no sirve. La encriptación no es trivial de hacer, pero es mucho más fácil de usar. Más adelante conocerás  la cantidad de años que estuvo estudiándose el algoritmo Rijndael o AES, para ser declarado vencedor.

El código que utilizamos para realizar la transformación XOR es el siguiente. El código de desplazamiento es muy similar. Este, al igual que todo el código del documento, está disponible en el archivo adjunto con este artículo. Por motivos que no corresponde explicar aquí, ya que no entran en el objetivo del documento, con la imagen que se está trabajando es necesario comenzar desde la posición 34, ya que si se comienza desde el byte 0 transformando, el archivo BMP ya no es posible visualizarlo.

Lo único que hace este “algoritmo” es aplicar XOR a un arreglo de bytes. Para aplicarlo sobre texto lo único que hay que hacer son las transformaciones de bytes a texto y viceversa. Como explicación sencilla de este proceso, podemos decir que lo que hace es tomar cada uno de los bytes y aplicarle un XOR (representado por el símbolo “^” en el código). Un XOR corresponde al resultado de la comparación bit a bit de dos bytes. En este caso, un byte viene del texto a transformar y el otro del número 87 elegido al azar.

public class TransformacionXOR
{
      
private static int _XOR = 87;

       public static byte[] Codificar(byte[] bytACodificar)
       {
             
for (int _intPos = 34; _intPos < bytACodificar.Length; _intPos++)
                    
bytACodificar[_intPos] = (byte) (_XOR ^ bytACodificar[_intPos]);
             
return bytACodificar;
       }

       public static byte[] DeCodificar(byte[] bytADecodificar)
       {
             
for (int _intPos = 34; _intPos < bytADecodificar.Length; _intPos++)
                    
bytADecodificar[_intPos] = (byte) (_XOR ^ bytADecodificar[_intPos]);
              return bytADecodificar;
       }

}

Como ven, este algoritmo tan lineal no nos da ninguna seguridad; y por lo mismo, jamás debe utilizarse para proteger información. No debe creerse que es XOR el responsable de esto. XOR es muy útil en otros contextos, pero no se puede utilizar de esta forma.

Nota: Antes de continuar, es muy importante aclarar lo siguiente. Si se va a proteger información, el proceso DEBE realizarse a conciencia y con algoritmos probados. Además, se debe utilizar el algoritmo de la forma en que fue concebido. Implementaciones a medias, son tan inútiles como las transformaciones recién vistas o como la no utilización de ésta. O el proceso se hace bien, o no se hace.

Veamos ahora los algoritmos reales de encriptación.

Encriptación simétrica

Cuando hablamos de encriptación y no de transformación, ya estamos adentrándonos en temas de mayor protección, de algoritmos conocidos y seguridad real. El proceso de realizar una encriptación es complejo para ser entendido por nosotros mismos, pero no es limitante para conocer cuáles son los pasos para utilizarlos y qué errores no se deben cometer.

Dentro de los algoritmos de encriptación simétrica podemos encontrar los siguientes, algunos más seguros que otros.

DES (Digital Encryption Standard)
Creado en 1975 con ayuda de la NSA (National Security Agency); en 1982 se convirtió en un estándar. Utiliza una llave de 56 bit. En 1999 logró ser quebrado (violado) en menos de 24 horas por un servidor dedicado a eso. Esto lo calificó como un algoritmo inseguro y con falencias reconocidas.

3DES (Three DES o Triple DES)
Antes de ser quebrado el DES, ya se trabajaba en un nuevo algoritmo basado en el anterior. Este funciona aplicando tres veces el proceso con tres llaves diferentes de 56
bits. La importancia de esto es que si alguien puede descifrar una llave, es casi imposible poder descifrar las tres y utilizarlas en el orden adecuado. Hoy en día es uno de los algoritmos simétricos más seguros.

IDEA (International Data Encryption Algorithm)
Más conocido como un componente de PGP (encriptación de
mails), trabaja con llaves de 128 bits. Realiza procesos de shift y copiado y pegado de los 128 bits, dejando un total de 52 sub llaves de 16 bits cada una. Es un algoritmo más rápido que el DES, pero al ser nuevo, aún no es aceptado como un estándar, aunque no se le han encontrado debilidades aún.

AES (Advanced Encryption Standard)
Este fue el ganador del primer concurso de algoritmos de encriptación realizado por la NIST (
National Institute of Standards and Technology) en 1997. Después de 3 años de estudio y habiendo descartado a 14 candidatos, este algoritmo, también conocido como Rijndael por Vincent Rijmen y Joan Daemen, fue elegido como ganador. Aún no es un estándar, pero es de amplia aceptación a nivel mundial.

En nuestra demo utilizaremos el algoritmo AES. .NET provee implementaciones de AES, DES y TripleDES entre otros.

El algoritmo más seguro hoy el AES, aunque 3DES también es muy seguro. Este último se utiliza cuando hay necesidad de compatibilidad. AES 128 es aproximadamente 15% más rápido que DES, y AES 256 sigue siendo más rápido que DES.

Cualquiera de estos algoritmos utiliza los siguientes dos elementos; ninguno de los dos debe pasarse por alto ni subestimar su importancia:

IV (Vector de inicialización)
Esta cadena se utiliza para empezar cada proceso de encriptación. Un error común es utilizar la misma cadena de inicialización en todas las encriptaciones. En ese caso, el resultado de las encriptaciones es similar, pudiendo ahorrarle mucho trabajo a un
hacker en el desciframiento de los datos. Tiene 16 bytes de largo. Puedes encontrar más información acerca de IV en http://www.atrevido.net/blog/PermaLink.aspx?guid=6136ce81-9fa1-4995-bb3e-15bc5f1f5899.

Key (llave)
Esta es la principal información para encriptar y desencriptar en los algoritmos simétricos. Toda la seguridad del sistema depende de dónde esté esta llave, cómo esté compuesta y quién tiene acceso. El largo de las llaves depende del algoritmo. Al final del documento se darán algunas recomendaciones para el almacenamiento, generación y rotación de llaves.

Algoritmo
Largos válidos (bits)
Largo por defecto (bits)
DES 64 64 (8 bytes)
TripleDES 128, 192 192 (24 bytes)
Rijndael 128, 192, 256 256 (32 bytes)

Veamos ahora nuestra aplicación y cómo funciona para encriptación y desencriptación con algoritmos simétricos, específicamente Rijndael (Ver figura 3):


Figura 3: Resultado de la encriptación con Rijndael aplicado sobre una imagen y texto.

Lo primero que debemos hacer es especificar una llave de encriptación. Una cadena de texto se utiliza en el ejemplo. En la caja de más abajo se puede ingresar el texto que se desea encriptar. Luego de presionar “Encriptar” se obtiene el resultado de la encriptación en la caja del medio, y como es de esperarse, presionando “DesEncriptar” se obtiene el texto desencriptado en la última caja de texto.

Si revisamos el código fuente que realiza la encriptación y desencriptación, encontraremos algo de mayor complejidad con respecto las transformaciones anteriormente vistas.

public class MiRijndael
{
      
public static byte[] Encriptar(string strEncriptar, byte[] bytPK)
       {
              Rijndael miRijndael = Rijndael.Create();
             
byte[] encrypted = null;
             
byte[] returnValue = null;

              try
              {
                     miRijndael.Key =  bytPK;
                     miRijndael.GenerateIV();

                     byte[] toEncrypt =  System.Text.Encoding.UTF8.GetBytes(strEncriptar);
                     encrypted = (miRijndael.CreateEncryptor()).TransformFinalBlock(toEncrypt, 0, toEncrypt.Length);

                     returnValue = new byte[miRijndael.IV.Length + encrypted.Length];
                     miRijndael.IV.CopyTo(returnValue, 0);
                     encrypted.CopyTo(returnValue, miRijndael.IV.Length);
              }
             
catch { }
             
finally { miRijndael.Clear(); }

              return returnValue;
       }

       public static string Desencriptar(byte[] bytDesEncriptar, byte[] bytPK)
       {
              Rijndael miRijndael = Rijndael.Create();
             
byte[] tempArray = new byte[miRijndael.IV.Length];
             
byte[] encrypted = new byte[bytDesEncriptar.Length – miRijndael.IV.Length];
             
string returnValue = string.Empty;

              try
              {
                     miRijndael.Key =  bytPK;

                     Array.Copy(bytDesEncriptar, tempArray, tempArray.Length);
                     Array.Copy(bytDesEncriptar, tempArray.Length, encrypted, 0, encrypted.Length);
                     miRijndael.IV = tempArray;

 

                     returnValue =  System.Text.Encoding.UTF8.GetString((miRijndael.CreateDecryptor()).TransformFinalBlock(encrypted, 0, encrypted.Length));

              }
             
catch { }
             
finally { miRijndael.Clear(); }

              return returnValue;
       }

       public static byte[] Encriptar(string strEncriptar, string strPK)
       {
             
return Encriptar(strEncriptar, (new PasswordDeriveBytes(strPK, null)).GetBytes(32));
       }

       public static string Desencriptar(byte[] bytDesEncriptar, string strPK)
       {
             
return Desencriptar(bytDesEncriptar, (new PasswordDeriveBytes(strPK, null)).GetBytes(32));
       }
}

Explicar el funcionamiento del algoritmo no entra dentro del alcance de este documento y, siendo sincero, desconozco cómo funciona. Tampoco creo que sea necesario que nosotros sepamos cómo funcionan los algoritmos de encriptación. Lo importante es utilizarlos bien.

Nota: Para obtener la llave del algoritmo, dado que una cadena de carácteres generada por un humano en un formulario es considerada poco variada debido a que los carácteres utilizados son pocos (a->z, A->Z, 0->9), se le aplica un proceso de variación con la clase PasswordDeriveBytes y se le pide un resultado de 32 bytes. Esto nos retorna una llave con carácteres variados, ideal para la encriptación. PasswordDeriveBytes utiliza Hash para generar la salida. Hash se verá más adelante.

Volviendo al ejemplo, si se presiona “Encriptar” nuevamente, las veces que se desee, se verá que el resultado de la encriptación varía. ¿Por qué sucede esto, si el resultado de la encriptación debiera ser el mismo? La variación en el resultado de una encriptación se produce por dos variantes en el proceso, el Key (o llave de encriptación) y el IV (o vector de inicialización). En este caso se está utilizando el mismo Key (explícito en la caja de texto) pero el IV está variando todas las veces. ¿Porqué varía el IV? ¿Qué es el IV?

El IV o vector de inicialización es un concepto no entendido del todo, por que algunos creen que se puede encriptar “sin él”. El IV cumple un rol pequeño, pero muy importante. El único motivo por el cual existe es para apoyar el proceso de encriptación, permitiendo exactamente que ocurra lo que se observó en la caja de texto, es decir,  la variación del resultado del proceso de encriptación. El IV no debe considerarse como una segunda llave, por que no lo es. Es una ayuda y por lo mismo, no es un dato que haya que esconder, si no que basta con almacenarlo para que cuando se vaya a desencriptar se utilice el mismo IV que se utilizó en la encriptación. En el ejemplo, el IV se retorna como parte del resultado de la encriptación y se genera cada vez que se realiza un encriptación. Su largo es de 16 bytes para el algoritmo de Rijndael.

Nota: Utilizar siempre un mismo IV para no tener que almacenarlo es equivalente en seguridad a no utilizar encriptación. La seguridad mal implementada es más insegura que no implementarla. ¿Por que? Porque da la creencia de tener seguridad, y produce una confianza que es nociva. Si no tienes seguridad en tu aplicación, eso lo sabes y estás preocupado, pero si se tiene una seguridad mal implementada se cree que se tiene, pero no es así.

Con la implementación entregada en la demo, el IV está correctamente utilizado. En las imágenes de la segunda parte se aplica el mismo algoritmo de encriptación, pero utilizando variaciones en el modo de ciframiento. En la segunda imagen se aplica el modo ECB y en la tercera CBC. Estos modos especifican al algoritmo cómo debe funcionar. El modo más seguro, que también es el modo por defecto es CBC.

El modo de ciframiento ECB no utiliza IV, por lo mismo no debe usarse como modo de ciframiento, a menos que sea necesario por compatibilidad. Esta es otra muestra de porqué es útil el IV y porqué debe usarse, y que sucede con la encriptación cuando el IV no varía nunca.

Es importante considerar que en los algoritmos simétricos se utiliza la misma llave para encriptar y desencriptar. En los algoritmos asimétricos se verá que existen dos llaves, una pública para encriptar y una privada para desencriptar.

Encriptación Asimétrica

La encriptación asimétrica permite que dos personas puedan enviarse información encriptada, sin necesidad de compartir la llave de encriptación. Se utiliza una llave pública para encriptar el texto y una llave privada para desencriptar. A pesar de que puede sonar extraño que se encripte con la llave pública y desencripte con la privada, el motivo para hacerlo es el siguiente: si alguien necesita que le envíen la información encriptada, deja disponible la llave pública para que quienes le desean enviar algo lo encripten. Nadie puede desencriptar algo con la misma llave pública. El único que puede desencriptar es quien posea la llave privada, quien justamente es el que recibe la información encriptada.

Los algoritmos de encriptación asimétrica más conocidos son:

  • RSA (Rivest, Shamir, Adleman)
    Creado en 1978, hoy es el algoritmo de mayor uso en encriptación asimétrica. Tiene dificultades para encriptar grandes volúmenes de información, por lo que es usado por lo general en conjunto con algoritmos simétricos.
  • Diffie-Hellman (& Merkle)
    No es precisamente un algoritmo de encriptación sino un algoritmo para generar llaves públicas y privadas en ambientes inseguros.
  • ECC (Elliptical Curve Cryptography)
    Es un algoritmo que se utiliza poco, pero tiene importancia cuando es necesario encriptar grandes volúmenes de información.

El tamaño de la llave es el siguiente:

Algoritmo
Largos válidos (bits)
Largo por defecto (bits)
RSA 384 a 16.384 (incrementos de a 8) 1.024

Mientras más larga sea la llave, más seguro será. La relación con los algoritmos simétricos no es directa. En este caso, una llave de 1024 bits de RSA es equivalente en seguridad a una de 75 bits de un algoritmo simétrico.

Como era de esperar, en nuestra demostración utilizaremos el algoritmo de mayor uso, RSA. Debido a que el funcionamiento es diferente comparado con el algoritmo anterior, la pantalla varía levemente. Existirá una zona donde se desplegará la llave pública generada, dejándole el almacenamiento de la llave privada a la clase miRSA.

La idea de esto es que exista una clase que provea una llave pública, para que el cliente encripte la información y luego éste le envíe lo encriptado a la misma clase, para que con la llave privada la desencripte. En este caso, se ha dejado el método desencriptar como público, por que se necesita para pintar el resultado en el formulario. Este no debiera ser un método público en una implementación real.

Antes de ver la demo, es necesario revisar el siguiente capítulo, debido a que la encriptación con RSA no es eficiente y tiene limitaciones de tamaño.

Encriptación Asimétrica + Simétrica

Debido a que la encriptación asimétrica es casi 1000 veces más lenta que la simétrica, cuando la información a encriptar es mucha, se utiliza una combinación de algoritmos. El algoritmo simétrico se utiliza para encriptar la información y el asimétrico para encriptar la llave del algoritmo simétrico con que se encriptó la información. Entonces, el proceso es mucho más rápido. Esta técnica se utiliza hoy en SSL en la negociación entre el navegador del cliente y el servidor. En cada ida y vuelta al servidor se generan nuevas llaves y se realiza todo el proceso. También es utilizada por Windows en la encriptación de los archivos.

Otro motivo para utilizar esta encriptación combinada es la necesidad de encriptar textos largos. La encriptación asimétrica además de ser ineficiente en tiempo, tiene limitaciones de tamaño. El tamaño máximo depende del largo de la llave. En la demostración, puedes probar colocando mucho texto y verás que se cae.

Estas limitantes nos obligan a utilizar el método combinado.

Nuestra demostración implementa ambos métodos, y se decide cuál utilizar con la selección del check Utilizar Simétrico. La aplicación se ve como se muestra en la figura 4 y figura 5. Antes de hacer cada demo, se debe seleccionar si se utilizará en conjunto con encriptación simétrica.

Figura 4: Encriptación utilizando solamente RSA.


Figura 5: Encriptación utilizando RSA con Rijndael.

El resultado de la encriptación es notoriamente más largo.

Como vemos, si generamos una llave y luego procedemos con los siguientes pasos, la encriptación se produce en un lado y la desencriptación en otro. Incluso .NET provee el método ToXmlString() de RSACryptoServiceProvider que retorna la llave en formato XML, ideal para ser enviada por algún medio público (Web Service, Archivo XML, etc.).

Dependiendo de si se utiliza en conjunto con algún algoritmo simétrico, la encriptación y desencriptación es diferente. Como algoritmo simétrico se utiliza AES ó Rijndael.

El código de nuestra clase miRSA es el siguiente:

public class miRSA
{
       private RSACryptoServiceProvider _objRSA = null;

       public miRSA()
       {
              this._objRSA = new RSACryptoServiceProvider(1024);
       }

       public string ObtenerLlavePublica()
       {
              return this._objRSA.ToXmlString(false);
       }

       public string DesEncriptar(byte[] bytEncriptado, bool blnConSimetrico)
       {
              if (blnConSimetrico)
              {
                     byte[] keyArray = new byte[_objRSA.KeySize/8];
                     byte[] encrypted = new byte[bytEncriptado.Length – keyArray.Length];
                     Array.Copy(bytEncriptado, 0, keyArray, 0, keyArray.Length);
                     Array.Copy(bytEncriptado, keyArray.Length, encrypted, 0, encrypted.Length);

                     byte[] simKey = this._objRSA.Decrypt(keyArray, false);

                     return MiRijndael.Desencriptar(encrypted, simKey);
              }
              else
              {
                     return System.Text.Encoding.UTF8.GetString(this._objRSA.Decrypt(bytEncriptado, false));
              }
       }
}

Y el código del cliente público es el siguiente (declarado en algún lugar del formulario):

private miRSA _objKey  = null;
 
public Form1()
{
       InitializeComponent();
       _objKey = new miRSA();
}

private void btnAsimEncriptar_Click(object sender, System.EventArgs e)
{
       byte[] _bytEncriptado = null;

       //Creamos una instancia del encritador publico
       RSACryptoServiceProvider _objEncriptadorPublico = new RSACryptoServiceProvider();
       //Le asignamos la llave genarada
       _objEncriptadorPublico.FromXmlString(this.txtAsimLlavePublica.Text);

       if (this.chkSimetrica.Checked)
       {
              //Se declara la memoria para almacenar la llave utilizada por nuestro Rijndael personalizado
              byte[] _bytKey = (Rijndael.Create()).Key;

              //Se encripta el texto y se obtiene la llave que se utilizó para la encriptación
              byte[] _bytEncriptadoSimetrico = TransEncripHash.MiRijndael.Encriptar(this.txtAsimAEncriptar.Text, _bytKey);

              //Se encripta la llave con el algoritmo RSA
              byte[] _bytEncriptadoLlave = _objEncriptadorPublico.Encrypt(_bytKey, false);

              //Se copia en un arreglo la llave encriptada y el encriptado de Rijndael
              _bytEncriptado = new byte[_bytEncriptadoLlave.Length + _bytEncriptadoSimetrico.Length];
              _bytEncriptadoLlave.CopyTo(_bytEncriptado,0);
              _bytEncriptadoSimetrico.CopyTo(_bytEncriptado, _bytEncriptadoLlave.Length);
       }
       else
       {
              _bytEncriptado = _objEncriptadorPublico.Encrypt(System.Text.Encoding.UTF8.GetBytes(this.txtAsimAEncriptar.Text), false);
       }
       this.txtAsimEncriptado.Text = Convert.ToBase64String(_bytEncriptado);
}

private void btnAsimDesEncriptar_Click(object sender, System.EventArgs e)
{
       this.txtAsimDesEncriptado.Text = this._objKey.DesEncriptar(Convert.FromBase64String(this.txtAsimEncriptado.Text), this.chkSimetrica.Checked);
}

private void btnAsimGenerar_Click(object sender, System.EventArgs e)
{
       this.txtAsimLlavePublica.Text = this._objKey.ObtenerLlavePublica();
}

En este ejemplo, la encriptación se produce en un lugar, luego de solicitar la clave pública a la clase (pero que perfectamente puede ser un servicio Web), la información encriptada se puede enviar a este servicio Web sin problemas de violación de seguridad, y sin necesidad de conocer ambas llaves. Si bien es cierto que la clase o servicio conoce ambas llaves, la llave pública no le es de ninguna utilidad.

Antes de finalizar con este algoritmo, cabe mencionar que donde puede haber problemas es en la generación de la llave. Por eso, Diffie-Hellman (& Merkle) se especializa en mejorar ese proceso, ya que es de vital importancia la generación de llaves.

Nota: Existe cierta codificación que puede haber pasado desapercibida hasta ahora. Esta es la utilización de distintos métodos para transformar textos en arreglos de bytes, y viceversa. Los métodos reales para hacerlo son los que están implementados en los encodings de .NET, como por ejemplo System.Text.Encoding.UTF8.GetString(bytes) y System.Text.Encoding.UTF8.GetBytes(string). En las demos se ha cambiado algunos deliberadamente por Convert.ToBase64String(bytes) y Convert.FromBase64String(string) para poder desplegarlo en los controles del formulario Windows. El resultado de una encriptación genera carácteres visibles y no visibles. Si no se transforman a Base64 y desde Base64 mientras son almacenados en un control, se pierde información. Si el resultado de la encriptación no va a ser desplegado sino que será almacenado en algún medio, los bytes de éste pueden escribirse sin problemas en el medio que sea.

Hash

Por último, nos quedan los algoritmos de tipo Hash. Estos son algoritmos del tipo de los que se conocen como de sólo ida, ya que no es posible desencriptar lo que se ha encriptado. Puede ser que a primera vista no se le vea la utilidad, pero en los siguientes dos escenarios éste es el tipo de algoritmo necesario para realizar el proceso. El primero de ellos está dirigido a proteger información muy valiosa encriptando el contenido; y el segundo busca validar que no se modifique la información que se está enviando desde un lugar a otro.

  • Escenario 1: Almacenar contraseñas de un sistema. Las contraseñas de cualquier sistema serio jamás debieran poder desencriptarse. El motivo es simple. Si se pueden desencriptar, el riesgo de que alguien conozca la llave y tenga acceso a todo el sistema con todos los usuarios es muy grande. Entonces, si no puedo desencriptar una contraseña, ¿Cómo puedo saber entonces si una contraseña entregada es válida? La respuesta es muy simple. Se encripta la contraseña entregada y se compara el resultado de la encriptación con la contraseña previamente almacenada.
  • Escenario 2: Validar que cierta información no se haya modificado. Si se desea enviar un bloque de datos y se quiere estar seguro de que nadie lo ha modificado durante el camino, lo que se hace es enviar el bloque de información sin encriptar y además se envía un Hash/Digest de este mismo bloque de datos. Entonces, nuestro receptor al tener la información sin encriptar, le aplica un Hash y con el mismo criterio del Escenario 1, determina si ambos bloques son iguales. Si lo son, el bloque que no está con Hash es el original. ¿Porqué no usar un algoritmo de encriptación reversible? Porque éste podría ser desencriptado, modificado y vuelto a encriptar y el receptor jamás sabría que fue modificado en el camino. Como el Hash no es reversible, esto último no podrá suceder jamás.

Los algoritmos para hacer Hash mas conocidos son los siguientes:

Algoritmo
Tamaño del Hash (bits)
MD5
128
SHA-1
160
SHA-256
256
SHA-384
384
SHA-512
512

La robustez de un algoritmo de Hash es comparable a la mitad del largo del resultado en bits. Entonces, pensar que con MD5 se está seguro puede ser un error. SHA-256 es el indicado para obtener seguridad de 128 bits. Es un hecho que SHA-256 es mucho más lento, pero como Bruce Schneier dice “Ya existen suficientes sistemas rápidos e inseguros”.

Pero a pesar de parecer muy efectivo, existe un problema con el Hash para cada uno de los escenarios indicados anteriormente. Como son problemas conocidos, existen soluciones conocidas también. Revisemos ahora cada uno de los problemas y sus soluciones.

Problemas Escenario 1
Si bien el problema no está tan relacionado con el
Hash, debido a la utilización de contraseñas de escasa dificultad, haciendo un ataque de fuerza bruta o por diccionario se podrían encontrar valores de Hash que coinciden con el Hash de las contraseñas almacenadas. Para esto se pueden ejecutar dos planes de acción independientes, pero obteniéndose el mejor resultado con la utilización de ambos.

La Solución 1 consiste en realizar varias pasadas de Hash sobre los datos, aplicándole el Hash al resultado del Hash anterior. Realizando esto varios cientos de veces (idealmente mil veces) podemos dificultar más el trabajo de un hacker. Más información de porqué se debe iterar muchas veces y las consecuencias de no hacerlo, la puedes encontrar en http://www.atrevido.net/blog/PermaLink.aspx?guid=12b1338b-171e-4692-850e-8f59ed12f24a .

La Solución 2 requiere agregar un texto adicional a la contraseña. Esto se conoce como Salt. Entonces, cada vez que un usuario cambie o esté validando una contraseña, hay que concatenarle a la contraseña este Salt y de ahí generar el Hash. De esta forma le agrega variación a las contraseñas pobremente definidas. Esta solución requiere un trabajo adicional ya que es necesario además almacenar el texto del Salt junto a la información del usuario y la contraseña encriptada. Este texto se debe crear con carácteres generados al azar.

En el ejemplo se mostrará la combinación de ambas soluciones. Como encriptación de Hash se usará SHA256. Los otros algoritmos mencionados con anterioridad están disponibles también en .NET.

El resultado de la encriptación 1000 veces de cierto texto con un Salt definido, es el que se muestra en la figura 6:


Figura 6.

El código para realizar esto es muy simple y es el que se muestra a continuación:

public class HashContraseñas
{
       public static byte[] Encriptar(string strPassword, string strSalt, int intIteraciones)
       {
                               
string _strPasswordSalt = strPassword + strSalt;
                                SHA256Managed _objSha256 = new SHA256Managed();
                                byte[] _objTemporal = null;

                                try
                                {
                                                _objTemporal = System.Text.Encoding.UTF8.GetBytes(_strPasswordSalt);
                                                for (int i = 0; i <= intIteraciones – 1; i++)
                                                               
_objTemporal = _objSha256.ComputeHash(_objTemporal);
                                }
                                finally { _objSha256.Clear(); }

                                return _objTemporal;
       }
}

Qué cosas se deben clarificar en este proceso
El resultado de este
Hash (Sha256) siempre retorna 256 bits, es decir, 32 bytes. Cuidado con confundir con el largo del string resultante de la conversión a Base64. Esta conversión genera más carácteres. Para verificar, puedes medir el largo del arreglo colocando un punto de interrupción en el retorno (return), y notarás que son 32 bytes.

Además, las funciones de hash no tienen limitante para el tamaño del texto de entrada. Sea cual fuere el tamaño, el largo de la salida es el mismo y se procesan todos los carácteres del string de entrada.

Para un sistema de almacenamiento de contraseñas de usuario seguro, hay que hacer algunas modificaciones a esta función, pero son mínimas. Las modificaciones no pasan por cambios en el algoritmo, sino más bien por crear una función para que genere Salt randómicos y crear la función para la comparación de las contraseñas.

Probablemente te preguntes porqué hay 2 cajas de texto con el mismo texto. Como este proceso es común, o al menos debiera serlo, .NET provee una clase llamada PasswordDeriveBytes que sirve para realizar de forma automática todo lo anteriormente visto. En el evento click, además de la llamada a la función detallada de más arriba (HashContraseñas.Encriptar), se crea una instancia de esta clase (PasswordDeriveBytes) y se le especifican los mismos parámetros que estamos utilizando en nuestro algoritmo de Hash (la contraseña, el Salt, el algoritmo SHA256 y la cantidad de iteraciones). Esto hace que en sólo pocas líneas se pueda realizar el mismo proceso.

El código ejecutado es el siguiente:

PasswordDeriveBytes _objPdb = new PasswordDeriveBytes(this.txtHashEncriptar1.Text, System.Text.Encoding.UTF8.GetBytes(this.txtSalt.Text), “SHA256”, Convert.ToInt32(this.txtCantidad.Text));

this.txtHashEncriptado2.Text = Convert.ToBase64String(_objPdb.GetBytes(32));

Para comparar arreglos de bytes, se verá más adelante una función con tal objetivo.

Problemas Escenario 2: Si alguien puede ejecutar un Hash sobre un conjunto de datos y obtener el resultado, ¿Porqué no podría modificar el contenido del documento que estamos enviando y generar el Hash nuevamente, reemplazando el contenido del Hash anterior? ¿Cómo se puede diferenciar mi proceso de Hash del proceso de Hash impostor? No es posible a menos que haya alguna llave de por medio.

Debido a lo anterior, se han inventado los siguientes algoritmos de Hash, que utilizan llaves para realizar el proceso. En este caso, si el resultado se sobrescribe, al obtener el Hash con la llave, el resultado jamás coincidirá con el que viene en el mensaje.

Estos algoritmos y sus llaves son los siguientes:

Algoritmo
Tamaño del Hash (bits)
Tamaño de llave (bits)
HMAC-SHA1 160
64
MAC-3DES-CBC 64
8, 16, 24

Para realizar la demo, debemos primero generar una llave. Hasta ahora, las llaves se han creado manualmente, pero ahora vamos a utilizar herramientas del Framework para generarlas. Como la llave de 64 bits es del mismo largo que la que utiliza el algoritmo DES, se utilizará la clase de éste para generar una. Existe el botón “Generar Key” que es el encargado de generar y almacenar una llave. Esta se genera en la función del botón, y se almacena en una variable de formulario. El código es el siguiente:

privatevoid btnHashKey_Click(object sender, System.EventArgs e)
{
      this._objHashKey = (new System.Security.Cryptography.DESCryptoServiceProvider()).Key;
      this.lblSalt.Text = “Key : ” + Convert.ToBase64String(this._objHashKey);
}

Con la primer sentencia se crea la instancia del proveedor de DES y se obtiene el Key almacenándolo en _objHashKey, una variable de formulario. Con la segunda línea, se copia el contenido de la Key en una etiqueta del formulario, previamente transformándolo en Base64. Cabe mencionar que la transformación a Base64 genera una cadena más larga que la cantidad de bytes que está recibiendo, por eso si cuentas la cantidad de carácteres del Key, no cumple con ser de 64 bits (8 bytes).

La validación es muy simple de realizar. Recuerda que lo que se quiere hacer es verificar que la información que se está enviando no se haya modificado en el camino. Entonces lo que se debe hacer es generar un nuevo Hash con el texto en cuestión y la contraseña, y comparar el resultado del Hash generado con el que se envió inicialmente. La comparación de Hash se realiza con una función para comparar arreglos (Ver figura 7):

Figura 7: Resultado de presionar Validar Hash sin hacer ninguna modificación.

En cambio, si se modifica levemente el texto, agregando la palabra “me” (o cualquiera) en el círculo rojo remarcado, y se trata de validar, el resultado es erróneo inmediatamente. Con esta funcionalidad se valida que nadie haya modificado el contenido de lo que se está enviando (Ver figura 8):


Figura 8: Resultado de presionar
Validar Hash con una modificación.

Además, si se genera una nueva Key (con el botón Generar Key) y se valida, el resultado es fallido, como era de esperarse. Existe otro método para realizar esta misma validación pero sin compartir las llaves. La idea es muy similar a la estudiada en los algoritmos asimétricos, en donde una de las partes tiene una llave para encriptar y otra parte tiene otra llave para encriptar también. Algo similar a esto es lo que se utiliza para la firma digital, pero por ser un tema tan amplio, no cabe en este artículo y merece un artículo completo para su explicación. Algunas claves para poder conocer como funciona esto, las siguientes clases del Framework poseen funcionalidad para lograrlo. Estas son RSACryptoServiceProvider y DSACryptoServiceProvider. Por último, algo de código. Para generar un Hash con un Key y para comparar arreglos de bytes se utilizan los siguientes bloques de código:

public class HashValidacion
{
       public static byte[] Encriptar(string strTexto, byte[] objKey)
       {
              HMACSHA1 _objHMACSHA = new HMACSHA1(objKey);
              try
              {
                     return _objHMACSHA.ComputeHash(Encoding.UTF8.GetBytes(strTexto));
              }
              finally
              {
                     _objHMACSHA.Clear();
              }
       }

       public static bool CompareByteArray(Byte[] arrayA, Byte[] arrayB)
       {
              if (arrayA.Length != arrayB.Length)
                     return false;

              for (int i = 0; i <= arrayA.Length – 1; i++)
                     if (!arrayA[i].Equals(arrayB[i]))
                           return false;
              return true;
       }
}

Conclusiones

A través de las dos partes de este artículo se han explorado los métodos más comunes de encriptación, revisándose cada escenario donde se aplican. Cada escenario se ha complementado con ejemplos y código 100% funcional. Se ha  desmitificado la encriptación como un proceso complicado y nebuloso, mostrándose de forma clara cuándo y cómo aplicar cada algoritmo, además de la forma correcta de hacerlo.

Respuestas a las Preguntas

Revisemos entonces algunas de las preguntas inicialmente planteadas y veamos cómo podemos responderlas ahora.

  • ¿Se está almacenando la información crítica de forma segura?
    Esta pregunta la puede respnder el lector.
  • ¿Qué entendemos por seguro?
    Se ha mostrado la diferencia entre algoritmos que transforman texto con respecto a otros que realmente encriptan.
  • Si se está encriptando, ¿Qué algoritmo se está utilizando?
    Se revisaron los algoritmos más conocidos y se mostraron sus ventajas y falencias. Sea cual fuere, el algoritmo debe implementarse adecuadamente.
  • ¿Se permite la desencriptación de datos que no debieran poder desencriptarse?
    Esta pregunta la puede responder el lector, pero la respuesta debe ser NO.
  • ¿Cómo está realizándose la encriptación?
    Se revisó la forma correcta de encriptar con cada algoritmo, como también las malas prácticas y los riesgos que se corren al no hacer el proceso como debe ser.
  • ¿Dónde están almacenadas la o las llaves?
    Esta es una muy buena pregunta y la respuesta escapa al alcance de este artículo, pero como sugerencia se puede mencionar lo siguiente: se puede utilizar DPAPI o Certificate Storage. Para implementar DPAPI tanto desde aplicaciones Windows como web, puedes visitar los siguientes enlaces:

    DPAPI: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/SecNetHT07.asp
    DPAPI en
    Web: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/SecNetHT09.asp

    Pensamientos como el siguiente no son seguros: “si almaceno la llave en un directorio escondido, es difícil que un hacker la encuentre”, como así también “A un hacker le va a tomar mucho tiempo encontrarla.” Si hay algo que le sobra a un hacker es tiempo y ganas.

  • ¿Con qué frecuencia se cambian las llaves?
    La respuesta es depende, lamentablemente. Depende de la criticidad del sistema, como también del uso de las llaves. Para una conexión de transferencia, se crean las llaves, se utilizan y se descartan. En un sistema crítico tal como el de un banco, el cambio de llaves debería hacerse cada una hora, o como máximo, cada un día. Para información que va a tener mayor duración, el tiempo puede ser mayor.
  • ¿Cómo enfrento el problema de cambiar las llaves con tanta frecuencia?
    Como cambiar las llaves no es una tarea simple en recursos y tiempo, se emplea el concepto de llave maestra y llaves hijas. La información a encriptar se encripta con las llaves hijas y luego  con la llave maestra se encriptan las llaves hijas y se almacenan.  Entonces, al momento de aplicar cambio de llave, se cambia sólo la llave maestra, procediéndose a desencriptar todas las llaves hijas con la antigua y a encriptar todas con la llave nueva. Esto disminuye el tiempo a factor de N (cantidad de llaves) y no de la cantidad de bytes de información encriptada. Esto no agrega seguridad, sólo ayuda a realizar un proceso de forma eficiente.
  • ¿Con qué criterio se definen las nuevas llaves?
    Para definir una llave se puede ejecutar la función GenerateKey() que todo algoritmo simétrico posee. Esto genera una llave randómica.

36 Replies to “Desmitificando la Encriptación (ex MTJ.NET)”

  1. Nelson,

    puedes compilar el proyecto y luego utilizar reflector para transformar el IL en C#. Esto debe funcionar.

    Saludos,

    Patrick

  2. Muy buena explicación sobre la encriptación, me ha gustado para enseñarlo en una clase. Hay algun sitio donde puedo bajar el programa?.

  3. Podrían fascilitarme la aplicación está excelente!!!.Lo necesito con fines académicos. Gracias por la Info.

  4. Este es el blog perfecto para cualquier persona que quiera saber acerca de este tema. Sabes tanto de sus casi difícil de discutir con usted (no es que realmente quiero … jaja). Que sin duda pone un nuevo giro a un tema se ha escrito sobre eso por años. Una gran cosa, simplemente genial!

  5. ce sera la page de blog parfait pour quiconque veut en savoir sur ce sujet. Vous savez beaucoup de sa pratique difficile d’argumenter avec vous (non pas que j’ai vraiment voudrait … haha). Vous devez absolument mettre une toute nouvelle sur un sujet thats écrit sur des années. Des choses fantastiques, tout simplement excellents!

  6. Sólo quería escribirle unas líneas para decirle que su msmvps.com realmente rocas ! He estado buscando este tipo de información por un largo tiempo .. No suelo responder a los mensajes , pero lo haré en este caso. WoW estupendo genial. saludos cordiales

  7. Grandes cosas desde tu msmvps.com , hombre. He leído tus cosas antes y usted es demasiado impresionante. Me encanta lo que usted ha llegado hasta aquí , me encanta lo que usted está diciendo y la forma en que lo dice. Usted lo hace entretenido y todavía se las arreglan para mantener de manera inteligente . No puedo esperar a leer más de usted. Esto es realmente un gran blog. saludos!

  8. Muchas gracias por el esfuerzo de desarrollo para discutir esto, me parece muy importante esto y como estudiar mucho más sobre este tema. Si es factible, a medida que adquiera experiencia, ¿te importaría actualizar msmvps.com tener una gran información mucho más? Es muy beneficioso para mí. respecto

  9. Me encanta tu blog y encontrar la mayoría de los de su puesto para ser exactamente lo que estoy buscando. ¿Le ofrecen escritores invitados a escribir el contenido para ti personalmente? No me importaría publicar un mensaje o la elaboración de una serie de los temas que escriben relacionadas hasta aquí. Una vez más , weblog impresionante! saludos!

  10. J’ai un ami qui a besoin de lever 35,000 $ à sortir de la dette, après avoir été mis à pied. Ce serait lui donner un nouveau départ dans la vie car il ya pas beaucoup d’emplois dans ce domaine, elle pourrait se déplacer et recommencer. Je voudrais l’aider et créer un site Internet pour les dons, même si les gens juste donné un dollar, vous obtenez assez de gens et il pourrait vraiment faire une différence ..

  11. Lo admito, no he estado en esta página web en mucho tiempo … sin embargo, fue otra alegría al ver que es un tema tan importante e ignorado por muchos, incluso los profesionales. Le doy las gracias para ayudar a hacer que la gente más consciente de los posibles problemas.

  12. Je pense que les propriétaires d’autres sites devraient prendre msmvps.com comme un modèle, très propre et un excellent style utilisateur conviviale et design, sans parler du contenu. Vous êtes un expert dans ce sujet!

  13. Doooooncs… el meu primer blog el vaig crear una mica perquè sí fa la tira d’anys. Potser en tenia 15, i això vol dir que en fa set. No em preguntis perquè el vaig fer que no t’ho sé dir. Bé, sí, m’agradava escriure i hi volia penjar les meves coses, però no sé quin ressort va saltar per fer-me començar. Si no fos perquè quan vaig néixer no existia internet (de forma domèstica, almenys) et diria que per mi els blogs han existit de tota la vida. Però no pas com els conec ara! La història va seguir amb què aquell primer blog va morir d’avorriment amb poquíssimes entrades (unes… cinc?). Diria que segueix existint, però n’amagaré el nom per vergonya pròpia. (Estic rebuscant per explicar bé la història). Aquell primer blog va morir perquè vaig començar a Relats en Català, que em va semblar un lloc millor on escriure. Però cap al cap d’un any, cap al 2006, em va agafar per escriure poesia. De vegades en castellà, i de vegades massa íntima per fer-la pública a RC. I per penjar-la a algun lloc va néixer el meu segon blog, que també està tancat i barrat (per vergonya pròpia). Van ser una vintena d’entrades, la majoria d’elles farcides de poesia terrible d’adolescent. I un any després, la nit de Nadal del 2007 va néixer el Coses de la Vida, amb la idea de penjar-hi les coses no-poètiques. El blog va estar vagarejant mig mort durant anys… fins que a mitjans del 2010 vaig començar a reactivar-lo, i a principis del 2011 vaig tenir la punteria d’anar a caure al blog d’en XeXu i a partir d’allà vaig descobrir toooooooota la blogosfera i tota la moguda que us porteu per aquí. Així que ja ho veus. M’ha costat sis anys de blogs adonar-me de tot el que hi ha… I ara és molt més divertit, i el blog ja no es mor com abans. I després del que he remenat per comentar-te i del rotllo que t’he clavat, em penso que això es convertirà automàticament en una entrada al meu post!! Això acabarà sent com un meme…

  14. Needs a good deal of work inside speed department, and perhaps within the menu system, but pleasant thus far.

    The layout of the function interface is well arranged and user friendly.
    This isn’t the first time Nintendo’s titles and designs have hit the App Store in an unofficial manner.

Leave a Reply

Your email address will not be published. Required fields are marked *