#!/usr/bin/env python3 """ ADIF API Authenticator - Réplica del Sistema Original Este módulo es una réplica fiel del algoritmo de autenticación HMAC-SHA256 utilizado por la API de ADIF (El Cano Móvil), obtenido mediante ingeniería reversa del código fuente original en ElcanoAuth.java. El algoritmo sigue el patrón AWS Signature Version 4 con características específicas de ADIF: - Derivación de claves en cascada (date_key -> client_key -> signature_key) - Orden NO alfabético de headers canónicos (crítico para el funcionamiento) - Timestamp en formato ISO 8601 con zona horaria UTC Fuente Original: apk_decompiled/sources/com/adif/elcanomovil/serviceNetworking/interceptors/auth/ElcanoAuth.java Uso: auth = AdifAuthenticator(access_key="YOUR_KEY", secret_key="YOUR_KEY") headers = auth.get_auth_headers("POST", url, payload={...}) response = requests.post(url, json=payload, headers=headers) """ import hashlib import hmac from datetime import datetime import json import uuid from urllib.parse import urlparse class AdifAuthenticator: """ Implementa el algoritmo de autenticación HMAC-SHA256 de ADIF Similar a AWS Signature Version 4 """ # User-keys estáticas (diferentes de las claves HMAC) USER_KEY_CIRCULATION = "f4ce9fbfa9d721e39b8984805901b5df" USER_KEY_STATIONS = "0d021447a2fd2ac64553674d5a0c1a6f" def __init__(self, access_key, secret_key): """ Inicializa el autenticador con las claves HMAC Args: access_key (str): Access key extraída de libapi-keys.so secret_key (str): Secret key extraída de libapi-keys.so """ self.access_key = access_key self.secret_key = secret_key def get_timestamp(self, date=None): """ Genera timestamp en formato ISO 8601 compacto UTC Args: date (datetime): Fecha a formatear (por defecto: ahora) Returns: str: Timestamp en formato yyyyMMddTHHmmssZ Ejemplo: "20251204T204637Z" """ if date is None: date = datetime.utcnow() return date.strftime('%Y%m%dT%H%M%SZ') def get_date(self, date=None): """ Genera fecha en formato compacto Args: date (datetime): Fecha a formatear (por defecto: ahora) Returns: str: Fecha en formato yyyyMMdd Ejemplo: "20251204" """ if date is None: date = datetime.utcnow() return date.strftime('%Y%m%d') def format_payload(self, payload): """ Formatea el payload JSON eliminando espacios y saltos de línea (ElcanoAuth.java:86-91) Args: payload: Diccionario o string con el payload Returns: str: Payload formateado sin espacios Ejemplo: Input: {"page": {"pageNumber": 0}} Output: {"page":{"pageNumber":0}} """ 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): """ Calcula SHA-256 hash en formato hexadecimal (ElcanoAuth.java:185-193) Args: text (str): Texto a hashear Returns: str: Hash SHA-256 en hexadecimal (64 caracteres) """ return hashlib.sha256(text.encode('utf-8')).hexdigest() def hmac_sha256(self, key, data): """ Calcula HMAC-SHA256 (ElcanoAuth.java:117-127) Args: key: Clave (str o bytes) data (str): Datos a firmar Returns: bytes: Firma HMAC-SHA256 (32 bytes) """ 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): """ Genera la clave de firma mediante derivación en cascada (ElcanoAuth.java:109-111) Proceso: kDate = HMAC(secretKey, date) kClient = HMAC(kDate, client) kSigning = HMAC(kClient, "elcano_request") Args: date_simple (str): Fecha en formato yyyyMMdd client (str): Nombre del cliente (ej: "AndroidElcanoApp") Returns: bytes: Clave de firma derivada (32 bytes) """ 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): """ Prepara la petición canónica para firma (ElcanoAuth.java:129-172) Estructura: content-type: x-elcano-client: x-elcano-date: x-elcano-host: x-elcano-userid: content-type;x-elcano-client;x-elcano-date;x-elcano-host;x-elcano-userid Args: method (str): Método HTTP (GET, POST, etc.) path (str): Path de la URL params (str): Query string (puede ser vacío) payload: Body de la petición content_type (str): Content-Type host (str): Host del servidor client (str): Nombre del cliente timestamp (str): Timestamp de la petición user_id (str): UUID del usuario Returns: tuple: (canonical_request, signed_headers) """ # Formatear payload formatted_payload = self.format_payload(payload) payload_hash = self.sha256_hash(formatted_payload) # Headers canónicos (ORDEN ESPECÍFICO, no alfabético completo!) # Nota: El orden DEBE coincidir exactamente con ElcanoAuth.java:137-165 canonical_headers = ( f"content-type:{content_type}\n" f"x-elcano-host:{host}\n" # ← Segundo (antes de client!) f"x-elcano-client:{client}\n" # ← Tercero f"x-elcano-date:{timestamp}\n" # ← Cuarto f"x-elcano-userid:{user_id}\n" # ← Quinto ) # Lista de headers firmados (MISMO orden que canonical_headers) signed_headers = "content-type;x-elcano-host;x-elcano-client;x-elcano-date;x-elcano-userid" # Construir canonical request 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): """ Prepara el string a firmar (ElcanoAuth.java:174-183) Estructura: HMAC-SHA256 ///elcano_request Args: timestamp (str): Timestamp ISO compacto date_simple (str): Fecha simple (yyyyMMdd) client (str): Nombre del cliente user_id (str): UUID del usuario canonical_request (str): Petición canónica Returns: str: String to sign """ 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): """ Calcula la firma final (ElcanoAuth.java:78-84) Args: string_to_sign (str): String preparado para firma date_simple (str): Fecha simple client (str): Nombre del cliente Returns: str: Firma en hexadecimal """ signing_key = self.get_signature_key(date_simple, client) signature_bytes = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).digest() # Convertir a hexadecimal (minúsculas) signature = signature_bytes.hex() return signature def build_authorization_header(self, signature, date_simple, client, user_id, signed_headers): """ Construye el header Authorization (ElcanoAuth.java:61-63) Formato: HMAC-SHA256 Credential=////elcano_request, SignedHeaders=,Signature= Args: signature (str): Firma calculada date_simple (str): Fecha simple client (str): Nombre del cliente user_id (str): UUID del usuario signed_headers (str): Lista de headers firmados Returns: str: Header Authorization completo """ 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, date=None): """ Genera todos los headers necesarios para autenticación Args: method (str): Método HTTP (GET, POST, etc.) url (str): URL completa de la petición payload: Body de la petición (dict o None) user_id (str): UUID del usuario (se genera si no se provee) date (datetime): Fecha de la petición (por defecto: ahora) Returns: dict: Headers completos para la petición Ejemplo: >>> auth = AdifAuthenticator(access_key="...", secret_key="...") >>> headers = auth.get_auth_headers( ... "POST", ... "https://circulacion.api.adif.es/path", ... payload={"page": {"pageNumber": 0}} ... ) >>> headers { "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-...", "Authorization": "HMAC-SHA256 Credential=..." } """ # Parse URL parsed = urlparse(url) host = parsed.netloc path = parsed.path params = parsed.query or "" # Defaults if user_id is None: user_id = str(uuid.uuid4()) if date is None: date = datetime.utcnow() client = "AndroidElcanoApp" content_type = "application/json;charset=utf-8" # Generar timestamps timestamp = self.get_timestamp(date) date_simple = self.get_date(date) # 1. Preparar canonical request canonical_request, signed_headers = self.prepare_canonical_request( method, path, params, payload, content_type, host, client, timestamp, user_id ) # 2. Preparar string to sign string_to_sign = self.prepare_string_to_sign( timestamp, date_simple, client, user_id, canonical_request ) # 3. Calcular firma signature = self.calculate_signature(string_to_sign, date_simple, client) # 4. Construir header Authorization authorization = self.build_authorization_header( signature, date_simple, client, user_id, signed_headers ) # 5. Retornar todos los 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 } def get_user_key_for_url(self, url): """ Obtiene la User-key estática correcta según la URL Args: url (str): URL de la petición Returns: str: User-key correspondiente """ if "circulacion.api.adif.es" in url: return self.USER_KEY_CIRCULATION elif "estaciones.api.adif.es" in url: return self.USER_KEY_STATIONS else: return self.USER_KEY_CIRCULATION # Por defecto def example_usage(): """ Ejemplo de uso del autenticador """ print("="*70) print("ADIF API Authenticator - Ejemplo de Uso") print("="*70) # PASO 1: Obtener las claves de libapi-keys.so # (Usar Ghidra o Frida para extraerlas) print("\n⚠️ IMPORTANTE: Reemplazar con las claves reales extraídas de libapi-keys.so") print(" Ver AUTHENTICATION_ALGORITHM.md para instrucciones de extracción\n") ACCESS_KEY = "and20210615" # ✅ Extraído con Ghidra SECRET_KEY = "Jthjtr946RTt" # ✅ Extraído con Ghidra # PASO 2: Crear el autenticador auth = AdifAuthenticator(access_key=ACCESS_KEY, secret_key=SECRET_KEY) # PASO 3: Preparar la petición url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/departures/traffictype/" payload = { "commercialService": "BOTH", "commercialStopType": "BOTH", "page": {"pageNumber": 0}, "stationCode": "10200", # Madrid Atocha "trafficType": "ALL" } # PASO 4: Generar headers de autenticación headers = auth.get_auth_headers("POST", url, payload=payload) # PASO 5: Añadir User-key estática headers["User-key"] = auth.get_user_key_for_url(url) # PASO 6: Mostrar resultado print("Headers generados:") print("-" * 70) for key, value in headers.items(): print(f"{key}: {value}") print("\n" + "="*70) print("Para hacer la petición:") print("="*70) print(""" import requests response = requests.post( url, json=payload, headers=headers ) print(f"Status: {response.status_code}") print(response.json()) """) if __name__ == "__main__": example_usage()