Creating an Apple/Android Wallet Pass for 24 Hour Fitness Memberships
24 Hour Fitness has converted their check-in method to time-based QR codes generated by their app 24GO.
This sucks. The app is not very optimized, sucks down battery, and takes over 200MB of space. The app blocks screenshots of the QR code and the code changes every five minutes. However, there are some flaws in their verification logic that we can use to convert this time-based QR code to a static one that can be added to Apple or Android Wallets without the need for dynamic background services or push updates!
TL;DR#
Don’t wanna read the guide and just want your wallet pass? I made a small webapp that can generate an Apple Wallet pass. If you have an Android, sorry, you gotta do the work yourself (mainly cause it’s easy, and doesn’t need a Developer subscription to get a signing cert).
Head over to Ditch24GO and follow the easy steps. I don’t store your data, except for the generated .pkpass which is automatically deleted five minutes after it’s generated.
The Technical Details#
Since the app prevents you from screen-shotting (at least on iOS) I used another camera to scan the QR code to get the JSON data. What I found was this:
{"TP":"X","OS":"X","DT":9999999999999,"DI":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","AP":"X.XX.X","MB":"MBRXXXXXXXX","SR":"24GO"}
Values have been replaced to avoid leaking personal info. Here’s my educated guesses for the descriptions of each value:
- TP: This is probably login type. I’ve only seen a value “P”. Because there’s an option to login as en Employee, my guess is there’s also an “E” option, which would make “P” most likely mean Personal.
- OS: Operating System. On my iPhone the value is “iOS” and on Android the value is “Android”.
- DT: The DateTime value in Unix Epoch time
- DI: Device Identifier. A randomly generated UUID v4 number which changes on reinstall or update.
- AP: App Version.
- MB: Member Number. The pass’s unique member number which always starts with “MBR”.
- SR: This is probably the scan source. In all my testing the value is “24GO”, the name of the app. Probably used to differentiate from dev environments or retired physical barcode media.
Circumventing the Time-Based Aspect#
Generally these types of QR codes rely on a salted dynamic value, even if that’s just a salted DateTime value. That is not the case here, which means as long as we regenerate the JSON string as a QR code with the current time every 5 minutes we should be fine.
However it gets even better. While poking around edge cases during some field testing I discovered that the check-in system only has a lower bound for the time verification check.
This means we can set the DT value to something in the future and it passes said validation check. I haven’t found a limit to the system, so technically you can set the time to however far you want in the future (i.e. January 19, 2038 if it’s a 32-bit System, or December 4, 292,277,026,596 if it’s a 64-bit System).
The Steps#
Take your original DT value and add 31557600000 to it, which is the number of seconds in 1 year (with millisecond precision). If you want to do a different time you can use a Unix time calculator like UnixTimestamp to do it for you.
Swap the original DT value in the JSON object with your newly modified Unix Epoch time and convert it to a QR code. There are lots of sites you can use for this, like SmallDevTools.
Or, use a Wallet Pass generator like AddPass.io to convert the JSON object directly to a wallet pass for you. Most apps you just paste the JSON text into the QR code generation box, and can even input a geo-location for the wallet to automatically open up to.
Ditch24GO#
I created this webapp to easily convert my pass (and those of my friends) into an Apple Wallet pass. If the components of this project were completely free I would have never made it public, but because I have to pay Apple the $99/yr for a Developer Account (granting access to pass signing certs), I thought others might find this app useful and they could save themselves some coin.
Right now it’s closed source. I may post it to GitHub eventually, but if you want to make a clone (or you’re just curious on my thought process) here’s how I chose to handle the JSON values in the pass generation:
- TP: Since I am pretty sure that means “Personal”, as apposed to “Employee” this is always set to “P”.
- OS: The webapp only generates Apple Wallet passes, so this is always set to “iOS”
- DT: I pin the DT to 3174170649000, which means passes generated won’t expire until August 1, 2070.
- DI: A new randomly generated UUIDv4 gets generated every pass via crypto.randomUUID()
- AP: A function gets the current app version from the Apple App store to save as the app version value.
- MB: You have to input this value.
- SR: I don’t know what relies on this in their backend, so I left this unchanged and it’s always set to “24GO”.
Right now Ditch24GO generated passes rely on the lack of upper bound. If 24 Hour Fitness implemented an upper bound for the DT value to check if passes have a time value ± five minutes, these passes would fail five minutes after creation. Eventually, I will implement an Apple Wallet Web Service backend to allow for dynamic pass updates, that just requires more overhead and right now this whole thing runs cleanly in a CloudFlare Worker.