Notes

The Swiss Travel Pass, Apple Wallet, and the Nightmares of QR Code Decoding

This post is inspired by a similar piece from Iliana Eatoin who describes the process of creating passes for Apple Wallet in much more (better) detail.

I'm kind of obsessed with Apple Wallet.

There's something about it that's just cool. I love having all my cards stored in one place on my phone. I love how "archived" passes show your travel history. I love how loyalty cards automatically update when you get more points. And most of all, I love the design decisions that go into creating a pass.

For this reason, you can understand my frustration when I spent over CHF 700 on a 15-day SwissRail pass for a trip later this summer, only to discover that you can't add it to Apple Wallet. This isn't a limit on the developer side, either. Normal SBB passes can be added to Apple Wallet like a boarding pass.

So, let's figure out how to add it.

Design

The specification for Apple Wallet passes is fairly simple. Each .pkpass file (the specification Apple uses for storing passes) is just a .zip file containing a bunch of images, a pass.json file for organizing the images, and a manifest.json file used for signing the pass (we'll talk about that later). There are also a bunch of different formats available for pass design: Boarding Passes, Coupons, Event Tickets, Store Cards, and then Generic passes that are used for everything else. For this project, I'll be using a Boarding Pass as that's what SBB uses for all of their other passes.

Now that we know what type of pass we want to use, we also have to settle on a design. I wanted the design to be as close as possible to the "real thing" issued by the SBB app, so I did the most reasonable thing expected: I bought the cheapest ticket I could find on the SBB app and added it to my wallet. That happened to be a tram ticket from Zürich: Beckenhof to Zürich: Stampfenbachplatz. It's also for a dog.

Now with the journey secured (for a total of CHF 2.40) we can look at the design. It seems like for these point-to-point journeys, SBB chooses to use the Boarding Pass specification. Otherwise, it seems fairly simple: the pass features the SBB logo in the upper-right corner, the validity information in the upper-left corner, and generic pass information in the fields you would expect.

With that in mind, it was now time to build.

Building the Pass

Luckily, SBB didn't disable sharing of passes, so I was able to take the pass I purchased for my (fictional) dog, and AirDrop it to my computer. Because .pkpass files are just zipped directories, I was then able to extract its contents to a directory and get all the original assets.

This was really useful for a few reasons. Primarily, it gave me all of the original assets used by the SBB app. But, more importantly, it gave me some insights into how SBB does code placement. As far as I can tell, the code SBB uses in their app is identical to the one on the pass, and isn't replaced every x minutes. I was also given a static QR code when I purchased my SwissRail pass, so I can also just use that QR code text.

Now to get signing working. I have an Apple Developer account, so I was able to follow the instructions here to get signing working without too much effort (I use a mac, but didn't want to use Apples's tool). With that done, I was able to simply change the passTypeIdentifier and teamIdentifier fields and re-generate the pass with changed fields! Now that we've got pass generation working, it's fairly trivial to change the fields of the pass to fit our needs. In addition to the pass we downloaded earlier, I'm basing the design off of a support page that seems to show the Swiss Travel Pass as a .pkpass. I can't find any such link, however.

QR Code Generation

The hardest part of creating a Wallet pass is actually the QR code. The codes created for the RailPass are complex, and are some of the larger QR codes I've seen in a boarding pass. You can see this in the dog's boarding pass from earlier. These codes are encoded in iso-8859-1 which also makes them particularly difficult to parse. For example, here's the data from my CHF 2.40 dog ticket:

\n·\u0001\bèÉÚ¿ß\u0012\u0012`\n\u0012\b\u0004\u0012\u000EZVV Dog Ticket\u0012\u0012Zürich, Beckenhof2\u000BKurzstrecke8\u0001B\u0007\b ¿º¸û1J\u0007\bà­¨¹û1`Œº\u0002z\u000B(2.)(V)(HA)˜\u0001\u0002 \u0001\u0000\u001A#\u001A\u000BHertenstein\"\u0005Eliot*\u0007\b€˜ðö!:\u0004HUND*\u0011\n\u0007\b˜’¼¸û1\u0010\u0004\u0018ߙ\u0001 \u000B2\u0010\n\u0003AMX\u0012\u0003CHF\u001A\u00042.40:\u0000H\u0000\u0012\u0002\b\u0001\"\r\n\u00043342\u0012\u000500001*.0,\u0002\u0014AÐi\u0011~%\u0006_¦qIw¿Y,坅²‡\u0002\u0014FOÚðÖb™ê̘6íz£0¡j]·

Because it's not just an id, we basically need to reverse-engineer the QR code and figure out how to generate a 1:1 equivalent. I went through a ton of trial and error at this stage, mainly because I couldn't find a reliable enough data source that I was happy with.

By first thought was just to scan the QR code. After all, the data should just be embedded in that, right? Well, it's more difficult than than. Not only does SBB seem to have a proprietary system for encoding QR codes,1 but I was getting horrible results from almost every QR code scanner I tried, mainly because as mentioned above, the data is INSANE.

The second method I tried was grabbing the official QR code data from some official SBB API endpoint. After looking through all of the data on sbb.chto no avail, I decided to look through the mobile app using a proxy and a manually-trusted SSL certificate. Sadly, that didn't work at all because the SBB mobile app uses certificate pinning and would just break as soon as I enabled a proxy. Why it does this I have no idea, but it makes it impossible to read that data.2

While doing all of this googling, I stumbled upon a help article from Rail Europe that includes a Swiss Travel Pass in pkpass format. Awesome! I'm not sure how they generated that, but I figured it couldn't hurt to send them a DM on Twitter addressed to their development team. It's worked for me before!

Their response was fairly concise:

With that option down the drain, I returned to step one: finding a QR code decoder. While most of the online QR code readers weren't meant to handle a massive QR code like the one I was decoding, there were a few which seemed to produce decent results. Specifically, the Dynamsoft QR code reader gave me consistent byte information across the two passes I was looking to read. Great! With that info, I wrote up a simple script to take the byte data from their generation, convert it to iso-8859-1, and then insert it into the pkpass bundle. Then I uploaded that bundle to the iOS simulator:

It turns out that for some reason, iOS just doesn't like some iso-8859-1 encoded strings and will refuse to open a pass using them. I wasn't able to debug this issue because it's so generic; what kind of error message is "Invalid data error reading pass … barcode message?"

6+ hours into this small project, I basically gave up. Encoding the QR code data in utf-8 should (hypothetically) give the same data when generated, right? With that in mind, I changed a string in my script, generated the utf-8 QR code data, and added it to the pass. Then, I clicked generate.

Sorry, you can't see the QR code.

I still have no idea if the code works or not, and at this point, I don't really care. Even if it doesn't work when scanning on the train (at this point I have to assume it won't work), I still learned a lot from this process overall. pkpass as a standard is really cool and also really frustrating if it doesn't work out the way you want it to. Hopefully going forward I can deal with some nicer QR codes and keep on adding stuff to my Apple Wallet.

Or, If this reaches anyone at SBB who knows how to find the QR code / pkpass data, please email me.

Footnotes

  1. I am kind of amused that someone working on the KDE PIM team ran into the EXACT problem I was having when trying to decode these.

  2. I still have a decent hunch that the encoded QR is sent here somewhere (or just the PKPass file) so if anyone has any ideas on how to intercept those messages / unpack the cache, my DMs are open.