13 KiB
Request Body Analysis - ADIF API
Reverse engineering of package
com.adif.elcanomovil.serviceNetworking
Date: 2025-12-04
Table of Contents
- 1. Authentication Headers
- 2. Request Bodies
- 3. Endpoints and Base URLs
- 4. Network Configuration
- 5. Authentication System
- 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: <host>
X-Elcano-Client: AndroidElcanoApp
X-Elcano-Date: <timestamp>
X-Elcano-UserId: <uuid>
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<timestamp>\n<date>/<client>/<userId>/elcano_request\n<hash> - 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
{
"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
{
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"page": {
"pageNumber": 0
},
"stationCode": "60000",
"trafficType": "ALL"
}
Allowed Values
commercialService / commercialStopType (CirculationPathRequest.java:65-67):
YES- Only commercial services/stopsNOT- Without commercial services/stopsBOTH- All types
trafficType (TrafficType.java:16-21):
CERCANIAS- Commuter trainsAVLDMD- High speed long/medium distanceOTHERS- Other typesTRAVELERS- PassengersGOODS- FreightALL- 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
{
"allControlPoints": true/false/null,
"commercialNumber": "string or null",
"destinationStationCode": "string or null",
"launchingDate": 1733356800000,
"originStationCode": "string or null"
}
Real Example
{
"allControlPoints": true,
"commercialNumber": "04138",
"destinationStationCode": "60000",
"launchingDate": 1733356800000,
"originStationCode": "71801"
}
Important notes:
launchingDateis a millisecond timestamp (Long in Java)allControlPointsindicates 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
{
"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
{
"detailedInfo": {
"extendedStationInfo": true,
"stationActivities": true,
"stationBanner": true,
"stationCommercialServices": true,
"stationInfo": true,
"stationServices": true,
"stationTransportServices": true
},
"stationCode": "60000",
"token": "string"
}
Important notes:
- The
detailedInfoobject controls what is returned - All boolean fields default to
true(seeDetailedInfoDTO.java:149) tokenis 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
{
"stationCodes": ["60000", "71801"]
}
Real Example
{
"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
OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
Secured client (with authentication)
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
-
Generate persistent User ID
- Uses
GeneratePersistentUserIdUseCase - The ID is stored and reused between sessions
- Uses
-
Build the token
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() -
Generate headers
- The
ElcanoClientAuthobject generates authentication headers - They are added automatically to the request
- The
Generated headers
X-CanalMovil-Authentication: <calculated_token>
X-CanalMovil-deviceID: <device_id>
X-CanalMovil-pushID: <push_id>
5.2 GetKeysHelper class
File: AuthHeaderInterceptor.java:44
Provides keys for authentication:
getKeysHelper.a()- First keygetKeysHelper.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
nullvalues leads to 401 responses - Date format: Millisecond timestamps (Long)
7.2 Security Considerations
- Hardcoded user-keys: API keys are in the code (easy to extract)
- Certificate Pinning: Makes proxy interception harder
- Dynamic authentication: X-Elcano headers require the HMAC algorithm to be reproduced
- 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