Initial import of ADIF API reverse-engineering toolkit
This commit is contained in:
328
adif_auth.py
Executable file
328
adif_auth.py
Executable file
@@ -0,0 +1,328 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user