2014-08-26

Convertir array de bytes en String de los valores en Hexadecimal

Cuando trabajaba en el algoritmo para crear un MAC ANSI X9.19 tuve que convertir el resultado que estaba en un array de bytes (byte[] result) en un String que representase cada byte como un valor Hexadecimal. Para ello, utilicé una función que encontré en stackoverflow y que resultaba elegante a simple vista:
    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b & 0xff)); // "%02X" for uppercase
        }
        return sb.toString();
    }

El caso es que mientras buscaba el código para el MAC ANSI X9.19 me encontré muchas formas de implementar esta conversión y he tenido la curiosidad de indagar cuál es la más eficiente.

Quiero comentar que hay librerías de uso bastante común que incorporan esta conversión, por ejemplo:
  • org.apache.commons.codec.binary.Hex.encodeHexString() de la librería Apache Commons Codec
  • org.bouncycastle.util.encoders.Hex.encode() de la librería Bouncy Castle
Pero es más, aunque mucha gente no lo sabe y acaba recurriendo a librerías externas, viene también incluida en la propia distribución de Java: javax.xml.bind.DatatypeConverter.printHexBinary()

Pues bien, ninguna de las implementaciones comentadas resulta ser demasiado eficiente. Así que si necesitáis una conversión que sea rápida, no queréis incluir librerías externas o estáis programando en Android y necesitáis esta conversión porque se dispone del DatatypeConverter en Android, aquí va el método óptimo de conversión. Mediante un benchmark personalizado que construí he podido comprobar que es el doble de rápido que el mejor de los demás que he probado y 100 veces más rápido que las conversiones incluidas en librerías comentadas o la propia DatatypeConverter.printHexBinary()

    private static final char[] hexArray = "0123456789ABCDEF".toCharArray();

    /**
     * Encode the input bytes producing a Hexadecimal output string
     *
     * @param bytes
     * @return Hexadecimal representation of the input bytes
     */
    public static final String byteArrayToHexString(final byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

Todo el mérito y mi reconocimiento es para esta contribución en stackoverflow: http://stackoverflow.com/a/9855338

Cifrado MAC ANSI X9.19 / ISO 9807 / ISO/IEC 9797 ...

Cuando me pidieron un cifrado MAC para firmar un tipo de transaciones bancarias no imaginé cuánto tiempo me llevaría dar con la codificación y la poca información directa que iba a poder encontrar, así que he decidido publicar aquí el resultado de todas mis averiguaciones y aportar una implementación en Java sin necesidad de librerías externas.

Empecemos, un MAC ("Message Authentication Code") es un código que permite validar la autenticidad de un mensaje, por lo tanto, sirve como firma electrónica del mensaje, aunque se difiere de ésta en que un MAC es calculado y verificado con la misma clave, por lo que sólo puede ser verificado por el receptor previsto. Se obtiene aplicando un cifrado con una clave secreta a un mensaje. El resultado lo conocemos también como "checksum".

Como se trataba de entorno bancario, existe un estandar de la banca nacional de EEUU para autenticación de transacciones bancarias que es el ANSI X9.19 (que es una actualización del X9.9, para quién tenga más interés). El algoritmo definido por ANSI X9.19 es conocido como Retail-MAC y es un cifrado de los llamados DES-CBC MAC, es decir cifrado DES ("Data Encryption Standard") en modo CBC ("Cipher Block Chaining"). Existe una norma internacional equivalente que es la ISO 9807 y que es un poco más abierta en cuanto a que permite otros cifrados para obtener el MAC. El cálculo de un MAC como se describe en ANSI X9.19 e ISO 9807 es un caso específico de la norma ISO / IEC 9797 cuando block size 64, MAC length 32, con MAC algorithm 1 o MAC algorithm 3 (ambos con padding method 1) [wikipedia.org], y el cifrado de bloque es DEA ("Data Encryption Algorithm"), como en ocasiones se denomina también a DES. [incits.org]

Cuando hablamos del cifrado de bloques DES, estamos hablando de un método de cifrado ya demasiado vulnerable hoy en día, debido al tamaño de clave de sólo 56 bits; además se sospecha de la existencia de alguna puerta trasera para la NSA ("National Security Agency"). Pero el caso del MAC en cuestión aplica un triple DES, también llamado 3DES ó DESede, lo que aumenta el espacio de claves dificultando su ataque. En concreto 3DES consiste en encriptar usando DES tres veces consecutivas: primero se cifra con una clave, después se descifra con otra y finalmente se vuelve a cifrar con una tercera. Este algoritmo se implementa en ANSI X9.19 de una forma un poco más particular, sólo se utilizan 2 claves y el triple DES sólo se aplica al último bloque:

  1. Rellenar el mensaje a cifrar con el menor número de '00' bytes a la derecha, de tal manera que la longitud del mensaje resultante sea múltiplo de 8 bytes.
  2. Cifrar el mensaje resultante del paso anterior con DES, la clave K1 y modo CBC.
  3. Descifrar el último bloque cifrado en el paso anterior con DES, la clave K2 y modo ECB.
  4. Cifrar el resultado del paso anterior con DES, la clave K1 y modo ECB.
  5. El MAC es el resultado del paso anterior. Para proteger la seguridad de la clave, el MAC puede restringirse, como era el caso que me ocupaba, a los 4 bytes más significativos del resultado obtenido.

Diagrama del cálculo del MAC

JAVA

En Java, el paquete javax.crypto incluye métodos de cifrado, pero no este DES-MAC directamente, así que o incorporamos a nuestro proyecto una librería externa como la de las Bouncy Castle Crypto APIs [bouncycastle.org], o seguimos los pasos descritos más arriba y lo implementamos nosotros a partir la arquitectura criptográfica proporcionada por Java.

Con Bouncy Castle:

Tendremos que incorporar a nuestro proyecto un jar de Providers (bcprov-jdk...jar) de the Legion of the Bouncy Castle Java cryptography APIs [https://www.bouncycastle.org/latest_releases.html], y por lo demás es bastante sencillo:
import org.bouncycastle.crypto.engines.DESEngine;
import org.bouncycastle.crypto.macs.ISO9797Alg3Mac;
import org.bouncycastle.crypto.params.DESedeParameters;
import org.bouncycastle.util.encoders.Hex;

import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;

public class CifradoBouncyCastle {

    public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        byte[] KEY_DATA = hexStringToByteArray("ABCDEF0101010101ABCDEF0202020202");
        DESEngine des = new DESEngine();
        ISO9797Alg3Mac mac = new ISO9797Alg3Mac(des, 32);
        mac.init(new DESedeParameters(KEY_DATA));

        byte[] data = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur cursus vulputate dapibus. Cras sollicitudin aliquet dui, ac pharetra arcu vehicula vel. Cras mattis ultrices ex.".getBytes("utf-8");
        byte[] result = new byte[mac.getMacSize()];

        mac.update(data, 0, data.length);
        mac.doFinal(result, 0);

        System.out.println("firma=" + new String(Hex.encode(result)));
    }

    private static byte[] hexStringToByteArray(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character
                .digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

}

Sin librerías externas:

No es difícil pero, después de buscar durante bastantes horas en Internet, no encontré ningún sitio que aportara una implementación en Java. Lo más parecido a ello es el "Retail MAC Calculation in Java" de Bharathi Subramanian, que tengo que reconocer, que aunque su código no era del todo correcto, me aportó bastante luz.

Ésta es por tanto mi verdadera aportación a todo esto. El código Java para calcular el ANSI X9.19 MAC de 4 bytes. Espero que os sirva de ayuda.
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.util.Arrays;

public class Cifrado {

    public static void main(String[] args) {
        try {
            String claveFirmar = "ABCDEF0101010101ABCDEF0202020202";
            byte[] claveFirmarBytes = hexStringToByteArray(claveFirmar);

            String textoFirmar = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur cursus vulputate dapibus. Cras sollicitudin aliquet dui, ac pharetra arcu vehicula vel. Cras mattis ultrices ex.";
            byte[] textoFirmarBytes = textoFirmar.getBytes("utf-8");

            byte[] allBytes = retailMac(claveFirmarBytes, textoFirmarBytes);
            byte[] firmaBytes = Arrays.copyOf(allBytes, 4);
            String firma = toHex(firmaBytes);

            System.out.println("firma=" + firma);
        } catch (Exception e) {
            e.getStackTrace();
        }
    }

    private static byte[] hexStringToByteArray(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character
                .digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

    final private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b & 0xff));
        }
        return sb.toString();
    }

    private static byte[] retailMac(byte[] key, byte[] data) {
        try {
            // Create Keys
            byte[] key1 = Arrays.copyOf(key, 8);
            byte[] key2 = Arrays.copyOfRange(key, 8, 16);

            // ISO/IEC 9797-1 or ISO 7816d4 Padding for data (adding 80 00 ..)
            byte[] pdata = addPadding(data);

            SecretKeyFactory mySecretKeyFactory = SecretKeyFactory.getInstance("DES");

            DESKeySpec myKeySpec1 = new DESKeySpec(key1);
            SecretKey myKey1 = mySecretKeyFactory.generateSecret(myKeySpec1);
            Cipher cipher1 = Cipher.getInstance("DES/CBC/NoPadding");
            cipher1.init(Cipher.ENCRYPT_MODE, myKey1, new IvParameterSpec(
                    new byte[8]));

            DESKeySpec myKeySpec2 = new DESKeySpec(key2);
            SecretKey myKey2 = mySecretKeyFactory.generateSecret(myKeySpec2);
            Cipher cipher2 = Cipher.getInstance("DES/CBC/NoPadding");
            cipher2.init(Cipher.DECRYPT_MODE, myKey2, new IvParameterSpec(
                    new byte[8]));

            byte[] result = cipher1.doFinal(pdata);

            byte[] block = Arrays.copyOfRange(result, result.length - 8, result.length);

            // Decrypt the resulting block with Key-2
            block = cipher2.doFinal(block);

            // Encrypt the resulting block with Key-1
            block = cipher1.doFinal(block);

            return block;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static byte[] addPadding(byte[] in) {
        int extra = 8 - (in.length % 8);
        int newLength = in.length + extra;
        byte[] out = Arrays.copyOf(in, newLength);
        int offset = in.length;
//        out[offset] = (byte) 0x80;
//        offset++;
        while (offset < newLength) {
            out[offset] = (byte) 0;
            offset++;
        }
        return out;
    }

}

UPDATE:

Ver http://raulcad.blogspot.co.uk/2014/08/convertir-array-de-bytes-en-string-de.html para ver un método más óptimo que el "toHex()" al convertir un array de bytes en un String hexadecimal.