Mon E.Leclerc is both Android and iOS app developed by Infomil, a company based in Toulouse that handles all of E.Leclerc’s IT needs (POS, connectivity, hosting, digital signage…).
The application is a Xamarin app.
https://hpcs.mservices.eu/Mobile/api
https://moneleclerc-mockv2.herokuapp.com/Mobile/api
Almost all requests use the POST
method, which isn’t very RESTful, but it’s partially justified by the fact that every request includes authentication data. Even the most basic requests require a minimum amount of parameters:
{
"CRC": 0,
"DATE_REQUETE": "2020-09-25T20:01:38.176Z",
"NO_VERIFICATION": "sHPMdfgY9tSfRgcGPWXulGNIA5teM4wFsZi8cF2nx2nBupITJqz1EtV1HP5uhH/q6/wjyTe/21XmmbBg9lFxky=="
}
CRC
: Integer - Last known CRC value for this resource (see CRCs)DATE_REQUETE
: String - Date of the request in ISO 8601 formatNO_VERIFICATION
: String - Base64 request “signature” (see Request signatures)For some reason (possibly for making debugging easier ?) the Android application adds two query parameters to each request:
MobileId
: String - UUIDv4 generated from the Android Device ID (using the java.util.UUID.nameUUIDFromBytes()
method)RequestId
: String - A random UUIDv4These two parameters are not (yet) required so can be dropped.
Most responses have the following structure:
{
"CR": 0,
"CRC": 868374834,
"DONNEES": {}
}
CR
: Integer - Server response code (not the HTTP status code):
0
: OK60
: Account desynchronized (whatever that means)80
: Invalid tokenCRC
: Integer - See CRCsDONNEES
: The dataIn both requests and responses you may come across a CRC
parameter, contrary to popular belief, this is not used for error detection (in this particular API), but for caching and saving bandwidth: standard HTTP caching doesn’t work when every request is a POST
request you see… It also appears that the client app doesn’t know how to calculate a CRC, it’s something only the server can do with some mathematical magic.
The CRC is sent in requests to prevent the server from sending back data that the client already has. Let’s say I have already requested a list of shops, and the CRC is 876543210
, the app saves the data and associated CRC in an internal cache system. Whenever the app wants to display the list of shops, it first fires off a request containing the known CRC, and there are two possibilities:
If no CRC is known (first request) or inappropriate, then you can just provide the value 0
.
Alongside the CRC
parameter, most requests require DATE_REQUETE
and NO_VERIFICATION
parameters: this appears to be an attempt at ensuring the request originates from a genuine client: their app.
If the user is unauthenticated, then it’s just a Base64 representation of an SHA512 hash of the current date and time in a YmdHisv
format, for example: TODO
If the user is authenticated, then it’s just a Base64 representation of an SHA512 hash of the user’s fidelity card number and the current date and time concatenated together !
Which gives us something like this in PHP:
function generateNoVerification(DateTime $dateTime, ?string $cardNumber = null) {
$string = $dateTime->format("YmdHisv");
if (!empty($cardNumber)) {
$string = "{$cardNumber}{$string}";
}
return base64_encode(hash("sha512", $string, true));
}
To authenticate your device, the app requires the user to first choose a shop. A list of shops is provided by a simple POST /Authentification/RecupererMagasinsPublic
, the response looks something like this:
{
"CR": 0,
"CRC": 868526804,
"DONNEES": [
{
"ADRESSE": "46 AV FRANKLIN ROOSEVELT",
"CODE_POSTAL": "06110",
"DEPARTEMENT": "06",
"LATITUDE": 43.57246,
"LONGITUDE": 7.000745,
"NOM_MAGASIN": "LE CANNET ROCHEVILLE",
"NO_MAGASIN": "0033",
"VILLE": "LE CANNET ROCHEVILLE"
},
{
...
},
{
"ADRESSE": "ZAC DES BATERSES",
"CODE_POSTAL": "01700",
"DEPARTEMENT": "01",
"LATITUDE": 45.82461,
"LONGITUDE": 4.997535,
"NOM_MAGASIN": "BEYNOST",
"NO_MAGASIN": "0101",
"VILLE": "BEYNOST"
}
]
}
All you need to keep handy is the NO_MAGASIN
of your chosen shop.
The device authentication endpoint (POST /Authentification/Authentifier
) uses encryption in an attempt to hide what’s really being sent to the server and also guarantee that the request is coming from their app, of course, there’s a way around that… The top-secret encryption key is stored in a Base64 encoded format in the app’s code: NEY0OUM4OEM2RkU4NDQx9zg3NjJFOEIx
, which gives us 4F49C88C6FE8441÷8762E8B1
once decoded.
Let’s take a look at what’s inside this encrypted blob:
{
"NO_MACHINE": "f0867762-b1a3-4515-9fd3-742ad8295751",
"CODE_SYSTEME": "A",
"CODE_MODELE": "JAT-L29",
"NO_VERSION_SYSTEME": "9",
"NO_VERSION_CLIENT": "5.0.1",
"NO_MAGASIN": "0033",
"NO_CARTE_FIDELITE": null,
"LISTE_REFERENTIEL": { ... },
"JETON": null,
"CRC": 0
}
NO_MACHINE
: String - UUIDv4 generated from the Android Device ID (using the java.util.UUID.nameUUIDFromBytes()
method)CODE_SYSTEME
: String - A
for Android, T
if the device is an MC18 Personal ShopperCODE_MODELE
: String - Model of phoneNO_VERSION_SYSTEME
: String - Android version numberNO_VERSION_CLIENT
: String - Application version numberNO_MAGASIN
: String - The NO_MAGASIN
of the chosen shopNO_CARTE_FIDELITE
: String - Fidelity card number, can be nullJETON
: String - Authentication token, can be nullA random 16 byte IV is then generated, and the string representation is then encrypted with AES256 according to the decompiled application code, but it’s actually using AES192 due to the length of the key: 24 bytes. AES256 would require a 32 byte encryption key.
Pretty simple to reproduce with a bit of PHP:
$iv = random_bytes(16);
$encryptedData = openssl_encrypt(json_encode($data), "aes-192-cbc", '4F49C88C6FE8441÷8762E8B1', 0, $iv);
In the response, if successful, you will find a JETON
parameter, this is your authentication token, so keep it safe and handy for future requests !
Once your device is authenticated the next step is to let the API know who you are, get your bright orange fidelity card ready !
Request: POST /MonCompte/EnregistrerCarteFidelitev2
:
{
"CRC": 0,
"DATE": "2020-11-17T21:48:12.500Z",
"DATE_REQUETE": "2020-11-17T21:48:12.500Z",
"DateGenerated": "2020-11-17T21:48:12.500Z",
"HASH": "lk2Hi[...]P1MyaPg==",
"JETON": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"LISTE_REFERENTIEL": { ... },
"NO_VERIFICATION": "Gh2h9[...]ujEwbg==",
"NUMERO_CARTE_FIDELITE": "2950972719579"
}
The important parameters here are:
NUMERO_CARTE_FIDELITE
: String - Fidelity card number (appears to always start with 295
)HASH
: String - Base64 encoded representation of an SHA512 hash of the user’s fidelity card number, card pin (4 digits) and the current date and time in a YmdHisv
format.Calculating the HASH
parameter can be performed with the following PHP function:
function generateCardHash($cardNumber, $cardPassword, $dateTime) {
return base64_encode(hash("sha512", "{$cardNumber}{$cardPassword}" . $dateTime->format("YmdHisv"), true));
}
Request: GET /CodeBarre/Image?codeBarre=1234567890
(unauthenticated)
Response:
You can also find out some basic information about a product from it’s barcode with the following request: POST /Scan/Scanner
{
"CODE": "3564700339398",
"CRC": 0,
"DATE_REQUETE": "2020-09-28T00:55:13.053Z",
"JETON": "string",
"NO_VERIFICATION": "string",
"ORIGINE_CONSULTATION": 4,
"TYPE": "EAN_13"
}
With the following parameters:
CODE
: String - The barcode contentsORIGINE_CONSULTATION
: Integer: Always 4
?TYPE
: String: Always EAN_13
?Partial response:
{
"OBJET": {
"DESIGNATION": "MOUCHOIRS 2 PLIS X110 CARESSE",
"EAN": "3564700339398",
"EST_EN_PROMO": true,
"EST_HEYO_GRATTAGE": true,
"IMAGE_PRODUITS": [
{
"URL_PRODUIT": "https://hpcs.mservices.eu/mobile/api/Photo/Obtenir?idPhoto=4118704"
}
]
}
}
Request: https://hpcs.mservices.eu/StickersPromo/StickerPromo.ashx?type=208&qte=1&val=30&prix=1.25&prixhp=9999.99&unit=0&cli=100&mindim=128&module=maSel
Response:
Most products have photos, and these can be downloaded (unauthenticated) via two URLs:
GET /Photo/ObtenirPhotoGalec?id=13534
or GET /Photo/Obtenir?idPhoto=4118700
For example, here’s a special family-pack of Tropicana (GET /Photo/ObtenirPhotoGalec?id=13530
):
Request: GET /Document/ObtenirDocument?id=1165
(no authentication required)
base.apk
split_config.armeabi_v7a.apk
split_config.en.apk
split_config.fr.apk
split_config.xhdpi.apk
base.apk
split_config.armeabi_v7a.apk
split_config.en.apk
split_config.fr.apk
split_config.xhdpi.apk
base.apk
split_config.armeabi_v7a.apk
split_config.en.apk
split_config.fr.apk
split_config.xhdpi.apk