Hacking Mobile App APIs for Automation

Mobile Apps / games are nice, but I often wish the experience would be better, more advanced. They are a one size fit all that could be improved. Time to hack a better way...

Hacking Mobile App APIs for Automation

As mentioned in another article, Website As List, I like to improve some of my activities building automations.

Recently, I have looked into hacking mobile apps and APIs for fun, let's dig deeper.

Disclaimer: As with everything I do, I do it, the 'nice' way. I am not trying to bring anything down or cheat the games or ... What I want to do is reuse some of existing things to build tools to enhance my personal experience. All that in an ethical way.

Context

While the context is specific to what I wanna do, the techniques used can be applied to other ones too.

During my spare time, for fun, I like to use the Panini Dunk mobile app. It is basically collecting basketball cards in an app. It is not really useful in any way, but it helps spend time on thing I enjoy.

However, there are some problems with it... Or things I don't necessarily like.

The main one is the UX. It is too slow, necessitate to many taps, isn't flexible to make it nice and there is no web app to make the experience better out of the phone.

Also, I am collecting / trying to gather Jason Kidd cards there, as my physical cards is as well, and ... I would rather not loose it if the app disappear at some point... (https://saasify.work/platform-risk/).

So, I started looking into it, could I access my account data and build some personal page and tools from it. Turns out, it is possible.

Tools needed

I strongly recommend using Postman (https://www.postman.com/) an API client to help with most of the work.

Execution

What do I want accomplished

The most basic thing I want is two things:

  • A page that can show my collection
  • An script that can alert me when a new Jason Kidd card is put on for auction

Understanding how it works

The very first step in order to do any hacking work, is to understand what are the various components and how they work together.

In the case of the Panini app, it is rather simple. There is the mobile app on one side, which is what the user is interacting with and seems to be built in Unity, and then there is a server side component that host the data. The mobile app <> server side exchanges are done through a rest API using json.

Different calls going to different host. This understanding comes from the following section.

Capturing the calls

First, given the context and the mechanics of the game, it was safe to assume that there were calls made between the app and servers somewhere. And given the tech context those days, it was also safe to assume that it was likely done through standard http calls.

The first step then to understand how it works was to see what get exchanged between the mobile app and the server. The easiest way to do that is to use Postman's proxy and capture the request.

This involves the following steps:

  • Setting up and enabling the proxy in Postman
  • Configuring my phone's wifi connection to use Postman's proxy (and accepting the SSL certificates)
  • Starting the capture in Postman
  • Use the app to get to the content I want to see/access

When you do that, if everything is setup as expected, and the app indeed uses http calls, you should be able to use the app as if there was no proxy, and all the request should start to appear in Postman.

Thankfully, my assumptions were correct and I can see all the calls I need

Understanding calls and what you can do

The next step, then is to understand what happens, when and how.

One aspect is to go step by step in the mobile app and see what calls get made. It becomes easy as it is as if you were using any other rest API. It gives you the context in which things happen. With that and the API request in Postman (URL, body, ...) you should start having a good idea of how it works.

Another aspect is to start analyzing and tweaking the calls you are interested in in Postman and see how the server responds. Few things of interests are, the URLs and request used (GET, POST, ...) , how (if any) is authentication done, is there any request signature involved, any pagination ? ,  ... Anything that can get you from the request the app did to what you wish the api can give you.

When you understand the API, you will have an idea of what's possible, can you achieve what you want and how, or possibly other ideas.

Building pages and tools

Unfortunately, the Panini API used is quite secure, it uses jwt tokens with encryption, but also requires a valid request signature to return data. That's quite annoying as it is hard to guess the key used.

For now, that means it reduces my ambitions and it means that my tools can't be too generic and I can't share or make them available to other if I want to. It's fine, I will stick with having those for me for now.

To build the tools, you can take advantage of the API call to source code feature in Postman. It can give a starting point. And then, it becomes like any other programming script. Get the data, and do something with it.

Here is my auction alert script, using pushover for notifications.

<?php

// Replace XXXXX with relevant values
$pushoverAppToken = 'XXXXX';
$pushoverKey = 'XXXXX';

$paniniAppId = 'XXXXX';
$paniniBearerToken = "XXXXX";
$paniniNonce = "XXXXX";
$paniniSignature = "XXXXX";

$cacheKnownAuctionsFile = 'auctions-jason-kidd.txt';

//region Helpers and Grouping
function pushoverNotify(array $notificationData = []) {
    global $pushoverAppToken, $pushoverKey;

    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_URL => "https://api.pushover.net/1/messages.json",
        CURLOPT_POSTFIELDS => [
            "token" => $pushoverAppToken,
            "user" => $pushoverKey,
            ...$notificationData
        ],
        CURLOPT_RETURNTRANSFER => true
    ]);
    curl_exec($curl);
    curl_close($curl);
}

function auctionDescription($anAuction)
{
    return join(" ", [
        $anAuction->card->collection_name,
        $anAuction->card->group_name,
        empty($anAuction->card->card_limit) ? "" : "/". $anAuction->card->card_limit
    ]);
}

function callPaniniAPI() {
    global $paniniAppId, $paniniBearerToken, $paniniNonce, $paniniSignature;

    $body = (object) [
        "attributes" => (object) [
            "level" => 0,
            "keyword" => "Jason Kidd",
            "albumNames" => [],
            "teams" => [],
            "collections" => "",
            "collectionIds" => [],
            "groups" => [],
            "positions" => [],
            "sortBy" => 1,
            "sortOrder" => 1,
            "is_featured" => false,
            "is_L_N" => false,
            "filter_card_type" => 0,
            "usr_crds_cnt" => 0,
            "lock_type" => [],
            "exclude_card_ids" => [],
            "src_typ" => "",
            "filterBy" => 0,
            "safe_cards" => true,
            "from_trade" => false
        ]
    ];

    $bodyStr = json_encode($body);

    $headers = [
        'host' => 'userinventory.panininba.com',
        'content-type' => 'application/json',
        'x-unity-version' => '2020.3.12f1',
        'accept' => '*/*',
        'authorization' => "Bearer $paniniBearerToken",
        'nonce' => $paniniNonce,
        'app_version' => '2.3.0',
        'accept-language' => 'en-US,en;q=0.9',
        'accept-encoding' => 'gzip, deflate, br',
        'signature' => $paniniSignature,
        'appid' => $paniniAppId,
        'content-length' => strlen($bodyStr),
        'user-agent' => 'NBADunk/262 CFNetwork/1402.0.8 Darwin/22.2.0',
        'connection' => 'keep-alive',
        'os_version' => 'iOS 16.2',
        'os_type' => 'iOS'
    ];

    $curl = curl_init();

    curl_setopt_array($curl, array(
        CURLOPT_URL => 'https://userinventory.panininba.com/v5/auction?featured=false&l=30&t=0',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => 'PUT',
        CURLOPT_POSTFIELDS => json_encode($body),
        CURLOPT_HTTPHEADER => array_map(fn($key, $value) => $key . ':' . $value, array_keys($headers), $headers)
    ));

    $response = curl_exec($curl);

    curl_close($curl);

    return $response;
}

//endregion

$response = callPaniniAPI();

// === Process the response
if ($response === false) {
    pushoverNotify(["message" => "DUNK - Failed to retrieve auction data", "priority" => 1]);
    die();
}

$responseObj = json_decode($response);
if ($responseObj->status !== 200) {
    pushoverNotify(["message" => "DUNK - Failed to retrieve auction data : {$responseObj->message}", "priority" => 1]);
    die();
}

$priorAuctions = [];
if (file_exists($cacheKnownAuctionsFile)) {
    $priorAuctions = json_decode(file_get_contents($cacheKnownAuctionsFile));
    unlink($cacheKnownAuctionsFile);
}

$allAuctions = $responseObj->data;
$currentAuctions = array_map(fn($anAuction) => $anAuction->_id, $allAuctions ?? []);
file_put_contents($cacheKnownAuctionsFile, json_encode($currentAuctions));


$newAuctions = array_diff($currentAuctions, $priorAuctions);
if (count($newAuctions) > 0) {
    printf("Notifying of %d new auctions\n", count($newAuctions));
    pushoverNotify([
        "title" => "DUNK - New auctions for Jason Kidd",
        "html" => 1,
        "message" => implode("\n", [
            count($newAuctions) . " new auctions",
            ... array_map(fn($anAuction) => "<a href='{$anAuction->card->image_url}'>".auctionDescription($anAuction)."</a>",
                array_filter($allAuctions, fn($anAuction) => array_search($anAuction->_id, $newAuctions) !== false)
            )
        ])
    ]);
} else {
    printf("No new auction\n");
}
Send pushover notification on new panini dunk auction

Going further

One step that I am still struggling on, and I will make an update when I solve the issue, is to generate the required signature for the request. I think I have identified how it is calculated but there may be an encryption key that I am still missing and would have to crack...

In order to do so, my first step what to get the android app .apk file and see whether I could reverse engineer it back to as close as the source code as possible, hoping to find something that looked like and encryption key, algorithm, ... anything to help. No success so far, but I am also in unknown territory and I need to learn how everything work there.

Hopefully I can find a way soon...

That would allow more advanced tools and automations to be built, especially to parameterize the API calls. I wouldn't have to rely on the proxy and making manual calls thourgh the app anymore.

Ideally though, Panini could either build a web app for it or give access to some API to make it easier. Unfortunately, with Fanatics getting the exclusive license for NBA cards (read more), I don't see it happening anytime.