Reverse Engineering Private API – OAuth Code Flow with PKCE
This post continues from my first post on reverse engineering a private API. In the previous post I explained how I had to jailbreak my iPhone to bypass SSL pinning used by the mobile app for secure communication with its server. I was so excited to finally get everything working and see the HTTPS network requests and their responses in plain text in Proxyman. Alas, my excitement was short-lived as what I found about how the mobile app handles authentication wasn’t what I was expecting. This app was using Auth Code Flow with PKCE, which makes it hard to get an access token to make authenticated API requests.
Authorization Code Flow
Check out this article on auth0.com for more details on how this works for server-side web apps.
When public clients (e.g., native and single-page applications) request Access Tokens, this simple auth flow has some security flows:
- Native apps cannot securely store a Client Secret. Decompiling the app will reveal the Client Secret, which is bound to the app and is the same for all users and devices.
- Native apps may make use of a custom URL scheme to capture redirects (e.g., MyApp://) potentially allowing malicious applications to receive an Authorization Code from your Authorization Server.
- SPA (Single Page Applications) Cannot securely store a Client Secret because their entire source is available to the browser.
Authorization Code Flow with PKCE
PKCE, pronounced “pixy” is an acronym for Proof Key for Code Exchange. The key difference between the PKCE flow and the standard Authorization Code flow is users aren’t required to provide a client_secret. PKCE reduces security risks for native apps, as embedded secrets aren’t required in source code, which limits exposure to reverse engineering.
Check out this article on auth0.com for more details on how this works for public clients (native apps and SPAs).
The PKCE-enhanced Authorization Code Flow introduces a secret created by the calling application that can be verified by the authorization server; this secret is called the Code Verifier. Additionally, the calling app creates a transform value of the Code Verifier called the Code Challenge and sends this value over HTTPS to retrieve an Authorization Code. This way, a malicious attacker can only intercept the Authorization Code, and they cannot exchange it for a token without the Code Verifier.
Based on the intercepted network requests from Mila app through Proxyman, I was able to capture the various servers used for the auth flow and the API base URL for making subsequent requests using the access token received from the login flow. So I started writing rough code to mimic this whole flow in a python script.
I wrote the following script as a scratchpad for interacting with Mila API – Github Gist.
The login method was the most interesting because I had to fetch the login html page with specific client id, redirect uri, random nonce and state along with a code challenge. From the HTML page response, I had to scrape for the form action url and submit the login request to the form’s action url with the cookies grabbed from the HTML page response. Also, the redirect uri is for in app redirect (“milacares://anyurl.com/”), so I had to make sure the request is made with “allow_redirects=False”, so python requests method doesn’t automatically redirect to in app redirect url, which would have returned a failure since we are making the request through the script. The authorization code from the login server is returned as a query parameter in the login redirect URL on successful login. I can grab the redirect url and extract the auth code and make another request to exchange this auth code for a user access token and refresh token.
Profile and Device Info
Once I have the access token, I can make various GET and POST requests to interact with the API. I can get profile information with a GET request to /profile. In order to get information about device id and appliance code for each devices, I have to make a GET request to /appliances/meta. Sometimes the API endpoints use device id, while some use appliance code, which was very confusing.
GET requests to /appliance/:deviceId/config returns the current settings including the smart modes (e.g. quiet enabled, quarantine enabled, night time start, end, etc.). I can make PATCH request to /smart-modes/:smartModeName to enable/disable the smart mode.
There are 8 types of sensors (PM 1.0, PM 2.5, PM 10, VOC, Carbon Monoxide, Carbon Dioxide, Temperature and Humidity) on the Air purifier device and API gives me access to their latest values and I can also query based on date and time ranges. I was only interested in their latest values since I can use Home Assistant for polling and recording the historical values over time. The respective endpoint uses appliance code for fetching the latest sensor value at GET /sensor/appliance?deviceId=:applianceCode&metric=:sensorName
Fan Mode Control
The last thing I was interested in was controlling the fan mode and its speed based on automations through Home Assistant. e.g. I can turn the speed to 100% when noone is home, but make it run quiet when we are home. Changing the fan mode to auto was easier than getting the manual control work as it has specific values for fan speed RPM values that it expects for manual control request and they are not linearly spread out. e.g. 10% fan speed maps to 600 RPM, but 20% is 740 RPM, and so on. So I used a helper map to convert the percentage values to hard coded RPM values when making the POST request to /appliance/:applianceCode/command/control-mode/manual. It also expects “target_aqi_float” in the payload, but it accepts any dummy value for this field.
Now that I have all helper methods to interact with the Mila API (Github Gist), next steps are to convert this into a python wrapper as an API client and create a python module, which I can then use when creating a Home Assistant component for Mila Air Purifiers. I will add a link to the post once I make more progress towards this goal. Until then you can refer to my post on creating a Home Assistant component for Snoo smart sleeper if you are interested in learning more about Home Assistant component development process I followed.