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:
- 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.
- Cifrar el mensaje resultante del paso anterior con DES, la clave K1 y modo CBC.
- Descifrar el último bloque cifrado en el paso anterior con DES, la clave K2 y modo ECB.
- Cifrar el resultado del paso anterior con DES, la clave K1 y modo ECB.
- 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; } }
Este comentario ha sido eliminado por el autor.
ResponderEliminarHola!
ResponderEliminarLo primero y más importante, muchas gracias por la implementación, que me ha ahorrado bastante tiempo :-) ...
Un mínimo e insignificante detalle es que el algoritmo falla cuando el texto a cifrar es justo un múltiplo de 8. Ya que el addPadding añade 8 bytes 00 extra sin necesidad.
Comprobar con:
TEXTO: HOLAHOLA
CLAVE: 0123456789ABCDEFFEDCBA9876543210
Según BP-Tools la mac (completa) sería
MAC: 3BA3F98DF9211BD6
Según tu algoritmo sale: 7F66147A214E2A4C
Habría que modificar addPadding para evitar el relleno si la entrada
ya es múltiplo de 8, de esa manera sale perfecto.
Hola, como estas, te pudo funcionar el código, ninguno de los que he visto coinciden con bp-tools
EliminarSegún tu algoritmo sale: 7F66147A214E2A4C
ResponderEliminar----------------------
Porque es el MAC caludao en 3DES
------------------------------------
Según BP-Tools la mac (completa) sería
MAC: 3BA3F98DF9211BD6
----------------------
es el retailMAC algoritmo 3 ( calculado en DES después finalizado en 3DES)
------------------------------------
Voili Voilou!
Saludos desde Francia!
----------------------------------------------------------------
ResponderEliminarHabría que modificar addPadding para evitar el relleno si la entrada
ya es múltiplo de 8, de esa manera sale perfecto.
----------------------------------------------------------------
ese eso el problema!
Porque la salida con el bueno padding es buena
--------------------------------
extra= 8 newLength= 8 out.length = 8 out= 484F4C41484F4C41 offset = 8
extra= 8 newLength= 8 out.length = 8 out= 484F4C41484F4C41
length(pdata)= 8 pdata = 484F4C41484F4C41
length(pdata)= 8 result = CD3C0D1991F4E8E0
length(pdata)= 8 block = C1755F2EFAF6B0D0
length(pdata)= 8 block = 3BA3F98DF9211BD6
firma=3BA3F98DF9211BD6 textoFirmarBytes= 484F4C41484F4C41 firmaBytes=3BA3F98DF9211BD6
================ READY ================
--------------------------------
Y para mensaje mayor que 8 Bytes debiera haber una bucle par llamar unicamente 8 bytes
- hacer un XOR con el "result"
y in final hacer un
// Decrypt the resulting block with Key-2
block = cipher2.doFinal(block);
// Encrypt the resulting block with Key-1
block = cipher1.doFinal(block);
Eso es para mi el bueno algoritmo
Voili Voilou!
Saludos desde Francia!
Gracia mi ha ayudado a hacer mi programa
ResponderEliminar##############################################################
[2018-02-25 10:31:05] BP-Tools - Cryptographic Calculator is ready
********************
[2018-02-25 10:31:06]
Retail MAC operation finished
****************************************
Key: 0123456789ABCDEFFEDCBA9876543210
Algorithm: DES
Finalize: 3DES
Data: 484F4C41484F4C41484F4C41
----------------------------------------
MAC: 95834C50FD693AF7
##############################################################
nbBlock= 2 length(pdata)= 16 pdata = 484F4C41484F4C41484F4C4100000000
i = 0 nbBlock = 2 inBlock(0) = 484F4C41484F4C41 --> result = CD3C0D1991F4E8E0
i = 1 nbBlock = 2 inBlock(1) = 484F4C4100000000 --> result = C1A7843A39EA429D
cipher2 length(block)= 8 blockin = C1A7843A39EA429D --> blockout = 546F19D208E2263C
cipher2 length(block)= 8 blockin = 546F19D208E2263C --> blockout = 95834C50FD693AF7
textoFirmarBytes= 484F4C41484F4C41484F4C41 --> firmaBytes = 95834C50FD693AF7
================ READY ================
Voili Voilou!
Saludos desde Francia ... pero de un portugués que creció aquí ;)!
Hola Raul, Necesito realizar un firmado de mensaje siguiendo las especificaciones de cálculo del MAC con clave de doble longitud según ISO 9797:1999 algoritmo 3 y método de padeo 1. ¿Puedes confirmarme si con el uso del ejemplo de bouncy cumpliría estos requisitos? Las pruebas iniciales no son satisfactorias y te pido confirmación si puedes :-)
ResponderEliminarGracias!
Creo que el comentario 2 es el más correcto sería ir aplicando DES sobre todos los Ei hasta llegar al En donde se finaliza con un 3DES
ResponderEliminarGenial aporte, lástima que no se nada de Java.
ResponderEliminarTienes alguna implementación en PHP?
Muy buen aporte. Cuenta con alguna implementación para PHP?
ResponderEliminar