What's behind a gym's QR code?

You open the app, a QR code appears and you walk in. Simple for the user, but technically a surprising amount happens. A look behind the scenes.

Share

You open your gym's app, a QR code appears, and a few seconds later you're inside.

For the user, it's no more complicated than that.

Yet a surprising amount happens behind that one QR code. The code has to match the right subscription, for instance, must not stay valid forever, and has to be checkable by the access system.

I wanted to know how such a system works technically. So I looked at a mobile gym app, the QR code itself, the network traffic of the iOS app, and the way the app handles this data internally.

For that I used, among other things, Stream on my iPhone and Jadx to inspect the Android app.

In this article I explain what I came across, without you needing any programming knowledge.

It starts with a simple QR code

A QR code looks like a random pattern of black and white blocks.

But such a QR code simply contains information.

That could be a website, a piece of text, a serial number, or a combination of different data.

For a gym's access system, the contents could conceptually look roughly like this:

TYPE:MEMBER:TEMP:Time:CHECK

For a user, that's mostly a strange string of characters.

For the access gate, they're separate parts that together say something about the code.

Think for example of:

  • what kind of access code it is;
  • which subscription the code belongs to;
  • a temporary value;
  • when the code was created;
  • a check value used to verify that everything is correct.

That immediately explains why such a QR code is more complicated than just a membership number in an image.

Why does the QR code change?

One of the first things you notice when using such an app is that the QR code can change.

That's deliberate.

Suppose you had exactly the same QR code forever. Then the image itself would essentially be your access token.

A system can handle this more cleverly by, for example, making the current time part of the code.

Put simply:

Subscription 123
at 10:30

becomes a different combination than:

Subscription 123
at 10:35

Because part of the information changes, the QR code ultimately changes too.

The code is therefore more of a temporary access token than a digital version of a fixed membership card.

There's more to it than just a membership number

During the research it became clear that the QR code doesn't stand on its own.

In the background, the app knows all kinds of data about the user and the device.

Two interesting examples of this are:

subscriptionId

and:

deviceId

That sounds technical, but the idea is fairly simple.

What is a subscriptionId?

A subscriptionId is a unique identifier for a subscription.

Computers prefer to work with unique numbers and codes rather than names.

Suppose, for example, a gym has two customers both named Jan de Vries. Using only a name would then be quite impractical.

Internally, the system can therefore use something like:

subscriptionId: 842193

That way the system knows exactly which subscription it's dealing with.

For the user, such a number is usually not interesting. It's mainly used by the app and the servers.

You can think of it as the internal reference number of your subscription.

What is a deviceId?

A deviceId has a similar function, but is tied to the device.

For example:

Subscription:
842193

Device:
phone-7F21

The real values usually look a lot more complicated, but the principle stays the same.

This tells the system not only which subscription is being used, but also which registered device belongs to it.

That's handy for an access system where the mobile phone has essentially become the digital membership card.

How I came across that data

To investigate the iOS app, I used Stream on my iPhone.

A mobile app communicates with servers constantly.

When you, for example:

  • log in;
  • open your account;
  • refresh data;
  • or request a QR code,

the app sends requests to a server and receives information back.

You can picture that process very simply like this:

Phone
   |
   | requests information
   v
Server
   |
   | sends information back
   v
Phone

Stream made it possible to get a better look, while using the app, at which kinds of information were being exchanged.

That's how data such as a subscriptionId and deviceId surfaced.

That made it clear the QR code is part of a larger system in which the subscription and the registered device are linked together.

Why looking only at the network isn't enough

Network traffic tells you what an app sends and receives.

But that doesn't always tell you what the app then does with that data.

Suppose you see the server sending back this information:

subscriptionId
deviceId
membership number

Then you know the app receives that data.

You just don't automatically know why the app needs it.

That's why I also looked at how the application itself is built.

Inspecting the Android app with Jadx

For that part I used Jadx.

Jadx is a program that lets you get a better look at the contents of an Android app.

An Android app is normally installed on your phone as a package. That package contains all kinds of files needed to make the app work.

With Jadx you can make part of that structure visible again.

You can compare it to taking a device apart.

From the outside you only see:

Open app
Show QR code
Scan

But when you take the app apart, you suddenly see all sorts of separate parts that together produce that result.

One app for iPhone and Android

While inspecting the application, React Native also came up.

React Native is a technology that lets companies develop a large part of a mobile app once and then use it on both iPhone and Android.

That saves a lot of duplicate work.

Instead of:

full iPhone app
+
full Android app

you can share a large part of the logic:

shared app logic
       |
   -----------
   |         |
 iPhone    Android

For users it makes little difference.

For investigating an app it is interesting, though. It means information you come across in the Android version can also give more insight into how the application works in general.

And then Hermes joins in

The app also used Hermes.

Hermes is a technology by Meta that's widely used with React Native apps.

It helps the phone run the app's code efficiently.

Normally you might expect an app's inner workings to sit somewhere as plain, readable text.

With Hermes that's not so simple.

The code is first converted into a format better suited to the phone.

Compare it to a Word document that's first converted to a different file format.

The information is still in there, but you can no longer read it in the same simple way.

For understanding the QR code, that mainly meant a bit more analysis was needed to see how the various pieces of data were combined.

The check value

One of the most interesting parts of the QR code is the check value at the end.

Why is that needed?

Suppose an access code consisted only of this:

MEMBER:123456

That's not particularly strong.

Anyone who understands how the format works could, in theory, create their own text in the same shape.

That's why a system can add an extra check value.

It's calculated based on several pieces of data.

For example:

subscription information
+
temporary value
+
time
+
device information
        |
        v
check calculation
        |
        v
check value

The exact characters matter less than the idea behind it.

The check value belongs to the information before it.

Change an important part, and the outcome changes too.

What is a hash?

For such a check, a so-called hash function can be used.

A well-known hash function is SHA-256.

That sounds complicated, but you can think of it as a machine you feed data into.

For example:

SUBSCRIPTION123-PHONE7-10:30

The computer turns that into a long string of characters.

For example:

82A7B1...

This is just a simplified example.

Change the input a little:

SUBSCRIPTION123-PHONE7-10:31

and you get a different outcome.

The system thereby has a handy way to tie data together.

As a user you don't have to do anything with such a hash. It happens entirely in the background.

From app to access gate

If we put it all together, the process becomes a lot clearer.

You first open the gym app.

The app has information about your account and subscription.

On top of that, the system knows which device is registered.

When the access code is built, various pieces of data are used to create a temporary QR code.

You then show that code at the entrance.

The access system reads the code and checks the relevant information.

Conceptually that looks roughly like this:

Open app
     |
     v
Recognise account
     |
     v
Check subscription
     |
     v
Recognise registered device
     |
     v
Use temporary data
     |
     v
Calculate check value
     |
     v
Show QR code
     |
     v
Scan QR code
     |
     v
Access system checks the code

For the user, all of that takes only a few seconds.

The phone is essentially your membership card

In the past, many gyms gave you a plastic card.

That card contained, for example, a chip or barcode with which you could identify yourself at the entrance.

With a mobile access code, that function shifts to your phone.

Your phone thereby essentially becomes a digital membership card.

Only, such a mobile membership card can do more than a simple plastic card.

The app can, for example, take into account:

  • your account;
  • your subscription;
  • your registered device;
  • the current time;
  • information from the server;
  • and a temporary access code.

That's why the access code can be rebuilt from scratch each time.

Why a screenshot is not the same as the system itself

This also shows why you shouldn't see the QR code as a standalone image.

The image is only the final step.

A lot has already happened before it.

A simple overview:

Account
   +
Subscription
   +
Phone
   +
Time
   +
Check
   =
QR code

When a QR code changes regularly, an old image is therefore not the same as the current code the app builds.

The real value isn't only in the appearance of the QR code, but in the information processed into it.

What happens during login?

While inspecting the network traffic, it also emerged that the app uses techniques like OAuth 2.0 and PKCE.

For this too, you don't need to be a programmer to grasp the basic idea.

An app doesn't want to have to send your username and password again every single time.

That's why a modern login usually works with temporary digital permissions.

Put very simply:

You log in
    |
    v
Server checks your details
    |
    v
App gets a digital permission
    |
    v
App uses it for further requests

OAuth helps arrange that process.

PKCE adds extra checking for mobile applications.

The result is mainly that, after logging in, the app can keep communicating with the server securely without you having to enter your password again for every action.

Simple on the outside, quite extensive on the inside

That's what I ultimately found most interesting about this research.

From the outside, the whole process consists of three steps:

Open app
Show QR code
Scan

But under the hood it looks more like this:

User
   |
   v
Account
   |
   v
Subscription
   |
   v
Registered device
   |
   v
Server communication
   |
   v
Temporary data
   |
   v
Check calculation
   |
   v
QR code
   |
   v
Access system

All these parts work together while the user notices almost none of it.

And that's exactly how good technology is supposed to work: the complicated things happen in the background, while the action stays simple for the user.

What I used during the research

Stream on iOS

I used Stream on my iPhone to get better insight into the communication between the mobile app and the server.

This made clear which kinds of data the app receives and uses, including identifiers relating to the subscription and the registered device.

Jadx

With Jadx I inspected the Android version of the application.

That lets you examine the structure of an Android app and better understand which technologies and components are in it.

A QR scanner

An ordinary QR scanner was actually the start of the whole research.

With it I could see that behind the visible QR code there really was a structured string of data.

Technologies behind the system

During the research, various well-known technologies came up.

React Native is used to build mobile applications where a large part of the code can be shared between iOS and Android.

Hermes helps run React Native code on mobile devices.

OAuth 2.0 is used to let users log in securely and give apps access to a logged-in session.

PKCE adds extra security to that login process for mobile apps.

SHA-256 is a hash function that can convert data into a check value.

Individually they're all technical terms.

Together they ensure that a user can ultimately just grab their phone, show a QR code and walk in.

Conclusion

A QR code at a gym's entrance may not seem particularly complicated.

But the visible block on your screen is only the end result.

Behind it sit information about the account, the subscription, the registered device, temporary values, communication with servers, and checks the system uses to tie the data together.

By looking at the app's communication with Stream on iOS and examining the Android app's structure with Jadx, it became increasingly clear how those different parts come together.

The most important conclusion is actually very simple:

the QR code is not the access system, it's only its visible end product.

For the user it stays:

Grab phone → open QR code → scan → walk in

And it's precisely behind those few simple actions that the interesting technology sits.

Tags

QR code, app research, reverse engineering, React Native, Hermes, OAuth 2.0, PKCE, SHA-256, mobile security, network traffic, cybersecurity

Get in touch