# Request Body Analysis - ADIF API > Reverse engineering of package `com.adif.elcanomovil.serviceNetworking` > Date: 2025-12-04 ## Table of Contents - [1. Authentication Headers](#1-authentication-headers) - [2. Request Bodies](#2-request-bodies) - [3. Endpoints and Base URLs](#3-endpoints-and-base-urls) - [4. Network Configuration](#4-network-configuration) - [5. Authentication System](#5-authentication-system) - [6. Code References](#6-code-references) --- ## 1. Authentication Headers ### 1.1 Static Headers **File:** `ServicePaths.java:67-76` #### Circulations ``` User-key: f4ce9fbfa9d721e39b8984805901b5df Content-Type: application/json;charset=utf-8 ``` #### Stations ``` User-key: 0d021447a2fd2ac64553674d5a0c1a6f Content-Type: application/json;charset=utf-8 ``` #### AVISA (Login/Refresh) ``` Authorization: Basic YXZpc3RhX2NsaWVudF9hbmRyb2lkOjh5WzZKNyFmSjwhXypmYXE1NyNnOSohNElwa2MjWC1BTg== ``` **Decoded (Base64):** ``` avista_client_android:8y[6J7!fJ<_*faq57#g9*!4Ipkc#X-AN ``` ### 1.2 Dynamic Headers (Generated by ElcanoAuth) **File:** `serviceNetworking/interceptors/auth/ElcanoAuth.java` The interceptor adds the X-Elcano headers produced by the HMAC calculation: ``` X-Elcano-Host: X-Elcano-Client: AndroidElcanoApp X-Elcano-Date: X-Elcano-UserId: Authorization: HMAC-SHA256 Credential=... SignedHeaders=... Signature=... ``` **Generation algorithm (summary):** - Canonical headers (order matters): `content-type`, `x-elcano-host`, `x-elcano-client`, `x-elcano-date`, `x-elcano-userid` - Canonical request: method + path + query + canonical headers + SHA256(payload) - String to sign: `HMAC-SHA256\n\n///elcano_request\n` - Signature key: HMAC(secretKey, date) → HMAC(result, client) → HMAC(result, "elcano_request") --- ## 2. Request Bodies ### 2.1 Circulations - Departures/Arrivals/Between Stations **Endpoints:** - `/portroyalmanager/secure/circulationpaths/departures/traffictype/` - `/portroyalmanager/secure/circulationpaths/arrivals/traffictype/` - `/portroyalmanager/secure/circulationpaths/betweenstations/traffictype/` **Model:** `TrafficCirculationPathRequest` **File:** `circulations/model/request/TrafficCirculationPathRequest.java:10-212` ```json { "commercialService": "YES|NOT|BOTH", "commercialStopType": "YES|NOT|BOTH", "destinationStationCode": "string or null", "originStationCode": "string or null", "page": { "pageNumber": 0 }, "stationCode": "string or null", "trafficType": "CERCANIAS|AVLDMD|OTHERS|TRAVELERS|GOODS|ALL" } ``` #### Real Example ```json { "commercialService": "BOTH", "commercialStopType": "BOTH", "page": { "pageNumber": 0 }, "stationCode": "60000", "trafficType": "ALL" } ``` #### Allowed Values **commercialService / commercialStopType** (`CirculationPathRequest.java:65-67`): - `YES` - Only commercial services/stops - `NOT` - Without commercial services/stops - `BOTH` - All types **trafficType** (`TrafficType.java:16-21`): - `CERCANIAS` - Commuter trains - `AVLDMD` - High speed long/medium distance - `OTHERS` - Other types - `TRAVELERS` - Passengers - `GOODS` - Freight - `ALL` - All types --- ### 2.2 Circulations - Specific Routes **Endpoints:** - `/portroyalmanager/secure/circulationpathdetails/onepaths/` - `/portroyalmanager/secure/circulationpathdetails/severalpaths/` **Model:** `OneOrSeveralPathsRequest` **File:** `circulations/model/request/OneOrSeveralPathsRequest.java:11-140` ```json { "allControlPoints": true/false/null, "commercialNumber": "string or null", "destinationStationCode": "string or null", "launchingDate": 1733356800000, "originStationCode": "string or null" } ``` #### Real Example ```json { "allControlPoints": true, "commercialNumber": "04138", "destinationStationCode": "60000", "launchingDate": 1733356800000, "originStationCode": "71801" } ``` **Important notes:** - `launchingDate` is a **millisecond** timestamp (Long in Java) - `allControlPoints` indicates whether to include every control point in the route - Optional fields should be omitted rather than set to `null` --- ### 2.3 Train Compositions **Endpoint:** `/portroyalmanager/secure/circulationpaths/compositions/path/` **Model:** `OneOrSeveralPathsRequest` (same as routes) **File:** `compositions/CompositionsService.java:14-18` ```json { "allControlPoints": true/false/null, "commercialNumber": "string or null", "destinationStationCode": "string or null", "launchingDate": 1733356800000, "originStationCode": "string or null" } ``` --- ### 2.4 Stations - Single Station Details **Endpoint:** `/portroyalmanager/secure/stations/onestation/` **Model:** `OneStationRequest` **File:** `stations/model/OneStationRequest.java:9-93` ```json { "detailedInfo": { "extendedStationInfo": true, "stationActivities": true, "stationBanner": true, "stationCommercialServices": true, "stationInfo": true, "stationServices": true, "stationTransportServices": true }, "stationCode": "60000", "token": "string" } ``` **Important notes:** - The `detailedInfo` object controls what is returned - All boolean fields default to `true` (see `DetailedInfoDTO.java:149`) - `token` is required #### DetailedInfo fields **File:** `stations/model/DetailedInfoDTO.java:10-151` | Field | Type | Description | |-------|------|-------------| | `extendedStationInfo` | boolean | Extended station information | | `stationActivities` | boolean | Station activities | | `stationBanner` | boolean | Banner/announcements | | `stationCommercialServices` | boolean | Commercial services | | `stationInfo` | boolean | Basic station information | | `stationServices` | boolean | Available services | | `stationTransportServices` | boolean | Transport services | --- ### 2.5 Station Observations **Endpoint:** `/portroyalmanager/secure/stationsobservations/` **Model:** `StationObservationsRequest` **File:** `stationObservations/model/StationObservationsRequest.java:10-53` ```json { "stationCodes": ["60000", "71801"] } ``` #### Real Example ```json { "stationCodes": ["60000", "71801", "79600"] } ``` **Notes:** - Array of station codes (strings) - Required field - Can contain multiple codes --- ## 3. Endpoints and Base URLs ### 3.1 Base URLs **File:** `di/NetworkModule.java:73-159` | Service | Base URL | Authentication | |---------|----------|----------------| | **Circulations** | `https://circulacion.api.adif.es` | Secured (with AuthHeaderInterceptor) | | **Stations** | `https://estaciones.api.adif.es` | Secured (with AuthHeaderInterceptor) | | **AVISA** | `https://avisa.adif.es` | Basic (without AuthHeaderInterceptor) | | **Elcano Web** | `https://elcanoweb.adif.es/api/` | - | ### 3.2 Full Paths - Stations **File:** `ServicePaths.java:106-112` ``` GET /portroyalmanager/secure/stations/allstations/reducedinfo/{token}/ POST /portroyalmanager/secure/stations/onestation/ POST /portroyalmanager/secure/stationsobservations/ ``` ### 3.3 Full Paths - Circulations **File:** `ServicePaths.java:41-51` ``` POST /portroyalmanager/secure/circulationpaths/departures/traffictype/ POST /portroyalmanager/secure/circulationpaths/arrivals/traffictype/ POST /portroyalmanager/secure/circulationpaths/betweenstations/traffictype/ POST /portroyalmanager/secure/circulationpathdetails/onepaths/ POST /portroyalmanager/secure/circulationpathdetails/severalpaths/ ``` ### 3.4 Full Paths - Compositions **File:** `ServicePaths.java:55-61` ``` POST /portroyalmanager/secure/circulationpaths/compositions/path/ ``` ### 3.5 Full Paths - AVISA **Files:** `ServicePaths.java:82-92` and `ServicePaths.java:29-37` ``` POST /avisa-ws/api/token (login) POST /avisa-ws/api/token (refresh) POST /avisa-ws/api/v1/client (register) GET /avisa-ws/api/v1/station (stations) GET /avisa-ws/api/v1/category (categories) GET /avisa-ws/api/v1/incidence (incidences list) GET /avisa-ws/api/v1/incidence/{id} (incidence details) POST /avisa-ws/api/v1/incidence (create incidence) ``` --- ## 4. Network Configuration ### 4.1 OkHttpClient Configuration **File:** `di/NetworkModule.java:100-132` #### Basic client ```kotlin OkHttpClient.Builder() .certificatePinner(certificatePinner) .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build() ``` #### Secured client (with authentication) ```kotlin OkHttpClient.Builder() .addInterceptor(AuthHeaderInterceptor(userId)) .certificatePinner(certificatePinner) .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build() ``` **Timeouts:** - Connect timeout: 60 seconds - Read timeout: 60 seconds ### 4.2 Services using the secured client **File:** `di/NetworkModule.java` - `CirculationService` (line 73) - `StationsService` (line 142) - `StationObservationsService` (line 135) - `CompositionsService` (line 156) ### 4.3 Services using the basic client - `AvisaLoginService` (line 50) - `AvisaStationsService` (line 57) - `IncidenceService` (line 80) - `SubscriptionsService` (line 149) --- ## 5. Authentication System ### 5.1 AuthHeaderInterceptor **File:** `interceptors/AuthHeaderInterceptor.java:27-84` This interceptor runs on **every** request for secured services. #### Authentication process 1. **Generate persistent User ID** - Uses `GeneratePersistentUserIdUseCase` - The ID is stored and reused between sessions 2. **Build the token** ```java ElcanoClientAuth.Builder() .host(request.url().host()) .contentType("application/json;charset=utf-8") .path(request.url().encodedPath()) .params(request.url().encodedQuery()) .xElcanoClient("AndroidElcanoApp") .xElcanoUserId(userId) .httpMethodName(request.method()) .payload(bodyJsonWithoutSpaces) .build() ``` 3. **Generate headers** - The `ElcanoClientAuth` object generates authentication headers - They are added automatically to the request #### Generated headers ``` X-CanalMovil-Authentication: X-CanalMovil-deviceID: X-CanalMovil-pushID: ``` ### 5.2 GetKeysHelper class **File:** `AuthHeaderInterceptor.java:44` Provides keys for authentication: - `getKeysHelper.a()` - First key - `getKeysHelper.b()` - Second key These keys are used by the signing/authentication algorithm. ### 5.3 Certificate Pinning **File:** `di/NetworkModule.java:64-70` The app uses **Certificate Pinning** to prevent MITM attacks: - Expected SSL certificates are in `PinningRepository` - Loaded asynchronously at startup - All requests validate the server certificate --- ## 6. Code References ### 6.1 Key Files | File | Location | Description | |------|----------|-------------| | `ServicePaths.java` | `serviceNetworking/` | Paths and static headers | | `AuthHeaderInterceptor.java` | `serviceNetworking/interceptors/` | Auth header generation | | `NetworkModule.java` | `serviceNetworking/di/` | Retrofit/OkHttp configuration | | `CirculationService.java` | `serviceNetworking/circulations/` | Circulation API | | `StationsService.java` | `serviceNetworking/stations/` | Stations API | | `StationObservationsService.java` | `serviceNetworking/stationObservations/` | Observations API | | `CompositionsService.java` | `serviceNetworking/compositions/` | Compositions API | ### 6.2 Request Models | Model | File | Usage | |-------|------|-------| | `TrafficCirculationPathRequest` | `circulations/model/request/` | Departures, Arrivals, BetweenStations | | `OneOrSeveralPathsRequest` | `circulations/model/request/` | OnePaths, SeveralPaths, Compositions | | `OneStationRequest` | `stations/model/` | Station details | | `DetailedInfoDTO` | `stations/model/` | Detailed info configuration | | `StationObservationsRequest` | `stationObservations/model/` | Station observations | ### 6.3 Important Code Lines - Static headers: `ServicePaths.java:67-76` - Circulations user-key: `ServicePaths.java:67` - Stations user-key: `ServicePaths.java:68` - AVISA login token: `ServicePaths.java:70` - Auth interceptor: `AuthHeaderInterceptor.java:38-83` - Circulations base URL: `NetworkModule.java:76` - Stations base URL: `NetworkModule.java:145` - Enum TrafficType: `TrafficType.java:16-21` - Enum State: `CirculationPathRequest.java:65-67` --- ## 7. Additional Notes ### 7.1 JSON Serialization - **Library used:** Moshi (configured in `NetworkModule.java:87-96`) - **Format:** JSON field names match Java property names exactly - **Null handling:** Optional fields are **omitted** by the client; sending explicit `null` values leads to 401 responses - **Date format:** Millisecond timestamps (Long) ### 7.2 Security Considerations 1. **Hardcoded user-keys:** API keys are in the code (easy to extract) 2. **Certificate Pinning:** Makes proxy interception harder 3. **Dynamic authentication:** X-Elcano headers require the HMAC algorithm to be reproduced 4. **AVISA token:** Base64 credentials in code (decodable) ### 7.3 Testing Use the Python client (`adif_client.py`) or the signed test scripts in `/tests` to validate each endpoint. All endpoints are currently reproducible with the shipped keys and HMAC implementation. --- **Last update:** 2025-12-05 **Source:** Decompiled ADIF El Cano Movil APK **Tools:** JADX, Ghidra, manual Java code analysis