Micro aporte algoritmos de cifrado, con esperanza de compendio

Bueno, de un tiempo para acá he estado buscando la manera de cifrar cadenas ordinarias con algún algoritmo de cifrado. De momento me puse a buscar algoritmos para comparar con MySQL ya que tiene funciones que te regresan el cifrado de una cadena que tu le pongas basta con sólo dar:
SELECT MD5('la_cadena_que_quieres_convertir')

En mi caso, no quiero hacer uso de la base de datos, por lo que me empecé a buscar algoritmos que me permitieran agregar esa funcionalidad para alguna base de datos que no tuviera dicha funcionalidad (de momento no sé de ninguna, pero por si a caso existe una sin esta funcionalidad).

Cómo bien dice título es un micro aporte (o incluso ni aporte, ya que los algoritmos que pondré aunque a veces se batalla para encontrar el correcto, pues están en varios sitios), pero lo que me gustaría hacer es que pues entre todos los que pertenecemos a JavaMéxico hagamos un compendio de algoritmos de cifrado obviamente usando Java.

Empiezo con los más comunes en una clase que llamo Hasher (fue el nombre que se me ocurrió, no sé porqué tengo una fijación con la palabra 'Hash'), viene con un pequeño ejemplo:

public class Hasher{
        static String cosa = "cosa cosa cosa";
       
        static public String convertToMD5(String stringToHash){
                final char[] HEXADECIMAL = {
                                             '0', '1', '2', '3',
                                             '4', '5', '6', '7',
                                             '8', '9', 'a', 'b',
                                             'c', 'd', 'e', 'f'
                                           };
                try {
                        MessageDigest md = MessageDigest.getInstance("MD5");
                        byte[] bytes = md.digest(stringToHash.getBytes());
                        StringBuilder sb = new StringBuilder(2 * bytes.length);
                        for (int i = 0; i < bytes.length; i++) {
                                int low = (int)(bytes[i] & 0x0f);
                                int high = (int)((bytes[i] & 0xf0) >> 4);
                                sb.append(HEXADECIMAL[high]);
                                sb.append(HEXADECIMAL[low]);
                        }
                        return sb.toString();
                } catch (NoSuchAlgorithmException e) {
                    return null;
                }
        }

        static public String convertToSHA1(String stringToHash){
                try{
                       
                        MessageDigest md = MessageDigest.getInstance("SHA-1");
                        byte[] sha1Hash = new byte[40];
                        md.update(stringToHash.getBytes("iso-8859-1"),0, stringToHash.length());
                        sha1Hash = md.digest();
                        StringBuffer buf =  new StringBuffer();
                        for(int i = 0; i < sha1Hash.length; i++){
                                int halfbyte = (sha1Hash[i] >>> 4) & 0x0F;
                                int two_halfs = 0;
                                do{
                                        if((0 <= halfbyte) && (halfbyte <=9)){
                                                buf.append((char) ('0'+halfbyte));
                                        }else{
                                                buf.append((char)('a'+(halfbyte-10)));
                                        }
                                        halfbyte = sha1Hash[i] & 0x0F;
                                }while(two_halfs++ < 1);
                        }
                        return buf.toString();
                }catch(NoSuchAlgorithmException nsae){
                        System.out.println("No se encontró el algoritmo");
                        System.out.println("===========================");
                        nsae.printStackTrace();
                }catch(UnsupportedEncodingException uee){      
                        System.out.println("Tipo de codificación no encontrado");
                        System.out.println("===========================");
                        uee.printStackTrace();
                }
                return "";
        }

        static public void main(String ... args){
                System.out.println("===========================");
                System.out.println("Vamos a codificar la cadena " + cosa);
                System.out.println("Primero a MD5 " + convertToMD5(cosa));
                System.out.println("Luego a SHA1 " + convertToSHA1(cosa));
                System.out.println("Ahora combinado " + convertToSHA1(convertToMD5(cosa)));
                System.exit(0);
        }
}

También estaría chido ver que tanto los podemos ir mejorando a nivel de código, ya sea para reducir líneas o conseguir mejor eficacia.

Bueno, espero les sirva y pues si tienes otro algoritmo en mente compártelo.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Convenciones

Por cierto vengo dándome cuenta que no seguí la convención de "public static" y puse "static public", perdón por eso, es una clase que escribí a la rápida, no se lo tomen tan a pecho con: "Ay la convención es tal y sin la convención nos morimos".

Imagen de ezamudio

Digestión != Cifrado

MD5 y SHA1 son algoritmos de digestión de mensajes, no cifrado de datos.

Si quiero digerir datos con MD5 y no se encuentra el algoritmo, obtengo null, pero si quiero digerir datos con SHA1 y no se encuentra el algoritmo o paso una cadena que no se puede convertir a ISO-8859-1, obtengo una cadena vacía. Debería ser coherente el funcionamiento de ambos métodos. Incluso yo tendría un sólo método estático, que reciba como parámetro adicional el nombre del algoritmo, y los dos métodos que tienes actualmente simplemente invocarían ese método pasando el nombre del algoritmo correspondiente (MD5 o SHA1). Así cuando quieras usar un algoritmo de digestión que no esté completamente roto para digerir un mensaje real (como por ejemplo SHA-256), lo puedes usar, no quedas limitado a dos algoritmos que ya han sido quebrados para varias aplicaciones.

Si los estás usando para "cifrar" datos, como passwords, espero que estés concatenando el password con un dato adicional como el email o algún otro dato único para cada usuario, de modo que dos usuarios con el mismo password no tengan la misma digestión.

no seguí la convención de


no seguí la convención de "public static" y puse "static public", perdón por eso

Perdonado

:)

Peor hubiera sido:

public class hasher
{
  ...

:)

Por cierto en el código

Por cierto en el código fuente de cierto web público aplica este algoritmo N veces ( por omisión 1000 ) ...

  #Python
  def slow_hash(password, iterations=1000):
    h = hashlib.sha1()
    h.update(unicode(password).encode("utf-8"))
    h.update(keys.salt_key)
    for x in range(iterations):
      h.update(h.digest())
    return h.hexdigest()

Es esto mejor? o da lo mismo? Ahí si le pusieron la sal :)

Imagen de ezamudio

es mejor

Mil veces se me hace exagerado, pero sí es mejor. La idea es que si haces un solo hash, es más fácil buscar texto que te dé ese hash usando rainbow tables, mientras que si le aplicas varias iteraciones pues ya no va a aparecer ahí, porque para que lo encontraras en un rainbow table tendrías que tener los hashes iniciales y luego los finales, pero entonces sólo funcionaría para ese sitio con mil hashes; para otro lugar donde se usan 512 hashes no serviría, ni para un sitio donde se hagan 8 ni 16 ni 32 ni 4 hashes...

Re: Digestión != Cifrado

mmm...muy cierto, pero depende el uso tengo entendido. En mi caso los uso cómo cifrado ;), que caigamos en las cosas cómo son (que me falta mucho para llegar a entender las cosas, pues...cómo son) ahí es diferente y es hasta interesante ver de las personas que SI tienen idea.

Respecto a lo del método, en mi caso lo quise hacer un poco para las personas que les agrade el cómodo "clase punto parámetro", sin más, claro que también estaría chido hacer lo que comentas. De ser tan amable, puedes postear tu código, nos dejaría a más de uno con la boca abierta (cómo me he quedado últimamente revisando tus posts sobre Scala ;)).

Respecto a cifrar datos cómo passwords...pues si los uso para eso pero cómo bien dices concatenado algo, alguien me recomendó el segurísimo: "[lo que regresa tu algoritmo] - [id de base de datos]". Sin embargo a final de cuentas las posibilidades son muy pocas (aunque no digo que no se pueda), de plano he visto sitios que ni siquiera se toman la molestia de cifrar datos. En mi caso lo que hice fue guardar las copias de los algoritmos que puse (uno se llama emailSHA y otro emailMD5). Así se te pasa el de digamos MD5 pero tienes el de SHA; un amigo me recomendó usar por ejemplo password+nombre a MD5 y ese MD5 a SHA, pero, me agradó más mi idea (y para mí resulta menos engorrosa). Seguro hay una opción mejor (espero y la haya xD).

Re: Por cierto en el código

Pues...en realidad depende más del tipo de cifrado (o digestión cómo bien comenta @ezamudio). En el caso de cifrar un cifrado pues creo que lo único que ganas es hacer algo más lenta tu aplicación (si, en un usuario ni se nota, pero pues si es una aplicación web con 10,000 haciendo inicio de sesión al mismo tiempo, o sea estamos hablando de 10000000 de cálculos...si si también sé que no todos estamos desarrollando facebook xD).

Para mí se me hizo más seguro (y simplista) tener dos campos con cifrados obtenidos de distinto algoritmo y ya ;), aunque no sé que tan "cool" pueda ser.

Imagen de ezamudio

No es cifrado

Es que es importante manejar la terminología correcta. No estás cifrando datos. Hay dos tipos de cifrado: simétrico y asimétrico. En cifrado simétrico usas la misma llave para cifrar y descifrar. En cifrado asimétrico usas una llave para cifrar, y otra para descifrar. Pero siempre debe haber una manera de descifrar.

MD5 y SHA son algoritmos de digestión. No hay manera de descifrar el resultado, el algoritmo funciona solamente en una dirección.

Para almacenar passwords de manera segura lo mejor es usar digestión, no cifrado. Y la manera de usar la digestión debe ser primero concatenar el password con algún otro dato (por ejemplo el email o el ID o algo que sea único para el usuario) y luego a eso le sacas la digestión. Ejemplo:

usuario1@a.com, password "prueba" -> MD5("pruebausuario1@a.com") = 410ef7ba952a3954940b3f30c0157ccf
usuario2@a.com, password "prueba" -> MD5("pruebausuario2@a.com") = 250bcb96b982b93c44ca9cfd6dc1dec6

Dos usuarios con el mismo password tienen almacenado algo completamente distinto, por lo que no hay manera de saber si dos usuarios tienen el mismo password.

En este caso de passwords almacenados pues da igual si haces una iteración o varias (es decir que del 410ef7ba952a3954940b3f30c0157ccf saques un segundo MD5 y de ese resultado le apliques otro MD5, etc), porque el dato se guarda en almacenamiento permanente, de modo que no hay ninguna ventaja de hacer las iteraciones.

Las iteraciones de digestión se usan para llaves de sesión. Es decir, si por ejemplo el sistema está intercambiando datos con el usuario1@a.com, no se debe usar su password directamente como llave para cifrar datos, sino que se debe generar una llave de sesión, usando como base el password del usuario.

Por ejemplo si le quieres enviar el mensaje "hola" cifrado al usuario para que sólo él lo pueda descifrar con su password, entonces tienes el mensaje, y debes generar sal, se recomienda generar primero creando un bloque de datos aleatorio, se concatena con el password del usuario (como sólo tienes la digestión, eso es lo que usas) y a eso le sacas una digestión N veces (digamos 100, cada iteración saca digestión del resultado de la iteración anterior). El resultado de la última iteración se usa como llave simétrica (debe ajustarse al algoritmo usado, por ejemplo AES usa bloques de 128 bits).

Imagen de ezamudio

refactoring

En cuanto a tu código, yo me refería simplemente a hacer algo como esto:

public class Hasher {

  private static final char[] HEXADECIMAL = {
    '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
  };

  //Puede serte util tener este metodo aparte
  public static String hexEncode(byte[] data) {
    StringBuilder sb = new StringBuilder(2 * data.length);
    for (int i = 0; i < data.length; i++) {
      int low = (data[i] & 0x0f);
      int high = ((data[i] & 0xf0) >> 4);
      sb.append(HEXADECIMAL[high]);
      sb.append(HEXADECIMAL[low]);
    }
    return sb.toString();
  }

  public static byte[] hash(String stringToHash, String alg) throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance(alg);
    return md.digest(stringToHash.getBytes());
  }

  public static String convertToSHA1(String stringToHash) {
    try {
      return hexEncode(hash(stringToHash, "SHA-1"));
    } catch (NoSuchAlgorithmException ex) {
      //Eso no deberia pasar nunca a menos que la JVM de plano estuviera mal instalada
    }
  }

  public static String convertToMD5(String stringToHash) {
    try {
      return hexEncode(hash(stringToHash, "MD5"));
    } catch (NoSuchAlgorithmException ex) {
      //Eso no deberia pasar nunca a menos que la JVM de plano estuviera mal instalada
    }
  }

}

Buen refactor

Este codigo veo que sirve solo para digestion porque para cifrado con encriptacion tienen que definirse las llaves y eso es ooootro despapaye... un detalle que (igual no sirve de mucho pero bueno): "SHA-1" es lo mismo que poner simplemente "SHA"

Ooops, me acorde de algo...

Ooops, me acorde de algo... si, nunca pongas clases con minuscula, Oscar suelta coscorrones cuando ve eso!   

Importante la distincion

Bueno, creo que Cifrado, resumen (o hash) y codificacion son cosas diferentes que muchos alguna vez asumimos que eran cosas similares pero no. Porque si por ejemplo te piden obtener el Hash de una cadena y despues encriptarlo y despues codificarlo a x cosa... puede ser que ese mal entendido me orille a pensar:

El resumen con RSA, lo encripto con Base64 y lo codifico con SHA... ahi sacamos un grandisimo tache (he ahi la importancia de definir con los terminos adecuados)

Re: Buen refactor

No soy muy fan de buscar información (o más bien de creer todo) en Wikipedia. Pero pues de momento es lo que tengo a la mano y pues podemos ver un párrafo que dice:

El primer miembro de la familia fue publicado en 1993 es oficialmente llamado SHA[...] Dos años más tarde el primer sucesor de SHA fue publicado con el nombre de SHA-1

Aquí la info

Me referia al codigo

Si, es diferente por lo que dice el articulo es una mejora SHA-1 sobre SHA... a lo que voy es que si le pones a Java "SHA" o "SHA-1" sale el mismo hash, intentalo y verás!

Re: Me refería al código

Ah ok. Era un problema de semántica ;)