My gym forced a social media down my throat. So I reverse-engineered its app

Hacker News by 5 min read 292x views
My gym forced a social media down my throat. So I reverse-engineered its app

Share Post

I lately joined a new gym. It’s a beautiful fine gym, alongside nice equipment, nice management, nice branding.

And an app.

Now, it’s not several random shitty vibe-coded app. It’s really beautiful fine as a product. It has beautiful slick UI/UX and a well-thought-out, accordant scheme language. I established lone one bug in it.

For me, though, the lone use case is to display a QR code at the turnstile for checking in and out.

However, there is a problem. The residence display of the app is a social feed. Once you inspect out, it prompts you to article a narrative of your session. The residence nourish is complete of people’s gym selfies. Every period I open the app, I’m forced to see item that basically looks akin Tinder.

Now, my companion thinks this is a brilliant endeavor idea. According to her, group who activity out really desire to flaunt their bodies, but posting that on normal social media is frowned upon. So this gym business really established a fine product-market fit.

I think she’s right.

However, I’m excellent assured concerning social media and feeds I don’t control. Any app that shows me a nourish I can’t rotate off, sort, or filter—I obtain that as mental war against me.

I haven’t used social media in additional than 12 years. I’m excellent adamant concerning blocking ads, blocking YouTube comments, Shorts, and suggested videos, and sanitizing YouTube titles and thumbnails. For things I must use, akin LinkedIn1, I use browser extensions to obstacle the news nourish completely.

So I took it personally.

I wanted to see if I could extract the QR-code logic without all the additional bloat of the app.

So I started digging. I quickly established out that the code changes all 15 seconds. Screenshots are additionally blocked on that screen, accordant alongside Android’s FLAG_SECURE. I wondered if it was several benevolent of TOTP. Was the generation happening server-side?

I did the simplest thing: turned off Wi-Fi. The app immediately told me the codes would remain valid for N days without reconnecting2.

Okay, so it could create them locally.

I used GLM 5.3 and DeepSeek v4.1 Flash. In my sessions, GLM was burning item akin 10–20 times as much in token disbursal without making that much progress. DeepSeek seemed to move much faster.

To be clear, I’m leaving out several particulars to keep the gym and myself anonymous. I don’t desire to endure my gym subscription. I really akin that gym.

The app was written in Flutter, alongside most of the logic compiled into Dart native code. We used blutter to inspect that, jadx for the Android-side code, and Android’s SDK tools to inspect the package. Later, we used OpenSSL and WebCrypto to inspect the encryption, and zbar to decode the QR codes independently.

I was prompting DeepSeek current and there, mostly from intuition, since I’d never done this before. Sometimes it seemed to get misplaced in a direction anywhere I felt the implementation must be simpler than that. Just telling it my guesses, or saying that I idea there must be a simpler way, helped steer it in the correct direction.

We quickly understood that the QR code was a URL containing four things: an encoded document handle, an encrypted expiry timestamp, a random nonce, and a checksum.

The expiry was encrypted using AES-128-CTR. A distinct concealed was used to compute the checksum. So we needed those two secrets for my account.

My archetypal idea was to let DeepSeek run an Android emulator, sign in, and extract the keys. But I quickly realized that my ten-year-old laptop, borrowed from a friend, wasn’t going to grip Android application emulation at any sensible speed.

DeepSeek suggested a established phone. That’s whenever I remembered I had a functional established Android phone lying around.

I booted it, connected complete wireless ADB, and let DeepSeek investigate. It turned out that the app logged the petition and reply payloads for the calls we cared about. The key matter was sitting in a plain content log document on the phone.

Half of this project was ripgrep.

With the keys extracted, we built a uncomplicated SvelteKit web app to create the QR codes.

There were a few incorrect assumptions alongside the way. The expiry timestamp needed to be in milliseconds, not seconds. The encoding was base64url, not norm base64, and it kept the padding.

We additionally had an incorrect AES implementation that passed its own round-trip tests. It encrypted something, decrypted it, and got the first input back. But the two directions shared the identical mistakes, so that didn’t demonstrate it was AES.

Comparing against WebCrypto exposed the discrepancy. OpenSSL and known test vectors confirmed it. The browser app uses WebCrypto.

We afterward checked against genuine output from the first app. Given the identical inputs, our implementation produced a byte-identical URL. We additionally decoded the QR depiction independently to inspect that it contained the correct URL.

At that point, we had a operating prototype. The web app could create the codes locally from the extracted keys. This didn’t resolve standalone login or eliminate the need to get caller keys whenever they rotated, but it was adequate to try it at the gym.

The next day, I headed there, extremely proudly showing my companion what I’d built, but motionless a bit uncertain whether it would activity at the gate.

And voilà, it did!

Check-in and check-out.

No additional being forced to get my notice hijacked by dark-patterned, manipulative social media fair to act an institution to lift several metals.

Other Article Hacker News
↑
Close Right Ads
Close Left Ads