research/gopay-merchant-qris-gateway/reverse-engineering.md

11 KiB
Raw Permalink Blame History

title description status category tags draft created updated
Reverse engineering — methods & drift playbook How the GoPay/GoBiz merchant APIs were reverse engineered — browser HAR captures and byte-for-byte replay, APK static analysis (apktool/androguard/strings), code archaeology, version-drift tracking, and the ethics/hygiene rules the researchers follow. concluded technical
reverse-engineering
har-capture
apk-analysis
gobiz
gopay
methodology
true 2026-10-15 2026-10-15

Reverse engineering — methods & drift playbook

Part of gopay-merchant-qris-gateway/index

1. The three evidence channels

The API knowledge behind these gateways came from three complementary methods — plus one public spec that needed no reverse engineering at all:

Channel Object Output Fidelity
A. Browser HAR captures portal.gofoodmerchant.co.id sessions (merchant's own login) Exact request/response shapes, headers, token formats Highest — live traffic, byte-for-byte
B. APK static analysis GoPay Merchant 2.3.0 (APKM), GoFood Merchant 5.49.0 (XAPK) ~250 endpoint paths, hosts, anti-tamper map, sister-app evidence Static strings only — mostly runtime-unverified
C. Code archaeology Earlier open-source repos (kavionn, warungerik, namtxs, alhifnywahid) Hypotheses, working patterns, prior art Variable — often unverified, to be capture-checked
EMVCo QRIS spec Public standard TLV structure, tag 54, CRC16 rules N/A (public)

All methods operate on the researchers' own merchant accounts and publicly distributed artifacts; no backend probing, no third-party data. [S9 §5][S11]

2. Channel A — HAR captures & byte-for-byte replay

Documented captures (all on the merchant's own account) [S6 §9, S7, S13]:

Capture Client Size / entries What it verified
gobizverbrowser.har iPhone WebView (mark.via.gp) → portal ~80 MB, 1063 entries, 77 API calls OTP flow, merchants/search, users/me, analytics, journals (+aggregation variant), payouts — plus 5 endpoints no repo had
GOTO.har (19 Sep 2026) Firefox desktop ~51 MB, 589 entries (2-min session) OTP login drift: X-AppVersion bump, bare phone format, split headers confirmed
emialku.har.json (19 Sep 2026) Reqable 169 entries (19 s) Email+password flow verified end-to-end
Browser HAR (3 Oct 2026) Browser → portal (size not stated) New build v3.125.0; auth calls moved to the portal host [S13]

Method, as documented in the repo's reproduction guide and reports [S6][S7][S9]:

  1. Capture the portal session with a normal browser/devtools export (Chrome/Firefox/WebView/Reqable) — i.e. record the merchant's own login and dashboard usage.
  2. Map every host/path, isolate the merchant API calls (e.g. 30 calls to api.gobiz.co.id out of 589 entries), and pick the flows of interest.
  3. Diff captured requests against client code header-by-header and field-by-field. Every mismatch is either a bug or drift; every match upgrades the client claim from [reported] to [verified].
  4. Cross-check the data endpoints (merchants/search, journals/search, payouts, analytics) against the SDK's assumptions.
  5. Fix minimally, mirror the browser byte-for-byte — the repo's stated principle ("tiru browser, byte-per-byte").

Credential hygiene is explicit — captures contain live access/refresh tokens, phone numbers, email, otp_token [S7 preamble]: HAR files are .gitignored and kept outside the repo; every sensitive value is masked in reports ("format only, e.g. 11-digit bare national digits"); APKs are deleted after analysis and only hashes retained. [S9 §3.9, S11]

3. The drift playbook (what breaks, and how it was found)

The portal is a moving target; the documented fixes read like a clinician's notes [S6 §9, S7, S13]:

  1. X-AppVersion bumps (the client build string sent on /goid/*): platform-v3.107.0 (reference repos, Jun–Jul 2026) → v3.119.0 (mid-Sep capture) → v3.122.0 (19 Sep) → v3.125.0 (3 Oct). Evidence that the server gates on it: an OTP flow that "doesn't work" was fixed by bumping nothing but this string + phone format. [S7 §3.1/§3.3/§4]
  2. Phone number normalisation: the portal always sends bare national digits (851…); passing 0851… or +62851… through produced server-side failures. Fixed with a normaliser that strips +, 62, one leading 0. [S7]
  3. login_type is vestigial: reference repos send login_type:"otp" on the OTP request; the browser sends no such field; server ignores it either way. QMID removed it to match the browser exactly. [S6 §9.2]
  4. Auth host moved: by Oct 2026 the browser sends /goid/login/request + /goid/token to https://portal.gofoodmerchant.co.id while data APIs stay on https://api.gobiz.co.id. Clients updated to route per-call. [S13][S10]
  5. Request-body drift: the portal started adding _source:[…] to merchants/search; the older bare {from,size} shape was kept after being verified non-broken. [S7 §3.4]
  6. Split-header discipline: device headers (X-AppVersion, X-PhoneModel, …) appear only on /goid/*; sending them everywhere or nowhere is a fingerprint mismatch. The SDK mirrors the split. [S7 §3.3]

[inference] The pattern across all six: GoBiz tolerates a plausible browser, not an arbitrary client. The RE work is less "break the gate" and more "stay indistinguishable from a current browser session" — an ongoing treadmill that explains why these repos need continuous maintenance and why version bumps break communities of users at once.

4. Channel B — APK static analysis

The QrisMerchantID project ran a full static-analysis pipeline on both merchant apps and published both the results and the reproduction guide [S8][S9]. Pipeline (from the guide) [verified — commands in S9]:

  1. Download the container from APKMirror/APKPure (APKM for GoPay Merchant 2.3.0 — com.gojek.gopaymerchant; XAPK for GoFood Merchant 5.49.0).
  2. Hash everything (md5sum/sha1sum/sha256sum), verify signature/certificates with androguard (CN=Gojek cert 2014–2039 + Google Play App Signing; v2-signed).
  3. apktool d decode → smali; strings on the Flutter AOT blob (libapp.so, 22 MB) for the Dart-half of the app.
  4. Grep for URL/host patterns in smali + libapp strings → endpoint inventory; scan for secrets (with mandatory masking), anti-tamper indicators (frida/xposed/root/ptrace/Play Integrity counts), crypto usage.
  5. Write an honest report: findings graded (0 critical), everything unproven marked BELUM TERVERIFIKASI + TODO, dynamic analysis explicitly skipped (no device/emulator available), tools recorded for reproducibility.

Outputs relevant here [S8][S12]:

  • ~150 GoPay-Merchant-app endpoints under groups: histories/reconciliation (/api/v1/unified-histories, /api/v2/histories), finance (/api/v1/finance/balance/*), transactions & QRIS (/api/transactions/charge/qris-link, /api/v1/qris/shared), payouts, auth (/api/auth/token, /api/v1/login/complete, /api/v1/totp), PIN endpoints, configs.
  • Realtime WebSocket channel: wss://gopay-merchant-raccoon.gojekapi.com/api/v1/events (protobuf odpf.raccoon.v1beta1) — the app's push path, an eventual replacement for polling if someone traffic-verifies it.
  • Hosts incl. gopaymerchant.midtrans.com (Midtrans gateway), customer.gopayapi.com, app.gwk.gopayapi.com, SSO accounts.goto-products.com.
  • Anti-tamper is heavy (frida/xposed/root/emulator/Play Integrity checks in the app), so dynamic hooking of the app is hard; the web portal path (Channel A) avoided all of that, which is [inference] the strategic reason the tooling standardized on portal replay instead of app interception.
  • Sister-app evidence: GoPay Merchant app and GoFood Merchant app share the same CN=Gojek signing certificate, 6 identical PIN SDK endpoint paths, and a shared module set — and GoPay Merchant's T&C links point at GoBiz. The maintainer of gobiz-payment had answered a user question (issue #3) that GoPay Merchant transactions are visible on the GoBiz web for the same account; the APK work independently confirmed "same account/system, different API gateways". [S8 §5.4][S4]

Honesty caveat, verbatim in spirit: the endpoint CSV marks all ~249 entries as static strings, runtime use unverified (file column values are all BELUM TERVERIFIKASI). Only Channel-A endpoints are capture-verified. [S12]

5. Channel C — code archaeology & cross-verification

The flagship research doc [S6] shows the working method: clone and read the prior repos (kavionn's gobiz.js read fully; warungerik's user-tested gateway; merchantid for ShopeePay; namtxs/gopay-api as the customer-side comparator), then verify every inherited claim against captures before shipping code. Examples of corrections this produced: login_type (dropped), X-AppVersion (bumped), phone format (normalized), analytics/journal shapes (confirmed 1:1). Unverified claims kept labelled TODO. Prior art is credited (README credits list) as a hard project rule. [S1][S6]

6. What the reverse engineering produced — and what it couldn't

Produced [verified]: a complete, capture-backed map of the merchant web-portal API subset (auth, merchants, analytics, journals, payouts), a static endpoint map of the mobile apps, and a working pattern (dynamic QRIS + watcher) replicated across at least 8 independent repos within ~4 months.

Couldn't / didn't [unverified, per the sources' own TODOs] [S15]:

  • the app-side live flow (needs device + frida/mitmproxy; app has anti-tamper);
  • the realtime WebSocket event protocol (identified, never hooked);
  • request signing specifics — no static signing secret exists in the client and none was observed on the web path, so "how the server authenticates beyond the Bearer token" is simply not answered;
  • impact of the app's embedded Google API key / staging hosts (static indicators, never tested).

7. Counterpoint: reverse engineering vs. readable results

The knowledge was gained by replaying the merchant's own browser session, but the flagship gateway repo (gopay-api-gateaway) then closed the loop by obfuscating its own auth code (login.js, sessionManager.js built with javascript-obfuscator; no plain strings leak). So the community can reuse the public API knowledge, but cannot audit what the most popular implementation does with the credentials it collects — a distribution choice, not an RE constraint. kavionn/gobiz-payment and QrisMerchantID went the other way: fully readable code + published methodology. [S3][S2][S1]

8. Assessment

[inference] This is a textbook example of gray-box research on one's own entitlements: using (a) your own account, (b) your own traffic, (c) publicly distributed apps and code — while staying away from probing, credential testing, and third-party data. The research outputs are unusually well-documented for this niche (reproduction guide, conflict register, responsible-disclosure doc, masking rules). The remaining ethical question isn't the method — it's the use: the same knowledge automates your own merchant account, and nothing in the pipeline prevents someone from pointing it at an account that isn't theirs (stolen session → payouts/balance visibility). Nothing in the audited repos encourages that; all three carry disclaimers, and the gateways self-host credentials rather than relaying them to a SaaS. [S1][S11]