Files
adif-api-reverse-engineering/adif_auth.py

329 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"""
ADIF API Authenticator - Replica of the Original System
This module is a faithful replica of the HMAC-SHA256 authentication algorithm
used by the ADIF API (El Cano Movil), obtained through reverse engineering
of the original source code in ElcanoAuth.java.
The algorithm follows the AWS Signature Version 4 pattern with ADIF-specific
characteristics:
- Cascading key derivation (date_key -> client_key -> signature_key)
- NON-alphabetical canonical headers order (critical for functionality)
- Timestamp in ISO 8601 format with UTC timezone
Original Source:
apk_decompiled/sources/com/adif/elcanomovil/serviceNetworking/interceptors/auth/ElcanoAuth.java
Usage:
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)
"""
from __future__ import annotations
import hashlib
import hmac
import json
import uuid
from datetime import datetime
from typing import Any, Dict, Optional, Tuple, Union
from urllib.parse import urlparse
class AdifAuthenticator:
"""
Implements the ADIF HMAC-SHA256 authentication algorithm.
The flow mirrors AWS Signature Version 4 with ADIF-specific header ordering.
"""
# Static User-keys (different from HMAC keys)
USER_KEY_CIRCULATION: str = "f4ce9fbfa9d721e39b8984805901b5df"
USER_KEY_STATIONS: str = "0d021447a2fd2ac64553674d5a0c1a6f"
def __init__(self, access_key: str, secret_key: str) -> None:
"""
Initialize the authenticator with HMAC keys extracted from libapi-keys.so.
"""
self.access_key: str = access_key
self.secret_key: str = secret_key
def get_timestamp(self, date: Optional[datetime] = None) -> str:
"""
Generate a timestamp in compact ISO 8601 UTC format (yyyyMMddTHHmmssZ).
"""
if date is None:
date = datetime.utcnow()
return date.strftime("%Y%m%dT%H%M%SZ")
def get_date(self, date: Optional[datetime] = None) -> str:
"""
Generate a compact date string (yyyyMMdd).
"""
if date is None:
date = datetime.utcnow()
return date.strftime("%Y%m%d")
def format_payload(self, payload: Optional[Union[Dict[str, Any], str]]) -> str:
"""
Serialize the JSON payload removing spaces and line breaks, matching
the behavior in `ElcanoAuth.java` (lines 86-91).
"""
if payload is None:
return ""
if isinstance(payload, dict):
payload = json.dumps(payload, separators=(",", ":"))
cleaned_payload = payload.replace("\r", "").replace("\n", "").replace(" ", "")
return cleaned_payload
def sha256_hash(self, text: str) -> str:
"""
Calculate a SHA-256 hex digest (64 chars) for the provided text.
"""
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def hmac_sha256(self, key: Union[str, bytes], data: str) -> bytes:
"""
Calculate an HMAC-SHA256 signature using the provided key and data.
"""
encoded_key = key.encode("utf-8") if isinstance(key, str) else key
return hmac.new(encoded_key, data.encode("utf-8"), hashlib.sha256).digest()
def get_signature_key(self, date_simple: str, client: str) -> bytes:
"""
Derive the signature key using the cascading HMAC steps defined in the APK.
"""
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: str,
path: str,
params: str,
payload: Optional[Union[Dict[str, Any], str]],
content_type: str,
host: str,
client: str,
timestamp: str,
user_id: str,
) -> Tuple[str, str]:
"""
Build the canonical request string that is hashed and signed.
Returns:
A tuple of (canonical_request, signed_headers).
"""
formatted_payload = self.format_payload(payload)
payload_hash = self.sha256_hash(formatted_payload)
# Canonical headers (SPECIFIC ORDER, not alphabetical)
canonical_headers = (
f"content-type:{content_type}\n"
f"x-elcano-host:{host}\n" # Host must precede client
f"x-elcano-client:{client}\n"
f"x-elcano-date:{timestamp}\n"
f"x-elcano-userid:{user_id}\n"
)
signed_headers = (
"content-type;x-elcano-host;x-elcano-client;x-elcano-date;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: str,
date_simple: str,
client: str,
user_id: str,
canonical_request: str,
) -> str:
"""
Build the string-to-sign payload that feeds the final HMAC.
"""
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: str, date_simple: str, client: str
) -> str:
"""
Compute the final hexadecimal signature for the request.
"""
signing_key = self.get_signature_key(date_simple, client)
signature_bytes = hmac.new(
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
).digest()
signature = signature_bytes.hex()
return signature
def build_authorization_header(
self,
signature: str,
date_simple: str,
client: str,
user_id: str,
signed_headers: str,
) -> str:
"""
Format the Authorization header expected by the ADIF backend.
"""
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: str,
url: str,
payload: Optional[Union[Dict[str, Any], str]] = None,
user_id: Optional[str] = None,
date: Optional[datetime] = None,
) -> Dict[str, str]:
"""
Generate all headers required to authenticate a single request.
"""
parsed = urlparse(url)
host = parsed.netloc
path = parsed.path
params = parsed.query or ""
effective_user_id = user_id or str(uuid.uuid4())
effective_date = date or datetime.utcnow()
client = "AndroidElcanoApp"
content_type = "application/json;charset=utf-8"
timestamp = self.get_timestamp(effective_date)
date_simple = self.get_date(effective_date)
canonical_request, signed_headers = self.prepare_canonical_request(
method,
path,
params,
payload,
content_type,
host,
client,
timestamp,
effective_user_id,
)
string_to_sign = self.prepare_string_to_sign(
timestamp, date_simple, client, effective_user_id, canonical_request
)
signature = self.calculate_signature(string_to_sign, date_simple, client)
authorization = self.build_authorization_header(
signature, date_simple, client, effective_user_id, signed_headers
)
return {
"Content-Type": content_type,
"X-Elcano-Host": host,
"X-Elcano-Client": client,
"X-Elcano-Date": timestamp,
"X-Elcano-UserId": effective_user_id,
"Authorization": authorization,
}
def get_user_key_for_url(self, url: str) -> str:
"""
Return the static User-key associated with the target host.
"""
if "circulacion.api.adif.es" in url:
return self.USER_KEY_CIRCULATION
if "estaciones.api.adif.es" in url:
return self.USER_KEY_STATIONS
return self.USER_KEY_CIRCULATION # Default
def example_usage() -> None:
"""
Example usage of the authenticator.
"""
print("=" * 70)
print("ADIF API Authenticator - Usage Example")
print("=" * 70)
# STEP 1: Get keys from libapi-keys.so
# (Use Ghidra or Frida to extract them)
print("\nIMPORTANT: Replace with real keys extracted from libapi-keys.so")
print(" See AUTHENTICATION_ALGORITHM.md for extraction instructions\n")
ACCESS_KEY = "and20210615" # Extracted with Ghidra
SECRET_KEY = "Jthjtr946RTt" # Extracted with Ghidra
# STEP 2: Create authenticator
auth = AdifAuthenticator(access_key=ACCESS_KEY, secret_key=SECRET_KEY)
# STEP 3: Prepare request
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",
}
# STEP 4: Generate authentication headers
headers = auth.get_auth_headers("POST", url, payload=payload)
# STEP 5: Add static User-key
headers["User-key"] = auth.get_user_key_for_url(url)
# STEP 6: Show result
print("Generated headers:")
print("-" * 70)
for key, value in headers.items():
print(f"{key}: {value}")
print("\n" + "=" * 70)
print("To make the request:")
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()