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
519 lines
14 KiB
Markdown
519 lines
14 KiB
Markdown
# 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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`
|
|
|
|
```java
|
|
public byte[] getSignatureKey(String secretKey, String date, String client) {
|
|
return hmacSha256(
|
|
hmacSha256(
|
|
hmacSha256(secretKey.getBytes(StandardCharsets.UTF_8), date),
|
|
client
|
|
),
|
|
"elcano_request"
|
|
);
|
|
}
|
|
```
|
|
|
|
**Pseudocódigo:**
|
|
```python
|
|
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`
|
|
|
|
```java
|
|
public String calculateSignature(String stringToSign) {
|
|
return bytesToHex(
|
|
hmacSha256(
|
|
getSignatureKey(secretKey, dateSimple, client),
|
|
stringToSign
|
|
)
|
|
);
|
|
}
|
|
```
|
|
|
|
**Pseudocódigo:**
|
|
```python
|
|
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`
|
|
|
|
```http
|
|
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`
|
|
|
|
```java
|
|
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`
|
|
|
|
```java
|
|
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`
|
|
|
|
```java
|
|
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`
|
|
|
|
```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:**
|
|
```cpp
|
|
Java_com_adif_commonKeys_GetKeysHelper_getAccessKeyPro
|
|
Java_com_adif_commonKeys_GetKeysHelper_getSecretKeyPro
|
|
```
|
|
|
|
### Extracción de Claves
|
|
|
|
**Opción 1: Ghidra / IDA Pro**
|
|
```bash
|
|
# Abrir libapi-keys.so en Ghidra
|
|
# Buscar las funciones JNI
|
|
# Analizar el código assembly para encontrar los strings
|
|
```
|
|
|
|
**Opción 2: Frida (runtime)**
|
|
```javascript
|
|
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**
|
|
```bash
|
|
strings libapi-keys.so | grep -E "^[A-Za-z0-9+/=]{32,}$"
|
|
```
|
|
|
|
---
|
|
|
|
## Implementación en Python
|
|
|
|
```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**
|
|
```bash
|
|
# 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 ⏳
|