gopay-merchant-qris-gateway: add topic — GoPay/GoBiz merchant→QRIS gateway reverse-engineering audit (gopay-api-gateaway, gobiz-payment, QrisMerchantID, pinned commits)
This commit is contained in:
parent
d512cd7514
commit
376a811476
6 changed files with 589 additions and 0 deletions
154
gopay-merchant-qris-gateway/analysis.md
Normal file
154
gopay-merchant-qris-gateway/analysis.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
title: Analysis — GoPay Merchant QRIS gateways
|
||||
description: Mechanism deep-dive — GoID portal auth, the merchant transaction feeds, offline EMVCo dynamic QRIS generation, the polling watcher, claim/dedupe design, and how the three repos differ.
|
||||
status: concluded
|
||||
category: technical
|
||||
tags: [gopay, gobiz, qris, emvco, payment-gateway, api-replay, mechanism]
|
||||
draft: true
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# Analysis — GoPay Merchant QRIS gateways
|
||||
|
||||
> Part of [[gopay-merchant-qris-gateway/index|GoPay Merchant QRIS gateways]]
|
||||
|
||||
## 1. Why this exists
|
||||
|
||||
GoPay/GoBiz does not offer small merchants a public, self-serve "payment gateway API". Officially, automated QRIS acceptance goes through licensed **PJP** gateways (Midtrans, Xendit, DOKU, …) with business onboarding and fees. Meanwhile every GoPay/GoBiz merchant already has: a registered QRIS (the static QR in the app), and a portal/app that shows every incoming payment in real time. This ecosystem closes the gap with automation instead of licensing [inference, consistent with [S6]]:
|
||||
|
||||
- **Payment presentation** — generate a dynamic QRIS per order from the merchant's own static QRIS string, offline.
|
||||
- **Payment acknowledgment** — watch the merchant's own transaction feed for the matching incoming payment, then confirm to the store backend.
|
||||
|
||||
Nothing else is needed for the common use case (web store / bot / WhatsApp shop that needs "did this order get paid?" answered automatically). No funds move through the tool; settlement follows normal QRIS rails to the merchant's own balance.
|
||||
|
||||
## 2. Architecture at a glance
|
||||
|
||||
```text
|
||||
┌─────────┐ scans ┌──────────────────┐ normal QRIS rails ┌─────────────────┐
|
||||
│ Buyer │ ─────────► │ Dynamic QRIS │ ────────────────────► │ GoPay / merchant│
|
||||
│ (any │ │ (offline TLV edit│ (buyer's app pays, │ balance / bank │
|
||||
│ QRIS │ │ of merchant's │ settlement T+…) │ (own account) │
|
||||
│ wallet) │ │ static QR) │ └────────┬────────┘
|
||||
└─────────┘ └────────▲─────────┘ │
|
||||
│ rendered │ transaction appears
|
||||
┌────────┴─────────┐ │ in merchant feed
|
||||
│ Store backend / │ poll every ~6 s │
|
||||
│ bot / gateway │ ────────────────► GoBiz/Gojek private APIs
|
||||
│ process │ ◄──────────────── (merchant's own token)
|
||||
└──────────────────┘ "payment found for Rp N at time T"
|
||||
```
|
||||
|
||||
The "gateway" is everything in the bottom box. Money flow and the tool are completely separate — the tool only *reads* the merchant's own data and *writes* a QR payload locally. [verified: code of all three repos]
|
||||
|
||||
## 3. Authentication — GoID portal replay
|
||||
|
||||
All three repos authenticate the **merchant's own account** against GoBiz's web login ("GoID"), by replaying what the browser portal does. Endpoints and shapes below are capture-verified ("HAR") per [S6][S7][S10]; code refs from [S2][S10].
|
||||
|
||||
**OTP flow** [verified, capture §9.1/§9.4 + code]:
|
||||
|
||||
1. `POST /goid/login/request` — body `{client_id: "go-biz-web-new", phone_number: "<bare national digits>", country_code: "62"}` → HTTP 201 `{data:{otp_token, otp_expires_in: 720, otp_length: 4, next_state:{state:"sms", timer_in_seconds: 120}}}`.
|
||||
- The live portal sends **no `login_type` field** here (some clients send `login_type:"otp"`; the server ignores it). [S6 §9.2]
|
||||
- Phone must be bare national format (`851…`): no `+`, no `62`, no leading `0`. The portal never sends prefixes; a client that passes `0851…` through breaks server-side. [S7]
|
||||
- OTP is **4 digits** per capture — but clients should read `otp_length`/`otp_expires_in` from the response instead of assuming. [S10]
|
||||
2. `POST /goid/token` — body `{client_id, grant_type:"otp", data:{otp, otp_token}}` → 201 `{access_token, refresh_token, dbl_enabled: true}`. The access token is a **JWE** string (~2.3 KB); refresh token ~751 chars. [S7]
|
||||
- Rate limit observed on the request endpoint: 10/min. [S7]
|
||||
|
||||
**Email+password flow** [verified live via capture §9.5]: `POST /goid/login/request` `{email, login_type:"password", client_id}` (validation probe) → `POST /goid/token` `{client_id, grant_type:"password", data:{email, password}}` → same token pair. Prerequisite: email+password must already be set on the merchant account in the portal. [S7][S10]
|
||||
|
||||
**Refresh:** `POST /goid/token` with `grant_type:"refresh_token"`, `data:{refresh_token}` rotates both tokens. [S10; `auth.refresh_session()`]
|
||||
|
||||
**Host split** [verified, Oct 2026 build]: auth calls (`/goid/*`) go to the **portal host** `https://portal.gofoodmerchant.co.id`; merchant/data APIs stay on `https://api.gobiz.co.id`. (Earlier captures had everything on `api.gobiz.co.id`; the Oct 3 2026 capture shows the browser sending auth to the portal host, and the SDK routes accordingly.) [S10 constants.py; S13]
|
||||
|
||||
**Header fingerprint** [verified in code, capture-diffed]: clients copy the portal's browser/device headers so requests are byte-compatible. Notable fields [S2][S10]:
|
||||
|
||||
```
|
||||
Authentication-Type: go-id X-User-Type: merchant
|
||||
x-appId: go-biz-web-dashboard X-Platform: Web
|
||||
x-uniqueid: <random UUID> X-AppVersion: platform-v3.125.0-e1923971 ← portal build string
|
||||
Origin/Referer: https://portal.gofoodmerchant.co.id
|
||||
Gojek-Country-Code: ID Gojek-Timezone: Asia/Jakarta
|
||||
User-Agent / X-PhoneMake / X-PhoneModel / sec-ch-ua… (Chrome desktop impersonation)
|
||||
```
|
||||
|
||||
- **Split-header design** [verified]: the full device header set appears **only** on `/goid/*` auth calls; data calls send a slimmer set. Scanning 589 captured entries found `X-AppVersion` **zero** times outside auth calls. [S7 §3.3]
|
||||
- `X-AppVersion` is the drift hot spot — clients are version-gated. Timeline in [[gopay-merchant-qris-gateway/reverse-engineering|reverse-engineering]] §3.
|
||||
- The journals endpoint additionally needs `Accept: …application/vnd.journal.v1+json` + `X-Client-Id: gobiz-web`. [S6 §9.2][S10]
|
||||
|
||||
**Token lifecycle differences across repos** [verified in code]:
|
||||
- `gobiz.js` [S2 L292–L304, L356–L359, L500–L511]: stores only the access token; probes validity via `POST /v1/merchants/search` (`401` = dead); on 401 it re-runs the **full login** (no refresh-token use).
|
||||
- `QrisMerchantID` [S10]: caches access + refresh; auto-refreshes **once** on an expired-token response, then retries; if refresh is rejected it stops and requires re-login.
|
||||
- `gopay-api-gateaway` [S3]: has a 6-hour background re-verify/refresh cycle (`server.js` L56–L69) and refreshes on 401 via `sessionManager` — whose implementation is **obfuscated** and unauditable (see [[gopay-merchant-qris-gateway/security|security]] §3).
|
||||
|
||||
## 4. Merchant & transaction data endpoints
|
||||
|
||||
The working subset of the API, capture-verified [S6 §9, S10]:
|
||||
|
||||
| Call | Host + path | Purpose |
|
||||
|---|---|---|
|
||||
| POST | `api.gobiz.co.id/v1/merchants/search` body `{from, size}` | Merchant list / token probe; returns `hits` with merchant `id` (`G…`) — used as merchant-ID discovery |
|
||||
| GET | `api.gobiz.co.id/v1/users/me` | Portal profile (roles, scopes, merchant_id) |
|
||||
| GET | `api.gojekapi.com/merchant-analytics/v2/merchants/transactions?from&size&statuses&payment_types&start_time&end_time&merchant_ids` | **Primary payment feed** (the watcher's source) |
|
||||
| POST | `api.gobiz.co.id/journals/search` (query DSL) | Detailed ledger + aggregations (fallback / issuer breakdown) |
|
||||
| GET | `api.gobiz.co.id/v1/merchants/payouts?page&per` + `…/payable_detail?merchant_id=` | Payout history + payable balance (discovered from HAR; absent from the JS reference repos) |
|
||||
|
||||
- Defaults mirror the portal exactly: `statuses=SETTLEMENT,CAPTURE,REFUND,PARTIAL_REFUND`; `payment_types=QRIS,GOPAY,OFFLINE_CREDIT_CARD,OFFLINE_DEBIT_CARD,CREDIT_CARD`. [S6][S10]
|
||||
- Analytics transaction objects carry 23 keys including `order_id` (`QRIS-…`), `transaction_status`, `payment_type`, `channel_type` (`STATIC_QR`), `transaction_source` (`GOPAY_INSTORE`), `qris_provider_aspi_issuer/acquirer`, `shares`, `promo_details`. [S6 §9.1]
|
||||
- The journal DSL used by `gobiz.js` [S2 L520–L629] filters to incoming `transaction_share`/`action` categories, excludes `GOSAVE_ONLINE/GoSave/GODEALS_ONLINE` sources, and ranges on `metadata.transaction.transaction_time` — an Elasticsearch-style query an app would never hand-write; it was lifted from the portal.
|
||||
|
||||
**Money units [verified, but with one cross-repo discrepancy].** GoPay answers amounts in **minor units (sen)**: `gross_amount: 10600000` = Rp 106 000; payouts use decimal strings at the same scale. [S6 §9.1, S10 money.py]
|
||||
|
||||
- `gobiz.js` divides by 100 (`tx.gross_amount / 100`) for display and matching [S2 L407, L435, L678–L681].
|
||||
- `QrisMerchantID` exposes raw minor units and provides `to_rupiah()`; its watcher matches on **minor units** (`wait_for_payment(5_000_000)` = Rp 50 000). [S10]
|
||||
- `gopay-api-gateaway`'s `verifyPayment()` [S3 L615–L620] compares `parseInt(tx.gross_amount)` **directly against a rupiah input with no conversion**. If the feed returns sen (as the other two repos and the capture evidence say), that equality can never hold — so either its deployment was validated against a different response shape, or `/check-payment` silently never confirms. **[unverified discrepancy — flagged, not resolvable without a live account.]**
|
||||
|
||||
## 5. Dynamic QRIS — the offline half
|
||||
|
||||
QRIS payloads are **EMVCo TLV strings**: `TAG(2 chars) + LENGTH(2 digits) + VALUE`. To turn the merchant's static QR into a per-order dynamic one [verified in all three repos; public spec, no RE needed]:
|
||||
|
||||
1. Parse TLV fields.
|
||||
2. Set tag **`54`** (transaction amount) to the order amount — insert if absent. Amount is plain rupiah digits, e.g. `25000`.
|
||||
3. Strip old tag **`63`** (CRC); append literal `6304` + recomputed **CRC16-CCITT-FALSE** (poly `0x1021`, init `0xFFFF`).
|
||||
4. Render the resulting string as a QR image.
|
||||
|
||||
Divergence between implementations [verified in code]:
|
||||
|
||||
- `gopay-api-gateaway` **also rewrites tag `01`** from static (`11`) to dynamic (`12`) [S3 L119–L121].
|
||||
- `QrisMerchantID` **deliberately does not touch tag `01`** and warns that "a valid checksum alone is not proof that every bank or wallet will accept the result" — the provider rule (static→dynamic flag) is a payment-layer decision, not something to assume locally. [S10 qris.py + docs/gopay/qris.md]
|
||||
|
||||
Both approaches circulate in the ecosystem; which is "correct" in production is **[unverified]** — it depends on what QRIS acquirers enforce when they see tag 54 with tag 01 still `11`. Both repos' QR strings are reported working by their communities, so in practice acquirer/wallet handling appears tolerant. [reported]
|
||||
|
||||
Why it works at all: the merchant identity fields (tag 26–51 ranges, NMID etc.) are untouched — the QR still routes to the same registered merchant; only the amount (and optionally the initiation flag) is local. The payment then appears in the merchant's own feed under `channel_type: STATIC_QR` [S6 §9.1] — note that even gateway-rendered dynamic QRs land as static-channel transactions, which is a useful signal that the network treats them as merchant-presented static QR with amount filled in. [inference from capture data]
|
||||
|
||||
## 6. Payment watcher — the online half
|
||||
|
||||
The pattern is the same everywhere ("ported from `gobiz.js`" is literally QMID's docstring) [verified in code S2 L632–L748, S10 watcher.py]:
|
||||
|
||||
1. **Seed** — fetch current history, mark every existing transaction as seen. Prevents old payments from being claimed by new orders.
|
||||
2. **Poll** — every **~6 s** (default in all implementations), fetch the last ~30 transactions (`days=1`).
|
||||
3. **Diff** — any transaction id not in the seen set is new → candidate. Seen set capped at 500 entries (memory-leak guard).
|
||||
4. **Match** — amount equal (± tolerance, default 0), optionally time-bounded by order creation.
|
||||
5. **Deadline** — a checkout waits up to ~5 min (300 s) for a match; else the order lapses. The gateway's hosted page mirrors this with a 5-minute countdown and an explicit expiry.
|
||||
|
||||
Notably, ecosystem etiquette (community advice, adopted by QMID) is to **poll only while a checkout is active** — an "on-demand poller" so idle traffic is 0 — to reduce rate-limit/suspension exposure. [S4][S6 §2.3]
|
||||
|
||||
## 7. Order matching, claims, and the double-claim problem
|
||||
|
||||
Two buyers paying the same nominal at the same time is the core race condition. Implemented defenses [verified in code]:
|
||||
|
||||
- **`gopay-api-gateaway`** generates a unique `trx_id` per QR (`TRX-…`) and scopes every claim: transaction → `{qrisId, claimedAt}` map; a transaction already claimed by another QR is skipped for everyone else; same QR re-checks return the same result. Claims expire after 24 h; cleanup hourly. **The map is in-process RAM** — a restart loses all claims (double-claim possible again within 24 h; also multi-instance deployments share nothing). [S3 L16–L53, L566–L653]
|
||||
- **`gobiz.js` / `QrisMerchantID`** dedupe by transaction id in the watcher's seen set; the SDK docs additionally recommend baking a **unique Rp1–99 code into the billed amount** so nominals don't collide in the first place — the recipe "every gateway uses". [S2][S6 §2.4, S10]
|
||||
|
||||
## 8. Store-integration shapes (what the three repos actually offer)
|
||||
|
||||
- **`gopay-api-gateaway` — a service.** Express REST API (`/create-qris`, `/check-payment`, `/transactions`, `/token-status`, `/api/logs`) + a hosted checkout page (`/qr/:id`) with countdown, a manual "check payment" button (deliberately avoiding constant polling), and optional 8 s auto-poll; a public status endpoint (`/api/qr-status/:id`, no API key) so the buyer's page never sees the gateway key. Supports GET or POST, deployable on VPS/cPanel/Pterodactyl/Docker. [S3 server.js L207–L727]
|
||||
- **`gobiz-payment` — a library.** `GoPayMerchant` (init/login/history) + `GoPayWatcher` (EventEmitter; `waitForPayment(amount)`; singleton process-wide poller via `getGoPayWatcher()`); embed it in any bot/backend. History falls back analytics → journal automatically. [S2]
|
||||
- **`QrisMerchantID` — a research SDK.** Facade `GoPayMerchant` with granular services (`auth`, `users`, `merchants`, `transactions`, `payouts`, `watch()`), typed errors (`ApiException` carries `http_status`, `code`, `payload`), retry policy (transport retries with backoff; HTTP errors never retried), offline test suite (100 % coverage claimed), plus a separate ShopeePay provider. [S1][S10]
|
||||
|
||||
## 9. Failure modes & reliability notes
|
||||
|
||||
- **Version drift** — the dominant breakage class: stale `X-AppVersion`/host routing suspected in an OTP regression; fixed by tracking portal builds. [S7][S13]
|
||||
- **Token expiry** — handled with re-login (all) / single refresh attempt (QMID) / 6-hourly refresh (gateway, unauditable internals). [verified in code]
|
||||
- **Feed anomalies** — field report of `getHistory` returning empty for all ranges; maintainer replied it still works (likely account/data scope, not endpoint death). [S14]
|
||||
- **OTP delivery** — an open issue asks why OTP doesn't arrive; consistent with version-gating/format issues rather than a dead flow. [S14]
|
||||
- **Single point of failure** — every design depends on one portal account's session; suspension or forced logout stalls all automation. No repo implements failover to a second account. [inference]
|
||||
59
gopay-merchant-qris-gateway/index.md
Normal file
59
gopay-merchant-qris-gateway/index.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
title: "GoPay Merchant QRIS gateways — how a merchant account becomes an unofficial payment gateway"
|
||||
description: "Reverse-engineering audit of three open-source repos (gopay-api-gateaway, gobiz-payment, QrisMerchantID): how a GoPay/GoBiz merchant's own QRIS + private portal APIs are automated into a self-hosted 'payment gateway' — dynamic QRIS via EMVCo TLV, payment confirmation by polling the merchant's own transaction feed, and the HAR-capture/APK-analysis methods used to reverse engineer it. Pinned at Oct 2026 commits."
|
||||
status: concluded
|
||||
category: technical
|
||||
tags: [gopay, gobiz, qris, payment-gateway, reverse-engineering, emvco, har-capture, apk-analysis, api-replay, indonesia, fintech, unofficial-api, gojek]
|
||||
draft: true
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# GoPay Merchant QRIS gateways
|
||||
|
||||
**Research question:** How do tools like `gopay-api-gateaway`, `gobiz-payment`, and `QrisMerchantID` turn a GoPay/GoBiz merchant account into a "payment gateway API" — how does the mechanism actually work, and how were the underlying APIs reverse engineered?
|
||||
|
||||
**Short answer [verified].** There is no fake gateway and no money-routing trick. All three repos automate **two things the merchant can already do with their own account**: (1) **render** a dynamic QRIS for an order — done fully offline by editing the merchant's own static QRIS string (EMVCo TLV: inject the amount into tag 54, recompute CRC16); and (2) **detect** the matching incoming payment by polling the merchant's own transaction feed through GoBiz's private web-portal API, authenticated as the merchant. Settlement still happens on GoPay's normal QRIS rails (buyer's wallet/bank app → merchant's GoPay/GoBiz balance). The "gateway" is presentation + reconciliation automation around the merchant's own account — which is why it works without any licensed payment provider (PJP), and why it sits in a ToS/compliance gray zone.
|
||||
|
||||
**How it was reverse engineered [verified].** Mostly *gray-box*, not exploits: the APIs were recovered by capturing the merchant's own web-portal sessions (HAR exports shared between researchers), replaying them request-by-request ("mirror the browser, byte-for-byte"), reading older open-source repos, and extracting endpoint inventories from the public Android APKs (apktool/androguard/strings on the Flutter app). The QR half needed no reverse engineering at all — EMVCo QRIS is a public spec. The ongoing maintenance burden is **client-version drift**: the portal appears to gate requests on an `X-AppVersion` build string and has moved its auth endpoints, so replay clients must track portal releases (observed sequence v3.107 → v3.119 → v3.122 → v3.125 between Jun–Oct 2026).
|
||||
|
||||
## Repos audited
|
||||
|
||||
| Repo | What it is | Lang · license | Stars (captured 2026-10-15) | Audit note |
|
||||
|---|---|---|---|---|
|
||||
| [S3] `ahmadzakiyox/gopay-api-gateaway` | Self-hosted Express gateway: REST API + hosted QRIS checkout page | Node.js · MIT | 233 ★ / 84 forks | `server.js` readable; `login.js` + `sessionManager.js` **obfuscated** |
|
||||
| [S2] `kavionn/gobiz-payment` | Library: `GoPayMerchant` client + polling watcher | Node.js · MIT | 73 ★ / 29 forks | Fully readable; primary code reference |
|
||||
| [S1] `AlfinAI/QrisMerchantID` | Python SDK + **the public RE knowledge base** (APK analyses, HAR verification reports, reproduction guide) | Python · MIT | 12 ★ / 2 forks | Rich documentation; parts explicitly runtime-unverified |
|
||||
|
||||
## Key findings
|
||||
|
||||
1. **[verified] The "gateway" never touches money.** It renders QRIS (offline string edit) and confirms payments (own-account polling). No PJP, no acquiring, no fund flow. [S6]
|
||||
2. **[verified] Auth = GoID portal replay.** OTP or email+password against `/goid/login/request` + `/goid/token` with copied browser fingerprint headers; JWE bearer tokens, cached on disk and re-obtained on expiry. [S7][S10]
|
||||
3. **[verified] Payment confirmation = polling the merchant's own feed.** `GET api.gojekapi.com/merchant-analytics/v2/merchants/transactions` (primary) or `POST api.gobiz.co.id/journals/search` (fallback/detail), every ~6 s while a checkout is active; match amount + time; dedupe by transaction id. [S2][S6]
|
||||
4. **[verified] Anti-double-claim is a known hard problem** (two buyers paying the same nominal). Implemented: claim scoping by a unique `trx_id` + dedupe by `transaction_id`. Recommended additionally: add a unique Rp1–99 code to the billed amount. [S3][S6]
|
||||
5. **[verified] Reverse engineering was HAR-driven.** Four captured sessions are documented (up to 80 MB / 1063 entries each); replay clients were diffed against capture data header-by-header. Live-credential HARs are treated as crown jewels: gitignored, never committed, masked in reports. [S6][S7]
|
||||
6. **[verified] The APK analysis is a map of what's possible, not what's used.** ~250 endpoints were inventoried statically from GoPay Merchant 2.3.0 + GoFood Merchant 5.49.0; almost all are marked *unverified at runtime*. The subset actually in use is the HAR-verified portal API above. [S8][S12]
|
||||
7. **[reported] Version drift breaks clients regularly.** Documented fixes: phone normalisation (bare national digits only), dropping the `login_type` field, moving auth calls to the portal host, and repeated `X-AppVersion` bumps. Issue trackers show OTP delivery and empty-history reports — maintenance is ongoing. [S7][S13][S14]
|
||||
8. **[verified] Obfuscation sits exactly where it hurts.** The most popular gateway hides precisely the OTP login + session-refresh code (built with `javascript-obfuscator`), so what it does with the merchant's OTP, credentials, and tokens **cannot be audited from source**. [S3]
|
||||
|
||||
## Confidence labels used in this report
|
||||
|
||||
- **[verified]** — read directly in pinned source code, or corroborated by a documented live capture.
|
||||
- **[reported]** — asserted by a source (repo author, capture report); plausible and consistent, not independently confirmed by us.
|
||||
- **[unverified]** — explicitly flagged as untested/unconfirmed in the sources themselves.
|
||||
- **[inference]** — this report's own analysis, not a source claim.
|
||||
|
||||
## Files
|
||||
|
||||
- [[gopay-merchant-qris-gateway/analysis|Analysis — how the mechanism works]]
|
||||
- [[gopay-merchant-qris-gateway/reverse-engineering|Reverse engineering — methods & drift playbook]]
|
||||
- [[gopay-merchant-qris-gateway/repos|Repo audits & comparison]]
|
||||
- [[gopay-merchant-qris-gateway/security|Security, legal & operational risks]]
|
||||
- [[gopay-merchant-qris-gateway/sources|Sources & citations]]
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **[unverified] Unit-handling discrepancy across implementations:** `gobiz.js` and `QrisMerchantID` both divide `gross_amount` by 100 (minor units, i.e. sen), while the gateway's `verifyPayment()` compares the raw field against a rupiah input with no normalisation. Either it was validated against a different response shape, or its `/check-payment` never matches. Resolvable only with a live merchant account. Details in [[gopay-merchant-qris-gateway/analysis|analysis]] §4 (Money units).
|
||||
2. **[unverified] App-side protocol remains traffic-unverified:** the GoPay Merchant app's own API set (realtime WebSocket `raccoon` events, `qris-link` charge endpoints) is inventoried from static analysis only; a realtime (non-polling) watcher on the app protocol is the obvious next build for anyone with a device lab. [S8][S15]
|
||||
3. **[unverified] Request signing:** no static signing secret exists client-side and no signing was observed on the web path; whether app-side calls add per-request signatures is unknown. [S15]
|
||||
4. **[reported] Suspension triggers:** community threads tie account bans to aggressive polling and to transaction spikes; neither is confirmed against any GoPay statement. [S4][S14]
|
||||
94
gopay-merchant-qris-gateway/repos.md
Normal file
94
gopay-merchant-qris-gateway/repos.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
title: Repo audits & comparison
|
||||
description: Audit cards for gopay-api-gateaway, gobiz-payment, and QrisMerchantID at pinned commits — what each actually executes, key implementation facts, strengths, and the surrounding ecosystem map.
|
||||
status: concluded
|
||||
category: technical
|
||||
tags: [gopay, gobiz, repos, source-audit, comparison]
|
||||
draft: true
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# Repo audits & comparison
|
||||
|
||||
> Part of [[gopay-merchant-qris-gateway/index|GoPay Merchant QRIS gateways]]
|
||||
|
||||
All three repos read at pinned commits (2026-10-15); star counts captured via GitHub API the same day. Entry-point claims were checked against source, not READMEs alone.
|
||||
|
||||
## 1. `ahmadzakiyox/gopay-api-gateaway` — the turnkey service
|
||||
|
||||
**Pinned:** `c753b8c23daf5ced58febe650bcd229b6b2b7dec` (2026-07-26) · Node.js · MIT · 233 ★ / 84 forks · created 2026-07-21 · deploy guides for VPS/cPanel/Pterodactyl/Docker [S3][S5]
|
||||
|
||||
**What it is:** a self-hosted Express API that exposes a merchant's GoPay account as a mini payment gateway: create a dynamic QRIS for an amount, host a checkout page, verify whether that exact payment arrived, read transaction history. [S3]
|
||||
|
||||
**Audit findings (from source):**
|
||||
|
||||
- **[verified] Entry `server.js` is fully readable (736 lines) and contains the real logic:** EMVCo TLV parser + CRC16 (`calculateCRC16` L72–L85), `generateDynamicQRIS()` L88–L147 (also flips tag 01 11→12), REST endpoints L176–L732, hosted HTML checkout with 5-min countdown + manual check + optional 8-s auto-poll L255–L481, claim-scoped `verifyPayment()` L566–L653, in-RAM claim map with 24-h expiry L16–L53.
|
||||
- **[verified] `login.js` (425 KB) and `sessionManager.js` (464 KB) are obfuscated** (javascript-obfuscator-style hex string arrays; the `javascript-obfuscator` package is a devDependency; no readable endpoint strings leak). The OTP login and token refresh — **the credential-handling code** — cannot be source-audited. The 6-hourly refresh *scheduling* is visible in `server.js` L56–L69; the refresh mechanics are not.
|
||||
- **[verified] Amount-unit question:** `verifyPayment()` compares raw `gross_amount` against a rupiah input with no /100 normalisation — inconsistent with the other two repos' handling of the same feed (see [[gopay-merchant-qris-gateway/analysis|analysis]] §4). Unresolved.
|
||||
- **[verified] QR rendering is outsourced:** checkout page builds QR images via `https://api.qrserver.com` (L267, L272) — the merchant's QR payload (merchant ID + amount) is sent to a third-party image service. Avoidable.
|
||||
- **[verified] Claim state is in-process RAM** (Map) — restarts lose claims; no multi-instance story.
|
||||
- **[verified] README claims beyond code:** "auto-refresh token every 6 hours" [reported — schedule visible, mechanics obfuscated]; "session 100 % safe on your server" [reported — true by design only if the obfuscated code is honest].
|
||||
- **[verified] Anti-poll design at the UI level:** the checkout page defaults to a manual button; auto-polling is opt-in at 8 s, an explicit nod to ban-avoidance.
|
||||
|
||||
**Take:** the clearest end-to-end reference for the *service* shape (REST contract, claim scoping, checkout UX). **Don't take:** obfuscated credential handling, the RAM claim store, or the third-party QR renderer. The obfuscation makes this a poor base for anything security-sensitive despite being the most popular.
|
||||
|
||||
## 2. `kavionn/gobiz-payment` — the readable library
|
||||
|
||||
**Pinned:** `0bac129fefa09a3b723b68dd8f09e4b0e3899527` (2026-09-07) · Node.js ≥18 · MIT · 73 ★ / 29 forks · created 2026-06-15 [S2][S5]
|
||||
|
||||
**What it is:** an ES-module library — `GoPayMerchant` (login, merchant-ID discovery, history) + `GoPayWatcher` (EventEmitter polling watcher, singleton). The code reference the rest of the ecosystem (including QrisMerchantID) builds on. [S2]
|
||||
|
||||
**Audit findings (from source, 886 lines, fully readable):**
|
||||
|
||||
- **[verified] Auth:** OTP (added Sep 2026 via PR #6) and email+password; both replayed against `/goid/*`. Quirk: login calls are executed via `execFileSync('curl', ['-4', …])` instead of `fetch` — IPv4 forced, external binary dependency; rationale never explained in-code [unverified why — likely TLS/IPv6 quirks, unconfirmed].
|
||||
- **[verified] Token model is minimal:** access token only, cached in `.gopay_cache.json`; validity probed via `merchants/search`; on 401 it redoes a **full login** (no refresh-token exchange). Contrast with QMID's single-refresh flow.
|
||||
- **[verified] History:** analytics primary (L469–L518), journals fallback (L520–L629, with the lifted ES query DSL and special journal headers); amounts ÷100 for display.
|
||||
- **[verified] Watcher (L632–L748):** seed-then-diff, seen-set cap 500, `waitForPayment(amount, {timeout, tolerance})`, singleton via `getGoPayWatcher()` so the whole process shares one poller; poll interval default 6 s.
|
||||
- **[verified] Merchant-ID auto-discovery** from `merchants/search` hits (first hit), cached.
|
||||
- **[reported] Community known-issues:** issue #4 (empty history — maintainer: still works), #7 (OTP not arriving — open). Both consistent with drift/account-state, not code death.
|
||||
|
||||
**Take:** the cleanest implementation reference for the pattern; good naming, JSDoc, honest constraints. **Don't take:** no refresh-token path (full re-login on 401 = avoidable OTP traffic), curl subprocess for auth.
|
||||
|
||||
## 3. `AlfinAI/QrisMerchantID` — the research SDK & knowledge base
|
||||
|
||||
**Pinned:** `ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b` (2026-10-10) · Python ≥3.10 (one dep: httpx) · MIT (LICENSE file; GitHub classifier says "NOASSERTION") · 12 ★ / 2 forks · created 2026-09-10 [S1][S5]
|
||||
|
||||
**What it is:** a typed Python SDK for the same merchant APIs **plus** the best public documentation of the reverse-engineering process itself: APK analyses of both merchant apps, HAR-verification reports, a reproduction guide, endpoint/host inventories, conflict register, responsible-disclosure doc. Also ships a ShopeePay provider (out of scope here). [S1]
|
||||
|
||||
**Audit findings:**
|
||||
|
||||
- **[verified] Structure:** `src/qrismerchantid/{core,gopay,shopee}` — gopay = auth, client, constants, merchants, money, payouts, qris, transactions, users, watcher; core = transport, token_cache, exceptions. Facade `GoPayMerchant` with per-service namespaces.
|
||||
- **[verified] Capture-grade fidelity:** constant defaults mirror the portal exactly (headers, `platform-v3.125.0-e1923971`, status/payment-type lists, journal headers); auth shapes annotated to specific capture sections (§9.1/§9.4/§9.5); phone normalisation to bare national; `login_type` omitted to match browser.
|
||||
- **[verified] Money handling:** raw minor units + `to_rupiah()`; watcher matches on **minor units**; docs call out the two providers' unit mismatch explicitly.
|
||||
- **[verified] QRIS helper is pure/offline and conservative** — injects tag 54, recomputes CRC16, **deliberately leaves tag 01 alone**, and warns a valid CRC ≠ guaranteed acceptance. [S10 docs/gopay/qris.md]
|
||||
- **[verified] Robustness engineering:** typed `ApiException` (http_status/code/payload), transport-only retries with backoff, single refresh attempt then fail-fast, 500-entry seen cache, offline test suite + ruff + mypy strict gates (as documented).
|
||||
- **[verified] Process discipline visible in-repo:** version-drift fixes documented as reports (OTP regression, email flow verification), TODO register with priorities/owners, conflict register, masking rules, disclosure doc, full credits to prior repos. Honest throughout about what is NOT verified (live login from SDK, request signing, app flows).
|
||||
- **[reported] "100 % offline test suite" / "190 passed"** — stated in reports; we didn't run it (out of audit scope).
|
||||
|
||||
**Take:** the model for how to document this class of work (and a genuinely usable Python client). **Don't take:** nothing critical — main caveats are the young project age and that live-verification TODOs remain open.
|
||||
|
||||
## 4. Side by side
|
||||
|
||||
| | gopay-api-gateaway | gobiz-payment | QrisMerchantID |
|
||||
|---|---|---|---|
|
||||
| Shape | Express service (REST + checkout page) | Library (embed in bot/backend) | SDK + research KB (+ ShopeePay) |
|
||||
| Language | Node.js | Node.js ≥18 | Python ≥3.10 |
|
||||
| Auth | OTP (CLI); obfuscated internals | OTP + email/password (readable) | OTP + email/password (readable, capture-verified) |
|
||||
| Token model | 6-h refresh cycle + 401 refresh [reported/partly verified] | re-login on 401 (no refresh) | single refresh attempt, then re-login |
|
||||
| Payment feed | analytics (no journal fallback) | analytics → journal fallback | analytics + journals + aggregations + payouts |
|
||||
| QRIS approach | tag 54 + tag 01 flip + CRC16 | (QRIS gen via demo/portal string) | tag 54 + CRC16, tag 01 untouched |
|
||||
| Dedupe/claims | trx_id claim map (RAM, 24 h) | seen-set (500) | seen-set (500) + docs recommend Rp1–99 amount codes |
|
||||
| Amount units | raw (discrepancy — see §1) | ÷100 | raw minor + `to_rupiah()` |
|
||||
| Readability | partial (auth obfuscated) | full | full |
|
||||
| Tests | none visible | none visible | offline suite + lint/type gates [reported] |
|
||||
| Docs | deploy-heavy README | README + mermaid flows | full docs/ tree + research/ + reference/ CSVs |
|
||||
| License | MIT | MIT | MIT |
|
||||
|
||||
## 5. Ecosystem map (context — not audited here)
|
||||
|
||||
The QMID research doc maps the wider field [S6 §1]; treat those entries as [reported], not verified by this audit: `warungerik/API-GOPAY-MERCHANT` (Vue/Nuxt, "user-tested", HMAC webhook), `namtxs/gopay-api` (PHP, **customer-side** wallet flow — different scope), `alhifnywahid/merchantid` (TS, ShopeePay full programmatic login + GoPay provider), `IbraDecode/gopaymerchant`, `ardianryan/qbiz-qrisdinamis`, `xirf/gobiz-merchant-sdk`, `azmi2409/gopay-merchant-qris-gateway`, plus `ahmadzakiyox/shoppepay-api-gateway` (same author; server fully obfuscated). The pattern reproduced across ≥8 independent repos within ~4 months — the clearest signal this is a real market need, not one person's project. [S6]
|
||||
|
||||
## 6. One cross-cutting observation
|
||||
|
||||
[inference] The three repos agree on mechanics (auth replay, TLV+CRC16, seed-poll-match) but each differs in an important engineering choice (refresh handling, unit conversion, tag-01 handling, claim store). None of them is a canonical reference for all of it — the audit found at least one substantive inconsistency per repo (gateway: units + RAM claims; kavionn: no refresh path; QMID: young project, live-login TODOs open). Anyone touching this space should treat all three as **complementary evidence**, and the capture reports [S6][S7] as the closest thing to ground truth.
|
||||
103
gopay-merchant-qris-gateway/reverse-engineering.md
Normal file
103
gopay-merchant-qris-gateway/reverse-engineering.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
title: Reverse engineering — methods & drift playbook
|
||||
description: 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.
|
||||
status: concluded
|
||||
category: technical
|
||||
tags: [reverse-engineering, har-capture, apk-analysis, gobiz, gopay, methodology]
|
||||
draft: true
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# Reverse engineering — methods & drift playbook
|
||||
|
||||
> Part of [[gopay-merchant-qris-gateway/index|GoPay Merchant QRIS gateways]]
|
||||
|
||||
## 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 `.gitignore`d 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]
|
||||
74
gopay-merchant-qris-gateway/security.md
Normal file
74
gopay-merchant-qris-gateway/security.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
title: Security, legal & operational risks
|
||||
description: Compliance frame (ToS, PJP/BI licensing, gray zones), account-suspension evidence, credential-handling audit across the three repos, operational failure modes, and practical hygiene for anyone touching this space.
|
||||
status: concluded
|
||||
category: technical
|
||||
tags: [gopay, gobiz, risk, tos, compliance, credentials, security]
|
||||
draft: true
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# Security, legal & operational risks
|
||||
|
||||
> Part of [[gopay-merchant-qris-gateway/index|GoPay Merchant QRIS gateways]]
|
||||
|
||||
## 1. The compliance frame
|
||||
|
||||
- **What these tools do is not itself payment processing.** They automate a merchant's own account: render a QR from their own static QRIS, and read their own transaction feed. Funds settle through normal QRIS rails to the merchant's own balance. No third party touches money. [verified — mechanism, see [[gopay-merchant-qris-gateway/analysis|analysis]]]
|
||||
- **But it is not a sanctioned integration either.** All three repos are explicit: unofficial, not affiliated with GoTo/GoPay/GoBiz, "may violate providers' Terms of Service". Automated access to the private portal API — replaying a browser session with a spoofed fingerprint — is the kind of use ToS clauses exist for. [verified — repo disclaimers S1][S2][S3; S11]
|
||||
- **Regulated edges [inference, legal framing]:** In Indonesia, QRIS acceptance is Bank Indonesia-regulated, and *providing payment services to third parties* requires a PJP license. A merchant automating their own acceptance is a gray zone of *consumer/micro-merchant self-service*; pointing the same mechanism at **other people's transactions** (aggregating sub-merchants through one account) would clearly cross into regulated activity. Nothing in the audited code prevents that misuse, and nothing in the repos encourages it.
|
||||
- **Data-protection angle:** session tokens are account credentials; a leaked session exposes transaction data, balances, and (per the app-side endpoint map) payout surfaces. Handling rules therefore match credential handling — see §3.
|
||||
|
||||
## 2. Account-action risk (what actually breaks accounts)
|
||||
|
||||
Evidence from the ecosystem's own comms [reported — no GoPay statement available]:
|
||||
|
||||
- Maintainer of `gobiz-payment`, in issue #3, on suspensions: users report suspension/limitation correlated both with **aggressive polling** and with **rapid transaction growth** ("masa usaha mulai laris malah di suspend karena alasan terjadi peningkatan transaksi" — account suspended for a transaction spike). [S4]
|
||||
- Community-adopted mitigation, endorsed in that thread and implemented by QMID: **poll only while a checkout is active** ("on-demand poller" → idle traffic 0), keep the ~6 s interval, one poller per account, no parallel watchers. [S4][S6 §2.3]
|
||||
- Observed server-side rate limit on the login/OTP request: 10/min. [S7]
|
||||
- Open field reports: OTP not arriving (#7), empty history (#4) — both likelier account-state/drift than protocol death. [S14]
|
||||
- Aggravating factor [inference]: all three designs funnel an entire business through **one** merchant session; any account action stalls all automation, and every re-login re-sends OTPs (higher exposure) if refresh tokens aren't used.
|
||||
|
||||
## 3. Credential-handling audit (repo by repo)
|
||||
|
||||
| Repo | What it stores / collects | Audit verdict |
|
||||
|---|---|---|
|
||||
| `gopay-api-gateaway` | Phone number + OTP entered at CLI; session written to `.GOPAY_SESI_JANGAN_DIHAPUS.json`; tokens used by `server.js` | **Cannot be audited**: `login.js` + `sessionManager.js` are obfuscated (no readable strings). Self-hosted, so credentials stay on your server *if the obfuscated code is honest* — that "if" is the entire problem. Also: checkout page sends the QR payload to third-party `api.qrserver.com`. [S3] |
|
||||
| `gobiz-payment` | Phone+OTP or email+password from `.env`; tokens in `.gopay_cache.json` | Readable; auth calls run through the system `curl` binary (odd but transparent — no hidden network calls). Tokens stored **plaintext on disk** (README says gitignore). [S2] |
|
||||
| `QrisMerchantID` | Same model, plus HAR files kept outside the repo | Readable; explicit hygiene rules: HARs contain live credentials → gitignored, never committed, values masked in reports ("4 leading + 4 trailing chars"); token cache API with documented save/load. [S7][S11] |
|
||||
|
||||
**[verified] Common pattern:** all three store session tokens in a plaintext file next to the app and remind users to gitignore it. No encryption at rest anywhere; no keyring. Treat every such file as a live credential.
|
||||
|
||||
**The obfuscation red flag, stated precisely:** the most popular repo hides exactly its credential-handling code (see [[gopay-merchant-qris-gateway/repos|repos]] §1). This is not an accusation of malice — but for a tool that asks for an SMS OTP with full account access, "trust us, it's obfuscated" inverts the property an auditable client should have. The README's "your data stays 100 % on your own server" is only verifiable modulo the hidden code. [inference]
|
||||
|
||||
## 4. Operational failure modes (verified in code / reports)
|
||||
|
||||
- **Drift kills silently** — version-gated endpoints can degrade into unhelpful errors; the literature's whole drift playbook exists because clients break when the portal moves. [S7][S13]
|
||||
- **Claim state in RAM** (`gopay-api-gateway`) — restart within 24 h → previously claimed transactions can be claimable again; multi-instance compounds it. [S3]
|
||||
- **Refresh fragility** — expired refresh token → forced re-login → OTP cycle; QMID disables polling on rejection (fail-fast), kavionn has no refresh path at all. [S10][S2]
|
||||
- **Third-party dependency in the hot path** — QR images rendered via an external service in the gateway; offline/blocked → checkout breaks. [S3]
|
||||
- **No delivery guarantees** — confirmation is pull-based (store polls the tool; the tool polls GoPay). If the store backend misses the confirmation window, orders lapse with money received; no retry/alarm semantics beyond the watcher timeout. [inference from architecture]
|
||||
- **Unit-scale bugs are lurking** — see the cross-repo discrepancy in [[gopay-merchant-qris-gateway/analysis|analysis]] §4: a 100× mismatch class of bug is easy to ship and hard to notice in tests (both 25000 vs 2500000 look like plausible amounts).
|
||||
|
||||
## 5. Who this is exposed to (risk surface, neutral)
|
||||
|
||||
- **The merchant running it:** account suspension/ToS action (real evidence above); credential custody (obfuscated clients; plaintext caches); business continuity on a single session.
|
||||
- **Buyers:** none directly — no funds route through the tool; QRIS is a push payment via the buyer's app; the checkout page exposes no buyer data beyond what the buyer types nowhere (page is static + polling). [inference from code]
|
||||
- **The payment ecosystem:** the load pattern (replayed portal sessions, spoofed browser fingerprints) imposes automation on a consumer-facing control plane; at scale this is indistinguishable from credential-stuffing/scapegoat traffic for anti-fraud systems, and may drive friction for legitimate users. [inference]
|
||||
- **Misuse vector to note:** stolen GoPay merchant session tokens become immediately useful (read balances, explore payout endpoints documented in the APK map). The tooling itself adds no new auth weaknesses — it *is* the demonstration that web-session tokens are the crown jewels of this ecosystem. [S8][inference]
|
||||
|
||||
## 6. Disposition of the research itself
|
||||
|
||||
The QrisMerchantID project published a **responsible-disclosure doc** covering its APK findings (static indicators only, 0 critical, secret masking, no re-distribution of APKs, vendor contact channel, 7-day correction policy) and holds `0 critical` as its verdict. [verified — S11]. Its stated non-negotiables — no backend probing, no credential tests, no publishing raw secrets, no "vulnerability" claims from static indicators — match what this audit observed in the artifacts. [S9 §5]
|
||||
|
||||
## 7. Practical hygiene (if you touch this space)
|
||||
|
||||
1. **Read the code first** — prefer readable implementations; treat obfuscated auth code as unauditable and avoid it for real credentials.
|
||||
2. **Own account only** — the entire method is "your account, your traffic". Anything else is account takeover territory.
|
||||
3. **Sessions are crown jewels** — plaintext caches + `.env` + HAR files: gitignore, exclude from backups/syncs, rotate after any exposure.
|
||||
4. **Poll like a human would** — only during active checkouts, ~6 s floor, one poller per account.
|
||||
5. **Expect breakage as a feature of the category** — budget for drift maintenance; don't build revenue-critical flows without a fallback (manual reconciliation is the fallback these tools automate away).
|
||||
6. **For production/compliance-grade needs, the official route is a licensed PJP gateway** (Midtrans/Xendit/DOKU etc.) — that's the option these tools exist to avoid, and their entire risk profile is the price difference.
|
||||
|
||||
Both the platforms' ToS and BI's licensing regime are the boundary of what *should* be built here; this vault records how the ecosystem works, not an endorsement of deploying it. [inference]
|
||||
105
gopay-merchant-qris-gateway/sources.md
Normal file
105
gopay-merchant-qris-gateway/sources.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
title: Sources — GoPay Merchant QRIS gateways
|
||||
description: Citation list for the merchant-gateway audit — pinned repos, capture reports, APK analyses, issue threads — with what each source evidenced and where each claim was verified.
|
||||
status: concluded
|
||||
category: technical
|
||||
tags: [gopay, gobiz, sources, citations]
|
||||
draft: true
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# Sources — GoPay Merchant QRIS gateways
|
||||
|
||||
> Part of [[gopay-merchant-qris-gateway/index|GoPay Merchant QRIS gateways]]
|
||||
|
||||
**Audit provenance:** all three repos were cloned shallowly on **2026-10-15** and read at the commits below (`git log -1`); star/fork/license metadata captured the same day via GitHub API (`gh api repos/<owner>/<repo>`). Issue threads fetched via GitHub API 2026-10-15. **No calls were made to any GoPay/GoBiz endpoint, no credentials were used, no traffic was captured by us** — everything about live behavior is quoted from the sources' own documented captures. Repo URLs below are pinned to the audited commit SHA.
|
||||
|
||||
## S1 — `AlfinAI/QrisMerchantID` (Python SDK + research knowledge base)
|
||||
|
||||
- Repo (pinned): `https://github.com/AlfinAI/QrisMerchantID/tree/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b` — commit `ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b` (main, 2026-10-10). MIT (LICENSE file); GitHub classifier reports "NOASSERTION". 12 ★ / 2 forks (2026-10-15).
|
||||
- Evidenced: the complete audit trail of the reverse-engineering work, capture-verified API shapes, the SDK whose code corroborates the mechanics (§ refs below), disclaimers and credits.
|
||||
|
||||
## S2 — `kavionn/gobiz-payment` (Node.js library)
|
||||
|
||||
- Repo (pinned): `https://github.com/kavionn/gobiz-payment/tree/0bac129fefa09a3b723b68dd8f09e4b0e3899527` — commit `0bac129fefa09a3b723b68dd8f09e4b0e3899527` (main, 2026-09-07). MIT. 73 ★ / 29 forks (2026-10-15).
|
||||
- Core file read in full: [`gobiz.js`](https://github.com/kavionn/gobiz-payment/blob/0bac129fefa09a3b723b68dd8f09e4b0e3899527/gobiz.js) — key anchors:
|
||||
- device header set: L71–L100 · password login: L102–L149 · OTP login: L158–L254
|
||||
- token validity probe: L292–L304 · merchant discovery: L256–L272
|
||||
- `getHistory` (analytics→journal fallback): L399–L467 · analytics: L469–L518 · journals: L520–L629
|
||||
- watcher: L632–L748 · singleton factory: L750–L763 · minor-unit ÷100: L407, L435, L678–L681
|
||||
- Evidenced: readable reference implementation of auth replay, feed normalization, watcher pattern; the §4 unit-handling comparison; curl-subprocess quirk (L107/124/174/226).
|
||||
|
||||
## S3 — `ahmadzakiyox/gopay-api-gateaway` (Node.js service)
|
||||
|
||||
- Repo (pinned): `https://github.com/ahmadzakiyox/gopay-api-gateaway/tree/c753b8c23daf5ced58febe650bcd229b6b2b7dec` — commit `c753b8c23daf5ced58febe650bcd229b6b2b7dec` (main, 2026-07-26). MIT. 233 ★ / 84 forks (2026-10-15).
|
||||
- Core file read in full: [`server.js`](https://github.com/ahmadzakiyox/gopay-api-gateaway/blob/c753b8c23daf5ced58febe650bcd229b6b2b7dec/server.js) — key anchors:
|
||||
- claim map + 24-h cleanup: L16–L53 · 6-h refresh cycle: L56–L69 · CRC16: L72–L85
|
||||
- `generateDynamicQRIS` (tag 54 + tag 01→12 + CRC): L88–L147 · token-status probe: L176–L204
|
||||
- `/create-qris`: L207–L252 · checkout page (5-min countdown, manual check, 8-s opt-in poll): L255–L481
|
||||
- `verifyPayment` (claim scoping; no unit conversion — see analysis §4): L566–L653 · public status: L656–L686 · `/check-payment`: L690–L727
|
||||
- Obfuscation evidence: `login.js` (425,457 B) and `sessionManager.js` (463,886 B) are javascript-obfuscator output (hex string-array, no readable endpoints; `javascript-obfuscator` in devDependencies); `package.json` checked for dependency list.
|
||||
- Evidenced: service architecture, claim-scoping design, RAM claim store, obfuscation red flag, third-party QR rendering (`api.qrserver.com` at L267/L272).
|
||||
|
||||
## S4 — `kavionn/gobiz-payment` issue #3: "Gopay Merchant app not Gobiz? and what are the risks?"
|
||||
|
||||
- Thread: `https://github.com/kavionn/gobiz-payment/issues/3` (open; fetched 2026-10-15).
|
||||
- Evidenced [reported]: maintainer statement that GoPay Merchant transactions are visible on the GoBiz web for the same account ("ambil datanya dari web gobiz"); community suspension observations ("kena suspend / peningkatan transaksi"); the on-demand-poller suggestion and QMID author's participation.
|
||||
|
||||
## S5 — GitHub API metadata (captured 2026-10-15)
|
||||
|
||||
- `gh api repos/ahmadzakiyox/gopay-api-gateaway`, `…/kavionn/gobiz-payment`, `…/AlfinAI/QrisMerchantID` — stars, forks, license, created/pushed dates quoted in the repo cards.
|
||||
|
||||
## S6 — QMID `research/RESEARCH_GOPAY_SHOPEEPAY.md` (flagship research doc, 2026-09-11, updated through §9.5)
|
||||
|
||||
- Pinned: `.../blob/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/research/RESEARCH_GOPAY_SHOPEEPAY.md`
|
||||
- Evidenced: ecosystem map (8+ repos); GoPay auth endpoint table (§2.1); data endpoints (§2.2); watcher pattern (§2.3); QRIS + anti-double-claim recipe (§2.4); gateway architecture pattern (§5); HAR verification results §9.1 (body/param shapes, 23-key tx object, minor units), §9.2 (corrections: `login_type`, `X-AppVersion` bump, split headers, journal headers), §9.3 (new endpoints: payouts, payable_detail, merchant detail), §9.4 (second capture, drift), §9.5 (email login verified live).
|
||||
|
||||
## S7 — QMID `research/REPORT_GOBIZ_OTP_2026-09-19.md` (OTP regression analysis, HAR-based)
|
||||
|
||||
- Pinned: `.../blob/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/research/REPORT_GOBIZ_OTP_2026-09-19.md`
|
||||
- Evidenced: capture provenance (`GOTO.har`, 51 MB, 589 entries, Firefox); OTP request/response shapes + headers + rate limit (§3.1); token exchange + JWE sizes (§3.2); split-header finding (§3.3); root-cause ranking incl. phone-format + `X-AppVersion` gating (§4); credential-hygiene rules (preamble §5); email login second capture (§8); "mirror the browser, byte-for-byte" principle (§5).
|
||||
|
||||
## S8 — QMID `research/APK_GOPAY_MERCHANT_2.3.0_ANALYSIS.md` (APK static analysis)
|
||||
|
||||
- Pinned: `.../blob/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/research/APK_GOPAY_MERCHANT_2.3.0_ANALYSIS.md`
|
||||
- Evidenced: APK identity/hashes (§1); permissions (§2); exported components (§3); anti-tamper + crypto inventory (§4); **host inventory §5.1 + ~150 endpoint groups §5.2** (incl. `unified-histories`, `transactions/charge/qris-link`, `qris/shared`, raccoon WebSocket); §5.4 sister-app evidence (GoBiz relation, answering kavionn#3); honest static-only limits (§7, §12 reproducibility log).
|
||||
|
||||
## S9 — QMID `docs/APK_RESEARCH_REPRODUCTION_GUIDE.md` (how the APK research was done)
|
||||
|
||||
- Pinned: `.../blob/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/docs/APK_RESEARCH_REPRODUCTION_GUIDE.md`
|
||||
- Evidenced: full repro pipeline (tooling, download patterns, hashing, apktool/androguard steps §3.1–§3.11); masking rule ("4 leading + 4 trailing chars + length" §3.9); secret-scan commands; ethics boundaries (§5).
|
||||
|
||||
## S10 — QMID GoPay client docs + source (the capture-annotated SDK)
|
||||
|
||||
- Docs pinned: `.../tree/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/docs/gopay/` (`README.md`, `auth.md`, `qris.md`, `watcher.md`, `transactions.md`, `merchants.md`, `payouts.md`).
|
||||
- Source pinned: `.../tree/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/src/qrismerchantid/gopay/` — read: `constants.py` (hosts, `platform-v3.125.0-e1923971`, journal headers), `auth.py` (OTP/password/refresh flows L56–L103, L105–L125), `qris.py` (TLV + CRC16 + `inject_amount` L51–L73), `watcher.py` (seed/poll/match L47–L82), `money.py` (minor units).
|
||||
- Evidenced: the SDK-side mechanics and defaults quoted in [[gopay-merchant-qris-gateway/analysis|analysis]] §3–§7; the tag-01 conservatism and its rationale (docs/gopay/qris.md); the anti-double-claim guidance.
|
||||
|
||||
## S11 — QMID `docs/SECURITY_DISCLOSURE.md` (responsible disclosure)
|
||||
|
||||
- Pinned: `.../blob/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/docs/SECURITY_DISCLOSURE.md`
|
||||
- Evidenced: static-only method, 0-critical classification, masking/no-redistribution commitments, vendor contact + 7-day correction policy.
|
||||
|
||||
## S12 — QMID `reference/endpoints.csv` + `reference/hosts.csv`
|
||||
|
||||
- Pinned: `.../tree/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/reference/`
|
||||
- Evidenced: 249-row endpoint inventory — **all rows marked `BELUM TERVERIFIKASI` for runtime use** (static strings only); host inventory incl. staging hosts, `gopaymerchant.midtrans.com`, raccoon wss.
|
||||
|
||||
## S13 — QMID `docs/ISSUE-1-RESPONSE.md` (Oct 2026 drift fix)
|
||||
|
||||
- Pinned: `.../blob/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/docs/ISSUE-1-RESPONSE.md`
|
||||
- Evidenced: `X-AppVersion` v3.122→**v3.125** bump; auth calls moved to `portal.gofoodmerchant.co.id` while data stays on `api.gobiz.co.id`; OTP length read from response.
|
||||
|
||||
## S14 — `kavionn/gobiz-payment` issues #4, #7
|
||||
|
||||
- `https://github.com/kavionn/gobiz-payment/issues/4` (closed) — empty-history field report + maintainer response ("masih bisa"); `https://github.com/kavionn/gobiz-payment/issues/7` (open) — OTP not arriving. Fetched 2026-10-15. Evidenced [reported]: real-world fragility profile.
|
||||
|
||||
## S15 — QMID `docs/KB_QRIS_MERCHANT_INDEX.md` + `reference/MANIFEST.md`
|
||||
|
||||
- Pinned: `.../blob/ac1f2ba3c1d82910bc8a81e9f0b98d10c0ead36b/docs/KB_QRIS_MERCHANT_INDEX.md` and `.../blob/.../reference/MANIFEST.md`
|
||||
- Evidenced: sister-app relation map; priority endpoint list for future work (raccoon wss, unified-histories); open research questions (signing, cross-app tokens, `/v1/payments/search` scope); artifact hashes + honest note that APK↔hash re-verification is pending (APKs deleted post-analysis).
|
||||
|
||||
## Referenced but not audited (reported by S6 only)
|
||||
|
||||
`warungerik/API-GOPAY-MERCHANT` (user-tested per S6, not independently verified), `namtxs/gopay-api` (customer-side, 2024, possibly stale), `alhifnywahid/merchantid`, `IbraDecode/gopaymerchant`, `ardianryan/qbiz-qrisdinamis`, `xirf/gobiz-merchant-sdk`, `azmi2409/gopay-merchant-qris-gateway`, `ahmadzakiyox/shoppepay-api-gateway`. Treat as pointers for future audits, not evidence.
|
||||
Loading…
Add table
Add a link
Reference in a new issue