Skip to content

fix(otp): restore TW password retrieval on the legacy launcher (#368) (#376) - #380

Draft
hiimyusheng wants to merge 2 commits into
pungin:old-version-5.9.1from
hiimyusheng:fix/368-otp-legacy
Draft

fix(otp): restore TW password retrieval on the legacy launcher (#368) (#376)#380
hiimyusheng wants to merge 2 commits into
pungin:old-version-5.9.1from
hiimyusheng:fix/368-otp-legacy

Conversation

@hiimyusheng

Copy link
Copy Markdown
Contributor

Ports the #368 / #376 fix to the legacy WPF launcher. Without it, 5.9.1 fails every 獲取密碼 on TW with Query String Error.

Base branch is old-version-5.9.1, not code. This branch has no equivalent of the Tauri client's code, so nothing here is a cherry-pick — it is a re-implementation of the same protocol change in C#.

What changed on beanfun

Same as #372 and #377, in short: game_start_step2.aspx no longer builds an OTP URL. It emits an m_objData handoff for the Gamania Games Manager, and the obfuscated data blob inside carries whatever that game's own OTP request needs. The full write-up lives in those two PRs; only the parts that shaped this port are repeated below.

What was ported

1. The launch-data decoderBeanfun/API/LaunchData.cs (new)

Eight substitution tables, an 8-character ASCII DES key lifted out of the normalized hex, then DES-ECB with no padding. Steps 3–5 are the same construction the pre-v2 OTP envelope used, so the existing WCDESComp.DecryStrHex handles them unchanged; only the substitution layer and the field parse are new.

Which table the selector names is not settled — n % 4 decodes every sample seen so far, but there are eight tables and one sample cannot tell that apart from the table order simply differing. So each table is tried until one yields a plaintext carrying a known marker. A wrong table gives noise, and noise does not spell a field name by accident.

2. Two payloads, two endpoints — the #376 half

m_objData.data carries either a LaunchTicket for get_webstart_otp_v2.ashx or the pre-v2 ppppp parameters for get_webstart_otp.ashx, and both are live. Both kinds of page declare m_objData and are indistinguishable from outside, so the blob has to be decoded before the route is known. Accepting a decode only when it held a LaunchTicket would make a correct decode of the second kind look like a wrong table — which is how every game but MapleStory ended in a decryption error for data that had decrypted perfectly.

ppppp is the value that matters most here: this client pinned a 64-character constant whose provenance nobody knew, and the live one arrives in the blob at 96 characters. webToken and secretCode are taken from the page when it declares them, and fall back to the session and to get_cookies.ashx when it does not.

3. The three things that move with the endpointBeanfun/Tools/ClientIntegrity.cs (new)

  • Handlers under generic_handlers began checking Referer, and answer a complaint about a null referrer without one. Every such request now sends the game-start page's own URL.
  • The request has to name which launcher build is asking: CV, Hash, arch. Read from the installed Game Manager when there is one — located via the gamaniagames:// protocol handler, then the conventional install roots — and from compiled-in constants otherwise, which is the case for most users of this app.
  • The page's GetResultByLongPolling call is the launcher's installation check, unrelated to the password and holding the connection open, so the handoff path does not wait on it. record_service_start is best-effort there for the same reason.

Those values also resolve without a release, same order as #374: a pin the user set, then the newer of the installed Game Manager and the published ggm-client.json cached for six hours, then the compiled-in pair. This branch reads the same ggm-client.json from code, so a hotfix published there reaches these users too.

Preferring the installed copy outright looked right, since the Game Manager self-updates — but it self-updates when it runs, and the people this app exists for are the ones who never run it. An install untouched since Gamania's last release reports exactly the values beanfun stops accepting, so the machines most likely to be refused would be the only ones the lever could never reach. Comparing versions and letting the newer build win, with a tie going to the installed file, avoids that.

Every layer is best-effort and falls through on failure. One addition over the Rust version: a run that reaches no mirror stops retrying for the rest of the process, so someone behind a block that reaches none of them does not pay four timeouts on every password.

Routing reads the handoff first. A migrated page has dropped the literals the old flow read — there is no GetResultByLongPolling&key= on it any more. Demanding them up front would fail the retrieval before the handoff in the same page had been read, naming a literal that is meant to be gone.

The route is chosen by the page's own shape rather than by region, so a page carrying the handoff takes the new path and everything else keeps the old one. HK needs no change now, and none later if it migrates.

Verification

Built on Windows with .NET 8 and run against the live service. Retrieval succeeds on 新楓之谷, 新龍之谷, 瑪奇, CSO and 艾爾之光.

Both payload kinds are covered by that set, per the characterisation on #376 — which is the point of testing more than MapleStory.

Before that, the decoder was checked by round-tripping both payload shapes through all 16 selectors, plus the malformed-input and block-alignment cases, and confirmed to fail with the #376 acceptance test reverted.

Not tested: HK. No account to try it with. Its page carries no m_objData, so it takes the unchanged branch — but that is reasoning, not a measurement.

This branch has no test project, so the round-trip check was a scratch script rather than something committed; the live run above is the real evidence.

Notes

  • The second commit swaps this file's log4net calls for Console. This branch references log4net but never configures an appender, so those lines wrote to nothing — silent exactly when someone is trying to find out which route ran and which CV was sent. It also prints the chosen route once per retrieval.
  • No new i18n keys: every failure reuses an existing errmsg string.

Decoding algorithm published by @takidog on #368. The four-title characterisation of the two payloads by @ToooAir on #376.

…n#368) (pungin#376)

beanfun moved TW game starts to the Gamania Games Manager in August 2026 and
the credential flow moved with it, so 5.9.1 fails every 獲取密碼 with
「Query String Error」. This ports the fix from the current branch.

The game-start page no longer builds an OTP URL; it emits an `m_objData`
handoff for the native launcher, whose `data` blob decodes locally into
whatever that game's own OTP request needs. The route is chosen by the
page's shape rather than by region, so a page carrying the handoff takes the
new path and everything else — HK included — keeps the old one.

The blob carries two payloads and both are live: MapleStory hands over a
`LaunchTicket` for `get_webstart_otp_v2.ashx`, while Mabinogi, Elsword, CSO
and others hand over the pre-v2 `ppppp` parameters for the endpoint that was
there all along. Accepting a decode only when it held a `LaunchTicket` made a
correct decode of the second kind look like a wrong table, which is why every
game but MapleStory failed with a decryption error for data that had
decrypted perfectly. `ppppp` matters most: the 64-character constant this
client pinned is stale, and the live 96-character value arrives in the blob.

Three things move together with the endpoint. Handlers under
`generic_handlers` began checking `Referer` and answer a complaint about a
null referrer without one. The request has to name which launcher build is
asking — a version and a file hash — read from the installed manager when
there is one and from compiled-in constants otherwise. And the page's
`GetResultByLongPolling` call is the launcher's installation check, unrelated
to the password and holding the connection open, so the new path skips it.

Those values are also resolvable without a release: a pin the user set, then
the newer of the installed GGM and the published `ggm-client.json` cached for
six hours, then the compiled-in pair. Preferring the installed copy outright
looked right since GGM self-updates — but it self-updates when it runs, and
the people this app exists for never run it, so the stalest machines would be
the only ones a hotfix could never reach. Every layer is best-effort: a
network hiccup must not cost someone their password when a good compiled-in
pair is right there, and a run that reaches no mirror stops retrying.

A migrated page has dropped the literals the old flow read, so step 1 reads
the handoff first — it decides the route — and only then demands what that
route needs.

Decoding algorithm published by @takidog on pungin#368; the four-title
characterisation of the two payloads by @ToooAir on pungin#376.

Ref pungin#368
Ref pungin#376
This branch references log4net but never configures an appender, so every
`log.Info` here wrote to nothing — silent precisely when someone is trying to
find out which route ran and which CV was sent. Console, like the rest of
this file, shows up when the app is started from a terminal.

Also prints the chosen route once per retrieval, which is the first thing
worth knowing when a game fails and another one works.

Ref pungin#368
@hiimyusheng
hiimyusheng marked this pull request as ready for review August 18, 2026 04:04
@lshw54
lshw54 self-requested a review August 21, 2026 06:55
@lshw54 lshw54 added the enhancement New feature or request label Aug 21, 2026
@lshw54

lshw54 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

關於小型UI 的更新後續會跟據B151 進行調整, 這個pr會先暫作保留
https://forum.gamer.com.tw/Co.php?bsn=7650&sn=6517398

@lshw54
lshw54 marked this pull request as draft August 21, 2026 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants