Reverse-engineering a Bluetooth Keyboard

Upd: I fell asleep in the middle of writing, oops!

About an year ago when I've just joined #meta I've asked a question, it was something like "Hey, can somebody recommend a foldable Bluetooth keyboard?". After some looking up I ended up buying one that folds from the side into a rather nice aluminum nugget, lo and behold:

./2026-05-06-reverse-engineering-a-bluetooth-keyboard/DSCF5877.png

It however has one serious flaw. Whoever made this has decided that media keys are much more important like good old F-key row, making use of many tools janky, especially with that Fn key on the right side.

Well, okay, enough is enough and I decided to open the thing up and see what is going on inside, maybe it has some kinda switch-pad like old Chinese tetris games did. Oh boy no, we're over that.

it a computer, oh no...

Turns out the keyboard runs on 2 processors, one at least capable of running tetris (SG8F6402, a generic USB keyboard thing) and another is CYW20730, better known as Broadcom BCM20730.

Importantly, besides a big blob on that yellow board you can see a smaller one, that is 256Kbit eeprom that stores firmware and dynamic data. The fact that this eeprom is a separate chip makes it possible to hook up a good old FTDI and try to figure out how this thing works.

Desoldering that eeprom and wiring switches took some time

Nope, not putting that chip back on the yellow board

And that's what I did, 32 kilobytes, loaded straight into ghidra and... it does not make much sense!

Time to dig. Meet https://github.com/carson512/BCM-CYW20730_PIN_Bypass

This seems to be one of 2 repositories on github directly related to the case, after translating readme it turns out another device running on the same chip had old way of pairing with devices, and iphones do not accept pin auth because it's old. carson512 ended up writing extra tool to analyze dump, it talked about TLV (type-length-values) objects, and it turns out that is what is stored on eeprom!

The firmware is split into static header that configures bootloader, raw binary looks like this:

and if we decode that, output looks like this:

The last record is one most curious. it has 2 LE addresses, 0x400 and 0xc0. 0xc0 is where firmware code stores paired devices and last active profile, and 0x400 contains the "script" that loads custom program.

Where did I find these block names?

It turns out Broadcom/Infineon call these Config Data Entries and they are a part of their SDK, ha!

I then fed that into chatbot to create a parser for the schema and loaded the second part of script to see how it looks like:

parsedfw.txt

Defininely lots of stuff I have no idea about. BUT. The data upload operationa always make sense, 0x0020**** corresponds to MCU RAM locations, woohoo. And taking a single data blob and putting that into binary and loading that at specified address now indeed procuces something that ghidra can decompile.

The way this works is that there is an on-crystal boot rom on that chip that has a task of detecting eeprom chip and then reading data from there to, it basically executes a script file that can do this:

Some structures do not add up, there are tail bytes. But then it struck me that 20703 and 20730 are 2 different chips, so maybe it is just a different definition file, regardless I now has enough information to create partial image that I can decompile.

After some poking around that script, I came up with spec and asked the other Gemini to gobble me up parser that takes the "sfx archive" on eeprom, takes all the data upload operations and fills an empty file with them while also interpreting where the data should be loaded.

With this I now had a dump that should contain all of the relevant code to make a device work as keyboard. Big mistake, that brom actually has built-in OS and routines to make your device an bluetooth keyboard. What the uploaded code does is some light customization around it, but notably that handles function keys, so let's dig again.

I know little to nothing about that broadcom chip but after some digging it seems:

Note the last 4 bytes, they indeed have 87 and 89 codes, except there is some other number before it. Hrm. Well in any case looking up a bit we can see this array is referenced by a function, another interesting thing is that this keyboard has exactly 22 Fn-mapped buttons (pairing and bluetooth pairing are different).

So this has to be a function that uses some index to find out what function key was pressed, but how is that index calculated? Hey there is another list:

I have no idea what it is but it's exactly 22 bytes as well! Maybe, this is an array of XY coordinates that denotes specific keys on a wire matrix. First things first, let's fool around with the first array:

04 HID keycode stands for "a" key, then goes b, c and so on. Now let's upload firmware and try pressing these keys to see what index is what key. Turns out it is like this:

Here are relevant functions after some renaming, This takes above table of 2-byte entries:

And this one checks the smaller table:

SAD, ahem. Intereting fact pops up:

Even though I've set them to normal heys, F-keys still try to send

latin letters until fn is pressed, this means there has to be a global switch that checks range of a returned "customized" key and if it's above 10, autosets "fn pressed" flag. Time to dig some more.

Bingo, we eventually end up in a keymatrix decoder procedure and it indeed has (keyPos < 10) check. Now let's find that in assembly...

We now replace the opcode with 1729 (means keyPos < 10) and write the firmware back. Finally, I can enjoy using F11 key to switch fullscreen or F4 to run terminal in current file browser path.

It's impossible to resist changing mac to something funny. Also changed device name to generic one.

What's next? Well, I am curious, there are 4 keys that do not perform anything useful to me: A, S, D and Control buttons. I want to try and find keycodes for the HJKL buttons and map page navigation there instead.

Also, Fn as the rightmost button it very clumsy, might try to remap it somewhere into left-side position.

In principle, with some poking it is possible to find out matrix codes of remaining keyboard keys, extend that array (and patch all the pointers to it…) and end up with a keyboard with a whole second key layer available to end user, woohoo. It is however unknown where it is safe to load data, I have no idea how is ram used by internal firmware that runs alongside this vendor handler.

P.S. I know I said blog would not be technical…

P.P.S. I am now in process of preparing docs on this, hopefully it will be a curious project for somebody besides me, stay tuned.