Files
adif-api-reverse-engineering/docs/AUTHENTICATION_ALGORITHM.md
Dasemu 68fac80520 Refactor: reorganización completa del proyecto y documentación consolidada
Esta actualización reorganiza el proyecto de reverse engineering de la API de ADIF con los siguientes cambios:

Estructura del proyecto:
- Movida documentación principal a carpeta docs/
- Consolidados archivos markdown redundantes en CLAUDE.md (contexto completo del proyecto)
- Organización de tests en carpeta tests/ con README explicativo
- APK renombrado de base.apk a adif.apk para mayor claridad

Archivos de código:
- Movidos adif_auth.py y adif_client.py a la raíz (antes en api_testing_scripts/)
- Eliminados scripts de testing obsoletos y scripts de Frida no utilizados
- Nuevos tests detallados: test_endpoints_detailed.py y test_onepaths_with_real_trains.py

Descubrimientos:
- Documentados nuevos hallazgos en docs/NEW_DISCOVERIES.md
- Actualización de onePaths funcionando con commercialNumber real (devuelve 200)
- Extraídos 1587 códigos de estación en station_codes.txt

Configuración:
- Actualizado .gitignore con mejores patrones para Python e IDEs
- Eliminados archivos temporales de depuración y logs
2025-12-05 11:22:13 +01:00

14 KiB

Algoritmo de Autenticación ADIF - Ingeniería Reversa Completa

Status: Algoritmo completamente descifrado

Pendiente: Extracción de claves secretas de libapi-keys.so

Resumen Ejecutivo

El sistema de autenticación de ADIF es similar a AWS Signature Version 4:

  • Usa HMAC-SHA256 para firmar peticiones
  • Requiere dos claves secretas: accessKey y secretKey
  • Las claves están en la librería nativa libapi-keys.so (ofuscadas)
  • Genera headers dinámicos para cada petición

Archivo Fuente del Algoritmo

Ubicación: com/adif/elcanomovil/serviceNetworking/interceptors/auth/ElcanoAuth.java

Líneas clave:

  • 47-53: Cálculo del header Authorization
  • 129-172: Preparación del Canonical Request
  • 174-183: Preparación del String to Sign
  • 78-84: Cálculo de la firma
  • 109-111: Generación de la clave de firma (Signature Key)

Paso a Paso del Algoritmo

1. Parámetros de Entrada

// Desde ElcanoClientAuth.Builder
String elcanoAccessKey;      // Clave de acceso (de libapi-keys.so)
String elcanoSecretKey;      // Clave secreta (de libapi-keys.so)
String host;                 // Ej: "circulacion.api.adif.es"
String path;                 // Ej: "/portroyalmanager/secure/circulationpaths/departures/traffictype/"
String params;               // Query string (puede ser "")
String httpMethodName;       // "GET" o "POST"
String payload;              // Body JSON (sin espacios ni saltos de línea)
String contentType;          // "application/json;charset=utf-8"
String xElcanoClient;        // "AndroidElcanoApp"
String xElcanoUserId;        // UUID persistente del usuario
Date requestDate;            // Fecha/hora actual

2. Formato de Fechas

// ElcanoAuth.java:195-199
public static String getTimeStamp(Date date) {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
    simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
    return simpleDateFormat.format(date);
}
// Ejemplo: "20251204T204637Z"

// ElcanoAuth.java:55-59
public static String getDate(Date date) {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
    simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
    return simpleDateFormat.format(date);
}
// Ejemplo: "20251204"

3. Preparar el Payload

// ElcanoAuth.java:86-91
public String formatPayload(String str) {
    if (str == null) {
        str = "";
    }
    return str.replace("\r", "").replace("\n", "").replace(" ", "");
}

Ejemplo:

Input:  {"page": {"pageNumber": 0}}
Output: {"page":{"pageNumber":0}}

4. Canonical Request

Archivo: ElcanoAuth.java:129-172

Estructura:

<HTTPMethod>\n
<Path>\n
<QueryString>\n
content-type:<ContentType>\n
x-elcano-host:<Host>\n
x-elcano-client:<Client>\n
x-elcano-date:<Timestamp>\n
x-elcano-userid:<UserId>\n
content-type;x-elcano-host;x-elcano-client;x-elcano-date;x-elcano-userid\n
<SHA256HashOfPayload>

Ejemplo real:

POST
/portroyalmanager/secure/circulationpaths/departures/traffictype/

content-type:application/json;charset=utf-8
x-elcano-host:circulacion.api.adif.es
x-elcano-client:AndroidElcanoApp
x-elcano-date:20251204T204637Z
x-elcano-userid:a1b2c3d4-e5f6-7890-abcd-ef1234567890
content-type;x-elcano-host;x-elcano-client;x-elcano-date;x-elcano-userid
<sha256_hash_of_payload_hex>

Nota importante: Los headers deben estar en minúsculas y en orden alfabético.

5. String to Sign

Archivo: ElcanoAuth.java:174-183

Estructura:

HMAC-SHA256\n
<Timestamp>\n
<DateSimple>/<Client>/<UserId>/elcano_request\n
<SHA256HashOfCanonicalRequest>

Ejemplo:

HMAC-SHA256
20251204T204637Z
20251204/AndroidElcanoApp/a1b2c3d4-e5f6-7890-abcd-ef1234567890/elcano_request
<sha256_hash_of_canonical_request_hex>

6. Signature Key (Clave de Firma)

Archivo: ElcanoAuth.java:109-111

public byte[] getSignatureKey(String secretKey, String date, String client) {
    return hmacSha256(
        hmacSha256(
            hmacSha256(secretKey.getBytes(StandardCharsets.UTF_8), date),
            client
        ),
        "elcano_request"
    );
}

Pseudocódigo:

kDate = HMAC_SHA256(secretKey, date)                  # "20251204"
kClient = HMAC_SHA256(kDate, client)                  # "AndroidElcanoApp"
kSigning = HMAC_SHA256(kClient, "elcano_request")

7. Signature (Firma Final)

Archivo: ElcanoAuth.java:78-84

public String calculateSignature(String stringToSign) {
    return bytesToHex(
        hmacSha256(
            getSignatureKey(secretKey, dateSimple, client),
            stringToSign
        )
    );
}

Pseudocódigo:

signatureKey = getSignatureKey(secretKey, "20251204", "AndroidElcanoApp")
signature = HMAC_SHA256(signatureKey, stringToSign)
signatureHex = signature.hex()

8. Authorization Header

Archivo: ElcanoAuth.java:61-63

Formato:

HMAC-SHA256 Credential=<accessKey>/<date>/<client>/<userId>/elcano_request,SignedHeaders=<signedHeaders>,Signature=<signature>

Ejemplo:

HMAC-SHA256 Credential=ACCESS_KEY_HERE/20251204/AndroidElcanoApp/a1b2c3d4-e5f6-7890-abcd-ef1234567890/elcano_request,SignedHeaders=content-type;x-elcano-host;x-elcano-client;x-elcano-date;x-elcano-userid,Signature=a1b2c3d4e5f6789...

9. Headers Finales de la Petición

Archivo: ElcanoAuth.java:97-107

Content-Type: application/json;charset=utf-8
X-Elcano-Host: circulacion.api.adif.es
X-Elcano-Client: AndroidElcanoApp
X-Elcano-Date: 20251204T204637Z
X-Elcano-UserId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Authorization: HMAC-SHA256 Credential=...

Nota: Estos reemplazan a los headers X-CanalMovil-* que pensábamos inicialmente.


Funciones Helper

HMAC-SHA256

Archivo: ElcanoAuth.java:117-127

public byte[] hmacSha256(byte[] key, String data) {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(key, "HmacSHA256"));
    return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
}

SHA-256 Hash (Hex)

Archivo: ElcanoAuth.java:185-193

public String toHex(String str) {
    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
    return String.format("%064x", new BigInteger(1, messageDigest.digest()));
}

Bytes to Hex

Archivo: ElcanoAuth.java:65-76

public String bytesToHex(byte[] bytes) {
    char[] hexArray = "0123456789ABCDEF".toCharArray();
    char[] hexChars = new char[bytes.length * 2];
    for (int i = 0; i < bytes.length; i++) {
        int v = bytes[i] & 0xFF;
        hexChars[i * 2] = hexArray[v >>> 4];
        hexChars[i * 2 + 1] = hexArray[v & 0x0F];
    }
    return new String(hexChars).toLowerCase();
}

Claves Secretas

Ubicación

Archivo: com/adif/commonKeys/GetKeysHelper.java

public final class GetKeysHelper {
    static {
        System.loadLibrary("api-keys");  // Carga libapi-keys.so
    }

    private final native String getAccessKeyPro();
    private final native String getSecretKeyPro();

    public final String a() {
        return getAccessKeyPro();
    }

    public final String b() {
        return getSecretKeyPro();
    }
}

Librería nativa:

  • lib/x86_64/libapi-keys.so (446 KB)
  • lib/arm64-v8a/libapi-keys.so (503 KB)
  • lib/x86/libapi-keys.so (416 KB)
  • lib/armeabi-v7a/libapi-keys.so (366 KB)

Funciones JNI:

Java_com_adif_commonKeys_GetKeysHelper_getAccessKeyPro
Java_com_adif_commonKeys_GetKeysHelper_getSecretKeyPro

Extracción de Claves

Opción 1: Ghidra / IDA Pro

# Abrir libapi-keys.so en Ghidra
# Buscar las funciones JNI
# Analizar el código assembly para encontrar los strings

Opción 2: Frida (runtime)

Java.perform(function() {
    var GetKeysHelper = Java.use('com.adif.commonKeys.GetKeysHelper');

    console.log('[+] Access Key: ' + GetKeysHelper.f4297a.a());
    console.log('[+] Secret Key: ' + GetKeysHelper.f4297a.b());
});

Opción 3: Strings + Análisis manual

strings libapi-keys.so | grep -E "^[A-Za-z0-9+/=]{32,}$"

Implementación en Python

import hashlib
import hmac
from datetime import datetime
import json

class AdifAuthenticator:
    def __init__(self, access_key, secret_key):
        self.access_key = access_key
        self.secret_key = secret_key

    def get_timestamp(self, date=None):
        if date is None:
            date = datetime.utcnow()
        return date.strftime('%Y%m%dT%H%M%SZ')

    def get_date(self, date=None):
        if date is None:
            date = datetime.utcnow()
        return date.strftime('%Y%m%d')

    def format_payload(self, payload):
        if payload is None:
            return ""
        if isinstance(payload, dict):
            payload = json.dumps(payload, separators=(',', ':'))
        return payload.replace('\r', '').replace('\n', '').replace(' ', '')

    def sha256_hash(self, text):
        return hashlib.sha256(text.encode('utf-8')).hexdigest()

    def hmac_sha256(self, key, data):
        if isinstance(key, str):
            key = key.encode('utf-8')
        return hmac.new(key, data.encode('utf-8'), hashlib.sha256).digest()

    def get_signature_key(self, date_simple, client):
        k_date = self.hmac_sha256(self.secret_key, date_simple)
        k_client = self.hmac_sha256(k_date, client)
        k_signing = self.hmac_sha256(k_client, "elcano_request")
        return k_signing

    def prepare_canonical_request(self, method, path, params, payload,
                                  content_type, host, client, timestamp, user_id):
        # Formatear payload
        formatted_payload = self.format_payload(payload)
        payload_hash = self.sha256_hash(formatted_payload)

        # Headers canónicos (en orden alfabético, minúsculas)
        canonical_headers = (
            f"content-type:{content_type}\n"
            f"x-elcano-client:{client}\n"
            f"x-elcano-date:{timestamp}\n"
            f"x-elcano-host:{host}\n"
            f"x-elcano-userid:{user_id}\n"
        )

        signed_headers = "content-type;x-elcano-client;x-elcano-date;x-elcano-host;x-elcano-userid"

        canonical_request = (
            f"{method}\n"
            f"{path}\n"
            f"{params}\n"
            f"{canonical_headers}"
            f"{signed_headers}\n"
            f"{payload_hash}"
        )

        return canonical_request, signed_headers

    def prepare_string_to_sign(self, timestamp, date_simple, client, user_id, canonical_request):
        canonical_hash = self.sha256_hash(canonical_request)

        string_to_sign = (
            f"HMAC-SHA256\n"
            f"{timestamp}\n"
            f"{date_simple}/{client}/{user_id}/elcano_request\n"
            f"{canonical_hash}"
        )

        return string_to_sign

    def calculate_signature(self, string_to_sign, date_simple, client):
        signing_key = self.get_signature_key(date_simple, client)
        signature = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
        return signature

    def build_authorization_header(self, signature, date_simple, client, user_id, signed_headers):
        return (
            f"HMAC-SHA256 "
            f"Credential={self.access_key}/{date_simple}/{client}/{user_id}/elcano_request,"
            f"SignedHeaders={signed_headers},"
            f"Signature={signature}"
        )

    def get_auth_headers(self, method, url, payload=None, user_id=None):
        # Parse URL
        from urllib.parse import urlparse
        parsed = urlparse(url)
        host = parsed.netloc
        path = parsed.path
        params = parsed.query or ""

        # Defaults
        if user_id is None:
            import uuid
            user_id = str(uuid.uuid4())

        client = "AndroidElcanoApp"
        content_type = "application/json;charset=utf-8"

        # Timestamps
        now = datetime.utcnow()
        timestamp = self.get_timestamp(now)
        date_simple = self.get_date(now)

        # 1. Canonical Request
        canonical_request, signed_headers = self.prepare_canonical_request(
            method, path, params, payload, content_type, host, client, timestamp, user_id
        )

        # 2. String to Sign
        string_to_sign = self.prepare_string_to_sign(
            timestamp, date_simple, client, user_id, canonical_request
        )

        # 3. Signature
        signature = self.calculate_signature(string_to_sign, date_simple, client)

        # 4. Authorization Header
        authorization = self.build_authorization_header(
            signature, date_simple, client, user_id, signed_headers
        )

        # Return all headers
        return {
            "Content-Type": content_type,
            "X-Elcano-Host": host,
            "X-Elcano-Client": client,
            "X-Elcano-Date": timestamp,
            "X-Elcano-UserId": user_id,
            "Authorization": authorization
        }

# USO:
# auth = AdifAuthenticator(access_key="ACCESS_KEY_AQUI", secret_key="SECRET_KEY_AQUI")
# headers = auth.get_auth_headers("POST", "https://circulacion.api.adif.es/path", payload={...})

Próximos Pasos

1. Extraer las Claves

Método recomendado: Ghidra

# 1. Instalar Ghidra
wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.0_build/ghidra_11.0_PUBLIC_20231222.zip

# 2. Abrir libapi-keys.so
./ghidra

# 3. Buscar funciones:
#    - getAccessKeyPro
#    - getSecretKeyPro

# 4. Analizar el código assembly
# 5. Encontrar los strings hardcodeados

2. Probar el Algoritmo

Una vez tengamos las claves, podemos probar con el script Python.

3. Validar contra API Real

Hacer peticiones y confirmar que funcionan.


Referencias

  • ElcanoAuth.java: serviceNetworking/interceptors/auth/ElcanoAuth.java
  • ElcanoClientAuth.java: serviceNetworking/interceptors/auth/ElcanoClientAuth.java
  • GetKeysHelper.java: commonKeys/GetKeysHelper.java
  • libapi-keys.so: lib/*/libapi-keys.so

Última actualización: 2025-12-04 Status: Algoritmo completo | Claves pendientes