Background
I stayed at a hotel a while back and if you read my post about the Flipper Zero, you’ll recall that I managed to clone my hotel key. Big deal right? I couldn’t do anything useful with it. Any attempt to change the expiration date or what I could do with it immediately rendered the key useless. I had read about a vulnerability called Unsaflok that was pretty disturbing but to date they haven’t released the details so I marked that one down as “scary, but safe from your average Flipper Zero user”.
However, a post on Reddit re-ignited my interest in the topic. On the surface, it seemed like a typical skid post from a user claiming they cloned their hotel key card and saying how terrible it was that you could clone cards like this. My initial reaction was incredulity. So what? I’ve cloned my hotel key and it didn’t help me with anything but getting in and out of my own room. But as I dug deeper into the post it became clear they did actually know what they were talking about. I was pretty oblivious about key cards and how they worked so I took some time to do a little research and the pieces started falling into place.
Mifare Classic 1k
So I’m no expert as I hope you would understand by now. But I have managed to scratch the surface of the different types of digital key cards available and part of the problem with my hotel card in particular is that it uses an outdated format to secure the data it contains. The way I understand it, the Mifare Classic 1k card is a container for some number of bytes of data. It isn’t part of the access control system at all, other than it stores the data used by the access control system. If you were to design your own system, you could use QR codes, or mag stripe, whatever. The key card could be anything, it’s what you put on it that matters the most. That said, you still shouldn’t give up the data without proper access controls.
What’s supposed to happen is the Mifare Classic 1k card won’t “release” the data stored unless presented with an appropriate key. The door reader has some key stored on it that is transmitted to the card, then the chip on the card verifies the key, and if it matches, the card returns the data. This should protect the card from being cloned, but as it turns out, many access control systems use the same keys. There are plenty of good resources out there that discuss the vulnerabilities that allow you to bypass this security feature so we’ll just gloss over it by saying the Flipper supports cloning these cards out of the box using a built in dictionary of keys as well as the ability to fetch keys from the card reader itself to make the process of recovering data even easier. Cool. But now what? I have the piece of code the card reader expects to see when I use the card to open a door. And that’s about it, right? I’ve not escalated my level of access and my card will stop working when I check out so this seems pretty useless on the surface.
Saflok
Enter Saflok. Saflok is an access control system used by literally millions of hotel doors around the world. You’ve almost certainly used one if you’ve stayed in a hotel before. Before NFC, they used those magstripe cards, but the operation of the locks and security features are the same. As I hinted earlier, there is a known vulnerability with Saflok, but the details aren’t public. But this got me thinking. What could you do with the Saflok data I have in my hands? What are all these numbers and what do they mean? And how is it exactly that the Flipper can read the data, but can’t change it?
Early attempts and naive approaches
Maybe you can just, edit in place?
The first thing I started doing was fiddle with the .nfc file the Flipper generates when it clones the key card. The Mifare Classic 1k card stores, you guessed it: 1kb of data. Good job, you’re really learning! It’s broken into 64 blocks with 16 bytes on each block. 4 blocks make up a sector and from the looks of it, only the first three blocks have actual interesting data in them; the fourth block is the key. So I started by searching for known pieces of data in the file. I knew, for example, that my key card had an ID of 0xf7. So I looked for any blocks containing that number. It wasn’t there. So I searched for a few other pieces of data like the property number, or the date, but none of it seemed to match up with what the flipper was showing on the info screen. So there was something more to it.
For fun I did change some of the bytes at random to see what would happen. I had a few interesting moments where the year changed from 2025 to 1997 and a few other odd changes here and there, but nothing predictable. And each time I did change anything, the checksum had a 50/50 shot of failing. This obviously wasn’t going to work. This was the point where I had previously decided not to bother with trying to modify the card because clearly something else was going on and I wasn’t sure what to do next.
Narrowing in on saflok.c
But then it occurred to me. The Flipper can read the card and display the information. On top of that, the Flipper source code was available online, which meant that I should be able to see exactly how the Flipper reads the data, which might then lead me to a way to write new data. So I fired up my laptop and started perusing the Momentum firmware GitHub repo. It actually didn’t take any time at all to find the file called saflok.c. Neat, I wonder what’s in that? I said to myself out loud. I had a peek and sure enough, the decryption function was right there. Aha! So the data is protected by another layer of encryption. Unfortunately, I couldn’t decipher the decrypt function myself and the encrypt function was not implemented so I had no way of making new keys; I could only view existing keys. Give up, right? Well, no. I had been changing one byte at a time to see what would happen using a very painfully slow process of edit/open/check/repeat. Each test took a minute or so and there is just no way to get anything reasonable done at that speed. But, what if I could program my computer to do that a hundred thousand times a second?
Chosen ciphertext attack
Naturally, that’s exactly what I did. Thankfully. Mercifully, C and Python share the same logical operators and syntax for bit shifting operations so translating between the two was surprisingly very easy to do. It took almost no time at all to code up an equivalent function to decode existing card data and display it to the screen. And the checksum validation function would also allow me to check to see if any modifications were considered valid. Then it was a simple matter of jamming as much crap into the input as possible and see if anything useful fell out the other side. My first test was just changing between 1 and 4 random bytes of an original cloned card. I wanted to see if I could get a valid future date for my card without breaking the property number (i.e. what hotel it goes to) and the checksum. I didn’t expect it to work, but hey, it was worth a shot, right?
Sure enough, no more than a second or two after firing off my search, I ran across a valid guest key with an expiration date 4 years in the future. I believe I literally shouted with excitement when more valid keys just kept popping up over and over again. These couldn’t possibly work, could they? I tried adding more parameters and changed the type of keys to see if anything else would come up. This was when my excitement started to wane a little bit. As more parameters were added, the length of the search process increased dramatically. Worse, some parameters seemed to be nearly impossible to set without breaking others. This suggested some underlying relationship between these fields, either by way of the encryption algorithm, or shared data segments. My suspicion was that the cipher just happened to create a situation where some fields were closely linked, where other fields were completely independent of each other. I may be a crypto-zoologist in my spare time, but I’m not a cryptographer, so I’m just going to have to guess at this one.
Testing 1, 2, 3
After generating a couple of promising card files (my program generated files that I could upload to my Flipper for use later, either by writing a new, blank card or by using the Flipper directly), I decided there was really only one way to find out if any of this would work. I went back to the hotel where I stayed a few weeks prior. With my Flipper in hand and trying desperately to look like I actually belonged there, I walked up to the side door with the key I assumed was most likely to work loaded up. I hit the emulate button and… well, you can guess since I’m writing this article, it worked! I walked right in through the door with a very, VERY expired access card that should not have worked. I was so surprised by this immediate, and resounding success that I didn’t know exactly what to do with myself. I wandered up to the continental breakfast area hoping they had some leftover coffee I could avail myself of, but my attempt at petty theft was foiled by the fact that most sane people don’t drink coffee after 2 in the afternoon.
So, I walked right back out the way I came in. Having had no real expectation of the door actually opening using this ridiculous key cloning technique, my plan of action was pretty basic. Step 1: try the door. Step 2: ???? I didn’t end up with a cup of free coffee after all, but I did walk away with the knowledge that I was onto something. That it should be possible to create cards with whatever data I wanted, assuming I could reverse engineer the encryption algorithm. That was a big if, as you’ll recall my lack of expertise in the cryptography field. I wasn’t even sure it was mathematically possible to reverse such an encryption function. I spent a whole lot of time pondering this question before I decided to just take a swag at it.
Cracking down
AI still isn’t that smart, y’all
Of course, the easiest way to get this to work is just not do it at all. Get the AI to figure it out for me, right? I am happy to report that despite my best efforts, and multiple rounds of prompting with ChatGPT and Gemini, I could not get any serviceable code from the AI. As you’re probably well aware, my outlook on our AI future isn’t exactly rosy. But at least for now, I know I’m still smarter than either of these two bumbling idiots because I couldn’t even get code that would run cleanly without throwing errors, much less actually reverse the encryption algorithm. One of the more endearing things about the AI apocalypse is how confidently wrong AI is. “Oh, of course you are correct to point out that I didn’t initialize that variable before using it. Here’s the corrected version of that code that addresses this issue and will return the encrypted data.” It didn’t, and it doesn’t. We’re safe for now.
Actual Intelligence for the win
So, fuck the AI. It can do the busy work crap I don’t want to bother with anymore like handling command line arguments, file reading and writing, and remembering what libraries I need to import. Meanwhile I was staring at the screen, talking to myself for the better part of an hour without so much as writing a single line of code. Maybe this isn’t possible? It’s true that not all encryption algorithms can be reversed. There is a whole branch of study called asymmetric key cryptography that uses pairs of keys: one for writing, one for reading. If you don’t have both, you’re shit out of luck. But I had a few hints that suggested this encryption scheme isn’t nearly as robust as anything like that. In fact, it was only 14 lines of code and an array of bytes that looked like some kind of glorified “Little Orphan Annie Decoder Ring” jawn. After a quick inspection of this array, it was clear it contained all 256 possible bytes from 0x00
to 0xff
with no duplicates and in a randomized order. The actual name for this type of lookup table would probably be more like a Ceasar cipher. But that wasn’t the whole story, otherwise this would have been completely trivial to reverse. Here’s the function, in its entirety. c_aDecode
is the substitution table.
void DecryptCard(
uint8_t strCard[BASIC_ACCESS_BYTE_NUM],
int length,
uint8_t decryptedCard[BASIC_ACCESS_BYTE_NUM]) {
int i, num, num2, num3, num4, b = 0, b2 = 0;
for(i = 0; i < length; i++) {
num = c_aDecode[strCard[i]] - (i + 1);
if(num < 0) num += 256;
decryptedCard[i] = num;
}
if(length == 17) {
b = decryptedCard[10];
b2 = b & 1;
}
for(num2 = length; num2 > 0; num2--) {
b = decryptedCard[num2 - 1];
for(num3 = 8; num3 > 0; num3--) {
num4 = num2 + num3;
if(num4 > length) num4 -= length;
int b3 = decryptedCard[num4 - 1];
int b4 = (b3 & 0x80) >> 7;
b3 = ((b3 << 1) & 0xFF) | b2;
b2 = (b & 0x80) >> 7;
b = ((b << 1) & 0xFF) | b4;
decryptedCard[num4 - 1] = b3;
}
decryptedCard[num2 - 1] = b;
}
}
The first step was to make this whole thing easier to read. I don’t know how exactly this works, but I suppose human names are just that much more natural to use so I ditched numX
variables in favor of simple human names. Now when reasoning about the core of the function, I’m not stuck trying to remember what bits were shifted from num3
to num2
and so on. This strategy worked surprisingly well. I also spread out the lines a bit to make it easier to see what was what. The end result was something like this (I didn’t save the actual code so you’ll have to forgive me for this crude re-enactment):
for(zeta = length; zeta > 0; zeta--) {
bob = decryptedCard[zeta - 1];
for(xi = 8; xi > 0; xi--) {
yip = zeta + xi;
if(yip > length) yip -= length;
int charlie = decryptedCard[yip - 1];
int alice = (charlie & 0x80) >> 7;
charlie = ((charlie << 1) & 0xFF) | david;
david = (bob & 0x80) >> 7;
bob = ((bob << 1) & 0xFF) | alice;
decryptedCard[yip - 1] = charlie;
}
decryptedCard[zeta - 1] = b;
}
Now that I had a way to create a narrative to describe the operations, it became much easier to implement the reverse. I won’t say it was easy, and there was some amount of guess and check going on, but in the end, I coded up this solution in python. I should add here that I was overjoyed when this code actually worked. My wife surely remembers me in a manic state, describing in detail all of the ins and outs of this hack at midnight, celebrating my graduation from skid to something one step above a skid. She probably just wanted to go to bed. I’m glad she listened anyway.
def encrypt_card(dcard_data):
ec = []
length = 17
alice = 0 # ??? not sure how this would be set, it doesn't seem to matter...
for zeta in range(1, 18):
bob = dcard_data[zeta - 1]
for xi in range(1, 9):
yip = zeta + xi
if(yip > length):
yip -= length
charlie = dcard_data[yip - 1]
# Retrieve charlie's lsigdig aka david
david = bob & 1
# Put bob back together
bob = (bob >> 1) | (alice << 7)
# Get alice back
alice = charlie & 1
# Put charlie back together
charlie = (charlie >> 1) | (david << 7)
dcard_data[yip - 1] = charlie
dcard_data[zeta - 1] = bob
# This works. No need to do anything here
for i in range(0, length):
num = dcard_data[i] + (i + 1)
if num >= 256:
num -= 256
ec.append(DECODE_ARRAY.index(num))
return ec
Forging ahead
The moment of truth had arrived. I had a working encryption function, a working parser, and a some glue code to pull .nfc
file data in from my Flipper to easily wrangle it on my desktop. Time to create some forged cards. My solution (which you can find here) has a few bells and whistles added to make cranking out forged cards much easier. Thanks to the ubiquitous argparse
library, these are all neatly presented to you on the console. Sometimes I wonder what I would do without the people who maintain these libraries.
usage: saflok.py [-h] [--key-level KEY_LEVEL] [--led-warning {0,1}] [--key-id KEY_ID]
[--opening-key {0,1}] [--key-record KEY_RECORD] [--property-id PROPERTY_ID]
[--override-deadbolt {0,1}] [--restricted-weekday RESTRICTED_WEEKDAY]
[--interval-year INTERVAL_YEAR] [--interval-month INTERVAL_MONTH]
[--interval-day INTERVAL_DAY] [--interval-hour INTERVAL_HOUR]
[--interval-minute INTERVAL_MINUTE] [--creation-year CREATION_YEAR]
[--creation-month CREATION_MONTH] [--creation-day CREATION_DAY]
[--creation-hour CREATION_HOUR] [--creation-minute CREATION_MINUTE]
[--sequence SEQUENCE] [--output OUTPUT]
input_file
Craft arbitrary Saflok cards using an existing NFC capture file from Flipper. Author: Gizmonicus
positional arguments:
input_file Path to the input NFC file. Defaults will be set using this file as the
template.
options:
-h, --help show this help message and exit
--key-level KEY_LEVEL
Key level number (int)
--led-warning {0,1} Enable LED warning (0|1)
--key-id KEY_ID Key ID (1-byte hex)
--opening-key {0,1} Opening key (0|1)
--key-record KEY_RECORD
Key record (1-byte hex)
--property-id PROPERTY_ID
Property ID (int)
--override-deadbolt {0,1}
Override deadbolt (0|1)
--restricted-weekday RESTRICTED_WEEKDAY
Restricted weekday (7 digit binary)
--interval-year INTERVAL_YEAR
Interval year (int)
--interval-month INTERVAL_MONTH
Interval month (int)
--interval-day INTERVAL_DAY
Interval day (int)
--interval-hour INTERVAL_HOUR
Interval hour (int)
--interval-minute INTERVAL_MINUTE
Interval minute (int)
--creation-year CREATION_YEAR
Creation year (int)
--creation-month CREATION_MONTH
Creation month (int)
--creation-day CREATION_DAY
Creation day (int)
--creation-hour CREATION_HOUR
Creation hour (int)
--creation-minute CREATION_MINUTE
Creation minute (int)
--sequence SEQUENCE Sequence combination number (12-bit hex)
--output OUTPUT Path to the output NFC file. If not specified, card data will be printed to
stdout.
So let’s say you have a room card and all you wanted to do was change the expiration date and give yourself the ability to override the deadbolt (yep, that’s electronic). Easy peasy. Clone the card with your Flipper, dump the .nfc
file to your desktop and just set your desired expiration dates. I was lazy and didn’t code for any of the rollovers or leap years. You’re on you own with that. When I first wrote this script, it ran disappointingly quickly so I added an artificial 2 second delay just to make it feel like it was doing something, hence the “preheating the oven” message.
Fun fact: This actually is a common practice used in real user experience designs. My favorite anecdote is that of the Huston Airport baggage claim. A study concluded that people didn’t like the perception of waiting around, and that customer satisfaction could be improved by simply making the walk to the baggage claim area longer so the bags were ready by the time they arrived, even if the actual wait time was identical. Likewise I’ve noticed that if you’re stuck in traffic on the highway, you can reduce the perception of the amount of time you have to wait by taking surface streets, despite the fact that this frequently only adds to the travel time.
./saflok.py BW_uni.nfc --interval-year 1 --override-deadbolt 1
Preheating the oven... \
Saflok key fully baked.
###### Key Level: 1
# Key Desc: Guest Key
# LED Warn: 0
# Key ID: 0xf7
###### Opening Key: 1
Key Record: 0x0011
## # Seq/Combo: 0xe34
# # # Property ID: 1142
# # # Override Deadbolt: 1
# ## Weekdays (mtwtfss): 0000000
Valid for: 01/00/01T13:00
# ### Created: 2025/02/01T20:13
# # # Checksum: 0xd4
# # Checksum Pass: True
###### Encoded Bytes: 0a c0 6f 56 11 69 20 db 8b 04 e9 32 bd 8a a4 7b 8a
Back to the task at hand. Having this newly minted forgery is great and all, but what if you want to get into any room in the hotel? Is that possible? Can you grant yourself full access, including staff areas? Short answer: Yes, you definitely can. There are many different key levels, including floor keys, master keys, emergency keys, and so on. Some of those are all powerful; they can open any door in the hotel and override any privacy features enabled on the lock. But… there’s a longer, less satisfying answer to follow.
Sequence and combination
We need to talk about the sequence number. It’s the biggest gotcha with this whole process. When you get a key card for your hotel room, the system that generates them keeps track of a number that corresponds roughly to how many times the room has been rented out. In simple terms it works like this:
- You rent the room. Your key card has a sequence number of 123.
- You open the lock with your key card. The lock checks to see if your sequence number is the next one it’s expecting to see. If it is, it increments its counter to 123 and lets you in the door, assuming your key is still valid. Saflok systems allow for some number of “skips”, meaning the sequence number can be a few numbers ahead and it will still work. The maximum, if I recall, is 15 skips. This is important because otherwise you could just pick a really high sequence number to bypass this restriction. Instead, you have to get very lucky and land in this 15 number window.
- When you check out and another guest is issued a key for the same room, they’ll get a new sequence number, most likely just incremented by one. In this case it would be 124.
- When the next guest uses their key card, their sequence number gets stored in the lock and your card will no longer work. Even if you have a valid expiration date. The moment a new card is used, yours becomes invalid.
So you can see where this might be a problem. There are a large number of valid sequence numbers so guessing the right one would be all but impossible. Not only that, you can’t just try all of them because the lock isn’t that stupid and will go into lock down mode after 10 failed attempts. So you’re stuck. You need to resequence the lock somehow. There exists a way to do this. I know because the people who figured this out did a whole Defcon talk about it and disclosed that information to the vendor responsibly (as you would hope, given the magnitude of this vulnerability).
This, unfortunately, is where my story ends. At least for now. If you pay close enough attention to the encrypt/decrypt/parse functions in the source code, you’ll notice a few irregularities. It took printing out the binary code and a highlighter for me to finally narrow it down.
You can see that most of the bits are accounted for, but you’ll also see in my scribbling that there are several whole bytes that aren’t accounted for: The last 3 bits of the first byte, what I assume is the key record (is it really almost 2 whole bytes?), and the 5th plus half of the 6th byte. I don’t really know what they do. I suspect that these bytes are important for actually pulling off the full exploit. My guess is that you need to set one card up as a programming key card (key levels 15 or 16) so you can overwrite the sequence number of a door. Then have a second key set up as an emergency or similarly high level key with a new sequence number that matches. But to test this theory out would require a controlled environment that is just too expensive to justify. At a minimum I would need a lock from a hotel, a programming tool (both of these are in the hundreds of dollars on eBay, trust me I’ve looked), and possibly even the .dll
files from the Saflok computer system itself. So while I would love to post a picture from inside some random guest room in a hotel I don’t actually have a key for, 1) I don’t want to go to jail, and 2) I don’t want that badly enough to spend hundreds of dollars on the off chance I can get lucky and figure it out myself.
The end?
I think so. I don’t anticipate putting much more effort into this because I’ve already gotten something tremendously valuable out of the experience. Despite the fact that I was following in the footsteps of others who came before me, I still felt that sense of discovery that I’ve been chasing my whole life. Finding an interesting problem to solve, being inspired to create and discover new-to-me solutions, and being rewarded with that brief euphoric moment when the light flashes green and the lock gives way with a satisfying ker-chunk brings me joy I struggle to express with written word. Perhaps in the future I’ll find myself in the right place at the right time and I’ll have another chance at digging deeper into the Saflok mystery, but for now I think I’ll simply allow myself to be content with a small victory.