docs: RE report + correct the privacy writeup from live analysis
Adds OFFICIAL_APP_REVERSE_ENGINEERING.md — a technical teardown (methodology, auth model, device-ID, GPS, Instabug) with mermaid diagrams and the mitmproxy capture timeline. Corrects OFFICIAL_APP_PRIVACY.md, which was wrong on two counts: - IMEI: the app reads no IMEI; it sends the Android SSAID to /api/Device in a field misleadingly named IMEINumber. - Trackers: Segment/Amplitude/Firebase-Analytics/Sentry/ad-ID are all ABSENT; the real telemetry SDK is Instabug (session replay, screenshots, network logs). Also folds in the confirmed GPS auto-send (with the GrapheneOS caveat). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2bdb59b49a
commit
2434879804
2 changed files with 371 additions and 84 deletions
|
|
@ -4,111 +4,119 @@
|
||||||
(`com.ipsgroupinc.parksmarter`, version 4.4.0), and how **BigBrainParking** — this
|
(`com.ipsgroupinc.parksmarter`, version 4.4.0), and how **BigBrainParking** — this
|
||||||
open-source alternative — is different.*
|
open-source alternative — is different.*
|
||||||
|
|
||||||
This was written by inspecting the official app's own files (its permission list and
|
This was written by taking the official app apart: reading its program code, and
|
||||||
its program code). It's meant to be readable by anyone, not just programmers. Where we're
|
watching its real network traffic through a proxy. Where we're **sure**, we say
|
||||||
**sure**, we say so; where we're making an educated guess, we say that too.
|
so; where something is situational, we say that too. (For the technical teardown
|
||||||
|
with diagrams and evidence, see
|
||||||
|
[OFFICIAL_APP_REVERSE_ENGINEERING.md](OFFICIAL_APP_REVERSE_ENGINEERING.md).)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The short version
|
## The short version
|
||||||
|
|
||||||
- **Your exact location is sent to ParkSmarter's servers whenever you look at the map**
|
- **Your exact location is sent to ParkSmarter's servers when you use the map**
|
||||||
in the official app — automatically, just by opening it, not only when you ask it to
|
to find meters — and the app is built to do this **automatically** when the map
|
||||||
"find parking near me."
|
opens, not only when you ask it to "find parking near me."
|
||||||
- **The app also carries a stack of tracking tools** (Segment, Amplitude, Firebase,
|
- **The app carries one heavy tracking tool — Instabug** — that can record your
|
||||||
Sentry) that quietly report what you do and your device's identity to outside companies.
|
screen, take screenshots, and log the app's network activity, reporting to an
|
||||||
- **It can read a permanent hardware ID from your phone and send it to ParkSmarter.**
|
outside company.
|
||||||
|
- **It sends a permanent per-device ID to ParkSmarter.** It is *not* your phone's
|
||||||
|
IMEI (despite the app calling the field `IMEINumber`) — it's Android's device
|
||||||
|
ID. Still a fixed code that identifies your specific phone to ParkSmarter.
|
||||||
- **Good news:** it **cannot** track your location while the app is closed.
|
- **Good news:** it **cannot** track your location while the app is closed.
|
||||||
- **BigBrainParking removes the tracking entirely** and only sends your location when
|
- **BigBrainParking removes the tracking entirely** and only sends your location
|
||||||
*you* deliberately tap "My location" and search.
|
when *you* deliberately tap "My location" and search.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## First, why does this matter?
|
## First, why does this matter?
|
||||||
|
|
||||||
"Location data" is a record of where you physically are. On its own, one location isn't
|
"Location data" is a record of where you physically are. On its own, one location
|
||||||
a big deal. But a *stream* of them — collected quietly and often — reveals where you live,
|
isn't a big deal. But a *stream* of them — collected quietly and often — reveals
|
||||||
work, worship, and who you visit. When that's combined with a permanent ID for your phone
|
where you live, work, worship, and who you visit. Tie that to a permanent ID for
|
||||||
and shared with advertising/analytics companies, it becomes a profile of you that can be
|
your phone and it becomes a profile of you that can be sold, leaked, or
|
||||||
sold, leaked, or subpoenaed. So the questions that matter are: **who collects it, how
|
subpoenaed. So the questions that matter are: **who collects it, how often, is it
|
||||||
often, is it tied to your identity, and does it leave in ways you didn't ask for?**
|
tied to your identity, and does it leave in ways you didn't ask for?**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## When does the official app send your location to ParkSmarter?
|
## When does the official app send your location to ParkSmarter?
|
||||||
|
|
||||||
**To ParkSmarter's own servers, your GPS is used for one thing: finding nearby meters.**
|
**To ParkSmarter's own servers, your GPS is used for one thing: finding nearby
|
||||||
There are exactly two requests that carry your coordinates (finding meters near a point);
|
meters.** We confirmed this by watching the traffic: the coordinates go out as a
|
||||||
your location is **not** attached to logging in, starting a parking session, or paying.
|
`GET …/api/Meter?Lat=…&Long=…` request, and your location is **not** attached to
|
||||||
That part is reasonable.
|
logging in, starting a session, or paying. That part is reasonable.
|
||||||
|
|
||||||
**The catch is *when* it happens.** The official app is built to grab your live GPS and
|
**The catch is *when*.** The app is built to grab your live GPS and ask "what
|
||||||
ask ParkSmarter "what meters are near me?" **automatically when you open the map** — you
|
meters are near me?" **automatically when the map opens** — no tap required — as
|
||||||
don't have to tap anything. So simply browsing the app quietly sends your precise location
|
long as you're logged in and have granted location permission. So simply browsing
|
||||||
to ParkSmarter's servers.
|
the map quietly sends your precise location to ParkSmarter.
|
||||||
|
|
||||||
> **How sure are we?** *Fairly sure.* The code clearly reads your current GPS and has a
|
> **One nuance we saw firsthand:** on a de-Googled phone (GrapheneOS), the map
|
||||||
> "nearby meters" feature wired to the map. Confirming the exact moment it fires would take
|
> itself doesn't draw (it needs Google services), which can suppress that
|
||||||
> watching the app's live network traffic, which we haven't done yet.
|
> automatic send. On a normal Google phone the auto-send happens as designed.
|
||||||
|
|
||||||
**BigBrainParking does the opposite on purpose:** it opens on your **last parking lot**
|
**BigBrainParking does the opposite on purpose:** it opens on your **last parking
|
||||||
(from your own history, not your GPS), and it only sends your location when you *explicitly*
|
lot** (from your own history, not your GPS), and it only sends your location when
|
||||||
tap **"My location"** and then search.
|
you *explicitly* tap **"My location"** and then search.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The bigger privacy concerns
|
## The bigger privacy concerns
|
||||||
|
|
||||||
These are things the official app *can* do beyond the parking API itself. They're taken
|
### 1. A heavy tracking tool: Instabug
|
||||||
from the app's official permission list and the tracking libraries built into it.
|
|
||||||
|
|
||||||
### 1. Hidden tracking/analytics companies (the biggest one)
|
The app bundles **Instabug**, a monitoring/bug-reporting toolkit — and it's the
|
||||||
|
serious one. It's capable of:
|
||||||
|
|
||||||
The app bundles four separate tracking/telemetry toolkits:
|
- **recording your session / replaying what you did**,
|
||||||
|
- **taking screenshots** of the app,
|
||||||
|
- **logging the app's network requests**,
|
||||||
|
- **tracking your taps and the steps you take** through the app.
|
||||||
|
|
||||||
- **Segment** — a "customer data platform." Its whole job is to collect what you do in the
|
All of this reports to an outside company's servers (`api.instabug.com`). We saw
|
||||||
app and fan it out to many other companies.
|
it phone home the moment the app started.
|
||||||
- **Amplitude** — product-analytics; records your actions to study behavior.
|
|
||||||
- **Firebase** (Google) and **Sentry** — Google's analytics/crash tools and an error tracker.
|
|
||||||
|
|
||||||
These typically collect: **which screens you visit and when, your device model, and your
|
> **What it is *not*:** we specifically checked and the app does **not** contain
|
||||||
IP address** (which reveals your rough city/neighborhood), and often an **advertising ID**
|
> Segment, Amplitude, Sentry, Google Analytics/Firebase-Analytics, an advertising
|
||||||
(a code used to follow you across different apps). Even without your exact GPS, this is a
|
> ID, or Play Store install-tracking. (An earlier version of this note listed
|
||||||
detailed behavioral profile shared with outside companies.
|
> those — that was wrong; Instabug is the actual story.) Google is present only
|
||||||
|
> for **push notifications**.
|
||||||
|
|
||||||
> **BigBrainParking has none of these.** No Segment, no Amplitude, no Firebase, no Sentry,
|
> **BigBrainParking has none of this.** No Instabug, no analytics, no ad-ID, no
|
||||||
> no ad-ID, no Google services at all.
|
> Google services at all.
|
||||||
|
|
||||||
### 2. A permanent hardware ID sent to ParkSmarter
|
### 2. A permanent device ID sent to ParkSmarter
|
||||||
|
|
||||||
The app is allowed to **read phone/device identifiers** (the "read phone state"
|
When you log in, the app sends ParkSmarter a fixed identifier for your phone,
|
||||||
permission), and it has a feature that sends your device's **IMEI number** — a permanent
|
inside a request to `…/api/Device`. The field is **named `IMEINumber`**, which is
|
||||||
serial number for your phone — to ParkSmarter. Unlike a password you can change, this ID
|
misleading — **it is not your IMEI**. Modern Android forbids apps from reading the
|
||||||
is fixed, so it can be used to recognize your specific phone forever.
|
IMEI at all, and this app never tries. The value it actually sends is your
|
||||||
|
phone's **Android device ID** (a code assigned to your device). Unlike a password,
|
||||||
|
it doesn't change, so it can be used to recognize your specific phone over time.
|
||||||
|
It goes only to ParkSmarter, not to a third party.
|
||||||
|
|
||||||
> **BigBrainParking never reads or sends this.**
|
> **BigBrainParking never reads or sends any device ID.**
|
||||||
|
|
||||||
### 3. Other capabilities worth knowing about
|
### 3. Other capabilities worth knowing about
|
||||||
|
|
||||||
- **Install tracking** — records how you found/installed the app (via the Google Play
|
- **Microphone & camera permissions** — camera is for scanning kiosk QR codes;
|
||||||
"install referrer"), tied to your Play account.
|
microphone is unusual for a parking app (may be an unused library leftover).
|
||||||
- **Wi-Fi scanning permission** — can see the Wi-Fi networks around you, which can be used
|
- **Wi-Fi & phone-state access** — used by a networking library to report your
|
||||||
to *estimate your location even without GPS*.
|
connection type and carrier name; it does **not** read your IMEI.
|
||||||
- **Microphone permission** — unusual for a parking app. It may be an unused leftover from
|
- **Bluetooth** — for talking to parking meters directly.
|
||||||
a library, but the app is allowed to access your microphone.
|
- **Google push messaging (FCM)** — notifications route through Google.
|
||||||
- **Google push messaging (FCM)** — notifications are routed through Google, which links
|
|
||||||
your device to Google and to ParkSmarter's messaging system.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## To be fair — what it does *not* do
|
## To be fair — what it does *not* do
|
||||||
|
|
||||||
- **No background location tracking.** This is important: the app can only see your
|
- **No background location tracking.** The app has the everyday location
|
||||||
location **while you have it open and are using it**. It has the everyday location
|
permission but **not** the "all the time / background" one, so it can't follow
|
||||||
permission but **not** the special "all the time / in the background" one, so it can't
|
you when it's closed.
|
||||||
follow you around when the app is closed.
|
- **No IMEI, no advertising ID, no Segment/Amplitude/Sentry.**
|
||||||
- Your location going to ParkSmarter is genuinely limited to the "find nearby meters"
|
- Your location going to ParkSmarter is genuinely limited to the "find nearby
|
||||||
feature — it isn't stapled onto payments or your account details.
|
meters" feature — it isn't stapled onto payments or your account details.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -116,12 +124,13 @@ is fixed, so it can be used to recognize your specific phone forever.
|
||||||
|
|
||||||
| | Official ParkSmarter app | BigBrainParking |
|
| | Official ParkSmarter app | BigBrainParking |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Sends GPS to ParkSmarter | **Automatically** when you open the map | **Only** when you tap "My location" and search |
|
| Sends GPS to ParkSmarter | **Automatically** when the map opens | **Only** when you tap "My location" and search |
|
||||||
| Opens the map on… | Your current GPS location | Your **last parking lot** (not your GPS) |
|
| Opens the map on… | Your current GPS location | Your **last parking lot** (not your GPS) |
|
||||||
| Hidden analytics/tracking SDKs | Segment, Amplitude, Firebase, Sentry | **None** |
|
| Third-party tracking SDKs | **Instabug** (session replay, screenshots, network logs) | **None** |
|
||||||
| Advertising ID / cross-app tracking | Likely collected | **Never** |
|
| Advertising ID / cross-app tracking | **No** (none present) | **Never** |
|
||||||
| Sends a permanent device ID (IMEI) | Yes (feature exists) | **Never** |
|
| Sends a permanent device ID | **Yes** — Android device ID, sent as `IMEINumber` | **Never** |
|
||||||
| Google services required | Yes (Firebase, FCM) | **None** (runs on GrapheneOS) |
|
| Reads your hardware IMEI | **No** (not possible on modern Android) | **Never** |
|
||||||
|
| Google services required | Yes (push notifications) | **None** (runs on GrapheneOS) |
|
||||||
| Background location | No | No |
|
| Background location | No | No |
|
||||||
| Source code you can inspect | No (closed) | **Yes** (this repo) |
|
| Source code you can inspect | No (closed) | **Yes** (this repo) |
|
||||||
|
|
||||||
|
|
@ -129,18 +138,19 @@ is fixed, so it can be used to recognize your specific phone forever.
|
||||||
|
|
||||||
## How we checked (for the curious)
|
## How we checked (for the curious)
|
||||||
|
|
||||||
- **Permissions** come from the app's own `AndroidManifest` (the official list Android
|
- **Permissions** come from the app's own `AndroidManifest`. Confirmed present:
|
||||||
shows). Confirmed present: precise + approximate location, read-phone-state, Wi-Fi
|
precise + approximate location, read-phone-state, Wi-Fi, microphone, camera,
|
||||||
state, microphone, camera, Google push, install-referrer. Confirmed **absent**:
|
Bluetooth, Google push. Confirmed **absent**: background location.
|
||||||
background location.
|
- **The device-ID and location behavior** were read from the app's decompiled
|
||||||
- **Tracking libraries** and the **location-bearing requests** (`getMetersByLocation` /
|
code (it's a React Native / Expo app, so the logic is in a JavaScript bundle)
|
||||||
`getLimitedMetersByLocation`) and the **IMEI-sending** device-registration request come
|
**and confirmed on the wire** with a live proxy capture: the `…/api/Device`
|
||||||
from reading the app's decompiled program code (version 4.4.0).
|
request carrying the Android ID as `IMEINumber`, and the `…/api/Meter?Lat=…`
|
||||||
- **Educated guesses** (clearly labeled above) are the auto-send-on-open behavior and the
|
request carrying GPS.
|
||||||
advertising-ID collection — code strongly suggests them, but a live network capture would
|
- **The SDK list** (Instabug present; Segment/Amplitude/Sentry/Firebase-Analytics/
|
||||||
make them certain. We can do that capture if you want proof.
|
ad-ID absent) comes from searching the app's bundled code and its network
|
||||||
|
endpoints.
|
||||||
|
|
||||||
*Not affiliated with or endorsed by IPS Group / ParkSmarter. This is an independent,
|
*Not affiliated with or endorsed by IPS Group / ParkSmarter. This is an
|
||||||
good-faith analysis of a publicly distributed app for the purpose of building a
|
independent, good-faith analysis of a publicly distributed app for the purpose of
|
||||||
privacy-respecting alternative. Findings reflect version 4.4.0 and could change in later
|
building a privacy-respecting alternative. Findings reflect version 4.4.0 and
|
||||||
versions.*
|
could change in later versions.*
|
||||||
|
|
|
||||||
277
docs/OFFICIAL_APP_REVERSE_ENGINEERING.md
Normal file
277
docs/OFFICIAL_APP_REVERSE_ENGINEERING.md
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
# Reverse-engineering the official ParkSmarter app
|
||||||
|
|
||||||
|
A technical teardown of the official **Park Smarter** Android app
|
||||||
|
(`com.ipsgroupinc.parksmarter`, **v4.4.0**, versionCode 170), focused on **what
|
||||||
|
data leaves the device, when, and to whom** — with special attention to the
|
||||||
|
**device identifier** and **GPS location**. Combines static decompilation with a
|
||||||
|
live man-in-the-middle capture.
|
||||||
|
|
||||||
|
> Independent, good-faith security/privacy research on a publicly distributed
|
||||||
|
> app, for the purpose of building a privacy-respecting alternative
|
||||||
|
> (BigBrainParking). Not affiliated with or endorsed by IPS Group. Findings
|
||||||
|
> reflect v4.4.0. The full decompiled source is kept in a **private** mirror; this
|
||||||
|
> report quotes only the excerpts needed as evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
| Claim | Verdict | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| The app reads your hardware **IMEI** | **False** | No `TelephonyManager.getImei/getDeviceId` anywhere; `react-native-device-info` isn't even bundled; and Android 10+ blocks IMEI for normal apps regardless. |
|
||||||
|
| The app sends a **persistent device ID** to its backend | **True** | `PUT /api/Device` carries the **Android SSAID** in a field misleadingly named `IMEINumber`, plus the push token as `DeviceID`. |
|
||||||
|
| The app sends your **GPS** to its servers | **True**, for meter search only | `GET /api/Meter?Lat=…&Long=…`; captured live. Not attached to login/session/payment. |
|
||||||
|
| GPS is sent **automatically** on map open | **True in code**, situational at runtime | Wired to map-ready when authenticated + permission granted; on GrapheneOS the map fails to render (needs Play Services), which suppresses the auto-fetch. |
|
||||||
|
| Bundles Segment / Amplitude / Firebase-Analytics / Sentry | **False** | None present. |
|
||||||
|
| Bundles heavy telemetry | **True** | **Instabug** — session replay, screenshot capture, network-request logging, visual user-steps → `api.instabug.com`. |
|
||||||
|
| Advertising ID / install-referrer | **False** | Neither is present. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Methodology
|
||||||
|
|
||||||
|
Both static and dynamic analysis were used.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["Park+Smarter_4.4.0.xapk<br/>(base + 3 config splits)"] --> B[jadx 1.5.1<br/>dex → Java]
|
||||||
|
A --> C["hermes-dec<br/>index.android.bundle → JS"]
|
||||||
|
A --> D[apktool 2.10.0<br/>manifest + resources]
|
||||||
|
A --> E["APKEditor merge → 1 universal APK<br/>+ apk-mitm (trust user CA)"]
|
||||||
|
E --> F["mitmproxy on device<br/>(live HTTPS capture)"]
|
||||||
|
B --> G[(Findings)]
|
||||||
|
C --> G
|
||||||
|
D --> G
|
||||||
|
F --> G
|
||||||
|
```
|
||||||
|
|
||||||
|
- **jadx** decompiled the dex → Java. This is only the RN framework + native
|
||||||
|
modules + SDKs; the app's own logic is **not** here.
|
||||||
|
- The app is **React Native / Expo SDK 53**, so its business logic ships as
|
||||||
|
**Hermes bytecode** (`assets/index.android.bundle`, Hermes v96). **hermes-dec**
|
||||||
|
recovered readable pseudocode (`decomp.js`) with de-obfuscated function names.
|
||||||
|
- **apktool** decoded the manifest and resources.
|
||||||
|
- For the live capture, the split APKs were merged into one universal APK
|
||||||
|
(APKEditor) and repackaged to trust a user CA (`network_security_config`), then
|
||||||
|
driven through **mitmproxy**. (Merging first was necessary: patching only the
|
||||||
|
base split renumbers its resource IDs and desyncs the untouched `config.hdpi`
|
||||||
|
split, which crashes the app on the first text field.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App architecture & network model
|
||||||
|
|
||||||
|
Expo SDK 53 RN app. All API traffic goes through one request builder that stamps
|
||||||
|
these headers on **every** call:
|
||||||
|
|
||||||
|
| Header | Value | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `Application_Token` | `B66EEDDA-…` (static) | App-wide, embedded in the bundle |
|
||||||
|
| `X-Request-Id` | random UUID | Per request |
|
||||||
|
| `Auth_Token` | rolling | Rotates on each response |
|
||||||
|
| `ParkSmarter_SessionId` | per session | |
|
||||||
|
| `Content-Type` | `application/json` | POST/PUT only |
|
||||||
|
| `User-Agent` | `okhttp/4.12.0` | RN's HTTP stack |
|
||||||
|
|
||||||
|
Five hard-coded environments (bundle string literals):
|
||||||
|
|
||||||
|
- Prod: `https://apiv2.parksmarter.com`, `https://apiv3.parksmarter.com`
|
||||||
|
- Staging: `https://staging-parksmarter-api.ipsmeters.com`
|
||||||
|
- Testing: `https://testing-parksmarter-api.ipsmeters.com`
|
||||||
|
|
||||||
|
No TLS pinning is configured (okhttp's `CertificatePinner` class is present but no
|
||||||
|
pins are set), and the manifest declares no `networkSecurityConfig` — which is why
|
||||||
|
a stock install can't be MITM'd without repackaging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 1 — Device identifier: the "IMEINumber" that isn't an IMEI
|
||||||
|
|
||||||
|
The app defines a Redux thunk literally named `device/setIMEINumberAsyncThunk`.
|
||||||
|
Despite the name, it **never reads an IMEI**. It reads the **Android SSAID**
|
||||||
|
(`Application.androidId` from `expo-application`), caches it in secure storage as
|
||||||
|
`psUUID`, and later transmits it.
|
||||||
|
|
||||||
|
Evidence (`decomp.js`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// getAndroidId → expo-application's Application.androidId (the SSAID)
|
||||||
|
r0 = r0.default; r0 = r0.androidId; return r0;
|
||||||
|
// stored under 'psUUID'
|
||||||
|
r2 = 'psUUID'; r2 = setObjectAsync.bind(...)('psUUID', androidId);
|
||||||
|
// later placed on the wire:
|
||||||
|
r2['DeviceID'] = pushNotificationsToken;
|
||||||
|
r2['IMEINumber'] = imeiNumber; // === Application.androidId (SSAID)
|
||||||
|
r2['DeviceType'] = '1';
|
||||||
|
// endpoint: putUpdateDeviceToken → { url: '/api/Device' }
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirmations:
|
||||||
|
|
||||||
|
- **No real IMEI read.** Zero `getImei` / `TelephonyManager.getDeviceId` /
|
||||||
|
`getSubscriberId` / `getSimSerialNumber` in the Java **or** the bundle. Every
|
||||||
|
`getDeviceId()` match in the Java is unrelated — `MotionEvent`/`KeyEvent`
|
||||||
|
device IDs (input routing) or `Context.getDeviceId()` (the API-34 *virtual*
|
||||||
|
device id, an `int`). `react-native-device-info` (which could read IMEI) isn't
|
||||||
|
bundled at all.
|
||||||
|
- **`READ_PHONE_STATE`** comes from `@react-native-community/netinfo`, which uses
|
||||||
|
`TelephonyManager` only for `getNetworkOperatorName()` (carrier name) and
|
||||||
|
network type — not identity.
|
||||||
|
- **Modern Android blocks it anyway**: since Android 10, `getImei()` throws for
|
||||||
|
non-privileged apps regardless of the permission.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant App
|
||||||
|
participant OS as Android OS
|
||||||
|
participant PS as apiv2.parksmarter.com
|
||||||
|
Note over App: cold start / login / token change
|
||||||
|
App->>OS: Application.androidId (SSAID)
|
||||||
|
OS-->>App: e.g. "a1b2c3…"
|
||||||
|
App->>App: cache as psUUID (secure store)
|
||||||
|
App->>PS: PUT /api/Device<br/>{ IMEINumber: SSAID, DeviceID: pushToken, DeviceType: "1" }
|
||||||
|
Note over PS: a stable per-device ID,<br/>tied to the account — just not the IMEI
|
||||||
|
```
|
||||||
|
|
||||||
|
**Net:** the app does transmit a persistent, per-device identifier to its own
|
||||||
|
backend, mislabeled `IMEINumber`. It is the SSAID, not the hardware IMEI, and it
|
||||||
|
is **not** sent to any third party. The earlier "sends your IMEI" claim was wrong
|
||||||
|
in substance but pointed at something real — this field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 2 — GPS location
|
||||||
|
|
||||||
|
Location is acquired via **expo-location** (`getLastKnownPositionAsync` +
|
||||||
|
`watchPositionAsync` at `Accuracy.Balanced`) and sent to the backend **only** on
|
||||||
|
the meter-search endpoints:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/Meter?Lat=<lat>&Long=<long>&localeCode=en-US
|
||||||
|
```
|
||||||
|
|
||||||
|
The query carries **only** `Lat`/`Long` (no radius/limit); "all vs limited meters"
|
||||||
|
is an endpoint choice, not a parameter. Location is **not** attached to login,
|
||||||
|
session load, or payment — confirmed both in code and in the live capture.
|
||||||
|
|
||||||
|
**Auto-send on map open.** The map hub wires `useAutoGoToUserLocation`, a state
|
||||||
|
machine that — when `isAuthenticated && isMapReady && locationPermissionGranted`
|
||||||
|
and the app is foreground — recenters on the user's GPS and fires the meter query
|
||||||
|
**without any tap**. Panning/zooming fires more (debounced).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Map as Map screen
|
||||||
|
participant Loc as expo-location
|
||||||
|
participant PS as apiv2.parksmarter.com
|
||||||
|
User->>Map: open Map (logged in, permission granted)
|
||||||
|
Map->>Loc: watchPositionAsync (Balanced)
|
||||||
|
Loc-->>Map: {lat, long}
|
||||||
|
Map->>PS: GET /api/Meter?Lat=…&Long=… (auto, no tap)
|
||||||
|
User->>Map: pan / zoom
|
||||||
|
Map->>PS: GET /api/Meter?Lat=…&Long=… (debounced)
|
||||||
|
```
|
||||||
|
|
||||||
|
**GrapheneOS caveat (observed).** `react-native-maps` needs Google Play Services
|
||||||
|
for tiles; on GrapheneOS the map doesn't render, so `isMapReady` may never flip
|
||||||
|
and the auto-fetch can be suppressed. In the live capture the single coordinate
|
||||||
|
sent (`Lat=48.274147628&Long=-116.550122619`) exactly matched a *known lot*
|
||||||
|
location rather than a fresh arbitrary GPS fix — consistent with the map being
|
||||||
|
degraded. Location egress is proven; the fully-automatic-on-open behavior is a
|
||||||
|
property of the code that a Play-Services device would exercise more visibly.
|
||||||
|
|
||||||
|
**BigBrainParking, by contrast**, never sends GPS to the API except when you
|
||||||
|
explicitly tap "My location" and search; the map opens on your last lot from
|
||||||
|
history, not your GPS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 3 — Third-party telemetry: Instabug (and only Instabug)
|
||||||
|
|
||||||
|
Contrary to the earlier analysis, **Segment, Amplitude, Sentry, Firebase
|
||||||
|
Analytics, Crashlytics, advertising-ID, and Play install-referrer are all
|
||||||
|
absent** (verified by package-dir and endpoint-host search). Firebase is present
|
||||||
|
only as **Cloud Messaging** (push), not analytics.
|
||||||
|
|
||||||
|
The one real telemetry SDK is **Instabug** (2,328 classes), and its scope is
|
||||||
|
broad:
|
||||||
|
|
||||||
|
- **Session replay** (`library/sessionreplay`)
|
||||||
|
- **Screenshot / screen capture** (`instacapture`, `screenshot`)
|
||||||
|
- **Network-request logging & interception** (`apm/networking`, `networkinterception`)
|
||||||
|
- **Visual user-steps / interaction tracking** (`visualusersteps`, `interactionstracking`)
|
||||||
|
- Crash reports, APM, surveys, user attributes
|
||||||
|
|
||||||
|
All reporting to `api.instabug.com`. Captured live on startup:
|
||||||
|
`POST /api/sdk/v3/sessions/v2`, `GET /api/sdk/v3/features`, `/api/sdk/v3/first_seen`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
D[Your device]
|
||||||
|
D -->|"SSAID as IMEINumber + push token<br/>GPS (Lat/Long) on meter search<br/>account, vehicle, payment, sessions"| PS[apiv2.parksmarter.com]
|
||||||
|
D -->|"session replay, screenshots,<br/>network logs, user-steps, crashes"| IB[api.instabug.com]
|
||||||
|
D -->|push registration| FCM[Firebase Cloud Messaging]
|
||||||
|
style PS fill:#294,color:#fff
|
||||||
|
style IB fill:#922,color:#fff
|
||||||
|
style FCM fill:#247,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permissions (from the manifest)
|
||||||
|
|
||||||
|
Present: `ACCESS_FINE/COARSE_LOCATION`, `READ_PHONE_STATE` (netinfo),
|
||||||
|
`ACCESS_WIFI_STATE`, `CAMERA`, `RECORD_AUDIO`, `BLUETOOTH_SCAN/CONNECT` (meter
|
||||||
|
BLE), `POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, `FOREGROUND_SERVICE`,
|
||||||
|
`USE_BIOMETRIC`, `SYSTEM_ALERT_WINDOW`, `DETECT_SCREEN_CAPTURE`, install-referrer
|
||||||
|
service binding, plus a long list of launcher badge permissions.
|
||||||
|
|
||||||
|
**Absent (important):** `ACCESS_BACKGROUND_LOCATION`. The app **cannot** track
|
||||||
|
location while closed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Live capture — evidence timeline
|
||||||
|
|
||||||
|
Captured through mitmproxy against a repackaged (user-CA-trusting) universal APK,
|
||||||
|
authenticated session:
|
||||||
|
|
||||||
|
```
|
||||||
|
18:44:07 GET apiv2.parksmarter.com/api/ParkingSession
|
||||||
|
18:44:07 GET apiv2.parksmarter.com/api/Session
|
||||||
|
18:44:08 POST api.instabug.com/api/sdk/v3/sessions/v2 (414B) ← telemetry
|
||||||
|
18:44:08 GET api.instabug.com/api/sdk/v3/first_seen
|
||||||
|
18:44:16 GET apiv2.parksmarter.com/api/Meter?ZoneName=dl
|
||||||
|
18:44:17 GET apiv2.parksmarter.com/api/Meter?Lat=48.274…&Long=-116.550… ← GPS
|
||||||
|
18:44:20 GET apiv2.parksmarter.com/api/ParkingEstimateItems
|
||||||
|
18:44:33 GET apiv2.parksmarter.com/api/ApplicationValidity
|
||||||
|
18:44:39 GET apiv2.parksmarter.com/api/ParkSmarterPrivacyPolicies
|
||||||
|
```
|
||||||
|
|
||||||
|
Header sample on a parksmarter request: `Application_Token: B66EEDDA-…`,
|
||||||
|
`Auth_Token: …` (rolling), `ParkSmarter_SessionId: …`, `User-Agent: okhttp/4.12.0`.
|
||||||
|
|
||||||
|
`PUT /api/Device` (the `IMEINumber` PUT) did **not** fire in this session because
|
||||||
|
the login was cached — it sends on fresh login / token change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reproducing this
|
||||||
|
|
||||||
|
The repackaged capture-ready APK and step-by-step mitmproxy instructions live in
|
||||||
|
the private source mirror (`ParkSmarterSourceCode`, `MITMPROXY.md`). In short:
|
||||||
|
merge splits → inject a user-CA `network_security_config` → resign → install the
|
||||||
|
CA as a user cert → proxy the phone through mitmproxy (mobile data **off** so the
|
||||||
|
Wi-Fi proxy applies).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix — backend endpoint inventory (from the bundle)
|
||||||
|
|
||||||
|
`ApplicationValidity`, `Auth`, `SignUp`, `User`, `Device`, `Session`,
|
||||||
|
`ParkingSession`, `Meter` (by location / scanner code / serial / zone name),
|
||||||
|
`ParkingLots`/`ParkingLogix`, `ParkingEstimateSingle/Multi/Items`,
|
||||||
|
`StartParkingSession`, `ParkingReceipt`, credit-card + vehicle CRUD,
|
||||||
|
`NotificationSettings`, `ParkSmarterPrivacyPolicies` / `Terms` / `About` / `FAQ`.
|
||||||
|
```
|
||||||
Loading…
Add table
Add a link
Reference in a new issue