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:
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.