Firmware images¶
This page describes what a stock Create keyboard firmware image is at the structural level: an encrypted, signed MCUboot image, shipped inside the vendor's NayaCore service as a whole flash slot with a swap trailer already written. It covers where the images ship, how they are signed and encrypted, the two flash generations, and how to name the image a half is running, without ever needing an image's bytes. The one thing to know before anything else: every stock upload is a permanent swap. There is no trial boot and no automatic revert.
Stock images do not boot on trial and do not revert automatically
Every stock resource carries a pre-written MCUboot swap trailer, so the upload itself arms a
permanent swap, and the bootloader refuses image state writes (rc 8, not supported), so a
test swap cannot be requested at all
STATIC MEASURED911. A signed image that boots badly stays in the primary slot;
the way back is another serial-recovery upload, not a reboot, so firmware experiments are not safe
on a daily driver. See
Flashing and Bootloader.
At a glance
- Standard MCUboot images: 512-byte header, AES-128 encrypted payload, four TLVs.
- One signing key for every MCUboot image Naya ever shipped, stable and beta channels alike.
- Each resource is a whole 648 KiB slot (663 552 bytes) with a pre-armed permanent-swap trailer.
- Two flash generations: plain file names are generation A,
_64names are generation B; the USB product id decides which one a half takes. - The plaintext SHA-256 in each image is what the bootloader reports, so it names what a half runs.
Where the images ship¶
The images are not files in the NayaFlow install tree and not in app.asar. They ship
inside the native service binary (NayaCore; earlier naya_core_fw_service and naya_core_project)
as Qt resources, each blob preceded by its length as a big-endian 32-bit integer. An early belief
that NayaFlow bundled no firmware came from scanning app.asar only STATIC54.
The Qt resource path changed over the product's life, and so did the uploader that went with it STATIC5[^fp-transport]:
| NayaFlow releases | Resource path | Keyboard images | Other firmware | Uploader |
|---|---|---|---|---|
| 0.0.1 to 0.0.2 | :/resources/includes/kbfw/ |
fwl.bin, fwr.bin |
none | bundled newtmgr |
| 0.1.0 to 1.6.10 | :/resources/includes/kb_fw/ + d_fw/ |
kb_fwl.bin, kb_fwr.bin |
d_fw.bin |
bundled newtmgr (nmgr_win/newtmgr.exe, nmgr_mac/newtmgr) |
| 1.11.0 to 1.14.5 | :/resources/includes/kb_fw/ + m_fw/ |
kb_fwl.bin, kb_fwr.bin |
FlashMemory.bin (module bundle) |
NayaCore's own SMP client |
| 1.15.0 to 1.25.1 | :/resources/Includes/... (capital I) |
as above, plus _64 from 1.25.0 |
FlashMemory.bin |
NayaCore's own SMP client |
NayaFlow 1.25.0 and 1.25.1 carry five firmware resources: kb_fwl.bin, kb_fwr.bin,
kb_fwl_64.bin, kb_fwr_64.bin and m_fw/FlashMemory.bin STATIC51.
The Windows and macOS builds of a release embed byte-identical images. The Linux NayaCore shipped in 1.15.0, 1.15.1, 1.17.2 and 1.17.3 is the exception: it embedded the older 1.14.5 images (keyboard 3.28.7, module 2.1.2) while Windows and macOS shipped newer ones STATIC16.
No release contains a recovery, factory or bootloader image; only application images ship STATIC1.
Anatomy of a keyboard image¶
Every keyboard image is a standard MCUboot image: header magic 0x96f3b83d, header size
512 bytes (0x200) STATIC14. The header's flags word is 0x00000004
(IMAGE_F_ENCRYPTED_AES128) on every keyboard image STATIC112.
The header's version field is the placeholder 1.2.3+4 in every image, beta images
included, so an image does not state its own firmware version. The version comes from the vendor's
release notes and, from NayaFlow 1.25.0 (beta 1.22.0), from the app constant
NAYA_CREATE_FW_VERSION ("3.41.0" in 1.25.1, beside NAYA_MODULE_FW_VERSION "2.3.3")
STATIC313.
The payload is encrypted: AES-128 in counter mode, as MCUboot's image format specifies, with a measured entropy of about 7.997 bits per byte. Even the first public images (NayaFlow 0.0.1) are encrypted: their first payload bytes are not a Cortex-M vector table. No plaintext keyboard firmware was ever shipped STATIC12.
After the payload comes the TLV area (info magic 0x6907; tools also accept the
protected-TLV magic 0x6908, which these images do not use). It holds four TLVs, in this order in
the file STATIC4:
| Type | Name | Length | What it holds | Secret? |
|---|---|---|---|---|
0x10 |
SHA256 | 32 bytes | SHA-256 of the decrypted image (header and plaintext payload) | no: it is what the bootloader reports for a slot |
0x01 |
KEYHASH | 32 bytes | hash of the signing public key | no: a public identifier |
0x20 |
RSA2048_PSS | 256 bytes | the RSA-2048-PSS signature | no |
0x30 |
ENC_RSA2048 | 256 bytes | this image's AES key, wrapped with RSA-2048-OAEP | no: only the bootloader can unwrap it |
Header plus TLV overhead is exactly 1 108 bytes in every keyboard and d_fw.bin image:
512 (header) + 596 (TLV area: a 4-byte info header, 36 for KEYHASH, 36 for SHA256, 260 for the
signature, 260 for the wrapped key) STATIC1. We re-parsed the headers, TLVs and trailers of the
stable, beta and d_fw.bin images on 2026-09-23 and found the same layout in each.
A resource (the file NayaCore uploads) is laid out like this STATIC913:
| Offset | Bytes | Content |
|---|---|---|
| 0 | 512 | MCUboot header (magic, sizes, flags 0x4, version 1.2.3+4) |
| 512 | img_size |
encrypted payload |
512 + img_size |
596 | TLV area |
| after the TLVs | up to offset -24 | 0xff fill |
| -24 | 1 | image_ok = 0x01 |
| -16 | 16 | BOOT_MAGIC 77 c2 95 f3 60 d2 ef 7f 35 52 50 0f 2c b6 79 80 |
copy_done and swap_info are left unset. The same trailer is in all 27 MCUboot images of the 25
stable releases and in the 6 beta-only keyboard images STATIC13.
The three sizes¶
Three sizes are easy to confuse STATIC1:
| Size | Example (3.41.0 left, generation A) | Meaning |
|---|---|---|
img_size |
328 880 | the payload length in the MCUboot header |
| MCUboot image length | 329 988 | img_size + 1 108 (header and TLVs) |
| resource | 663 552 | the whole slot, trailer included; what is uploaded |
Tables that list "image sizes", including ours on Versions, usually mean img_size.
Every keyboard resource in all 25 stable releases is 663 552 bytes (648 KiB, 0xA2000),
exactly the size of the bootloader's primary and secondary slots; the d_fw.bin resource is
331 776 bytes (324 KiB, 0x51000) STATIC1. The slot size itself was read from a half in its
bootloader with image slot info (owner's board, 3.41.0, 2026-09-16) MEASURED8.
The swap trailer: every stock upload is permanent¶
The consequence of that trailer: once the whole resource lands in the secondary slot,
MCUboot's swap table (secondary magic good and image_ok set) schedules a permanent swap on the
next boot. Nothing has to "mark" the image, and a later mark would be a no-op
STATIC917. Both halves of the owner's board showed it: after each upload the new
image was already in slot 0 and the replaced one in slot 1, on 3.35.4 to 3.41.0 on 2026-09-20, and
across six uploads in both directions between 2026-09-20 and 2026-09-22 MEASURED11. There is
no trial boot and no automatic revert for a stock-style upload; the bootloader also refuses the
image state write that could request a test swap (Bootloader).
What protects a half is MCUboot's own signature check: an image that fails it is never swapped in (Flashing).
Signing¶
One signing key signs every MCUboot image of the 25 stable releases (the 26 keyboard
images of both flash generations and the d_fw.bin image) and the 6 beta-only keyboard images. Its
KEYHASH is de8b07187913e6e788306618e4166e38a8c2eda99b68970d17fd00e75fd5b972. The KEYHASH is the hash
of the signing public key: a public identifier, not a secret and not a decryption key
STATIC13.
The keys are Naya's own: we checked the images with imgtool against the published sample keys of MCUboot, Nordic and Zephyr, and none matches STATIC.
Because every stock image is signed by the key the bootloader already trusts, any stock image can be flashed, older ones included: a downgrade is an ordinary flash STATIC. It has been done: the left half of the owner's board went from 3.41.0 to 3.35.4 (whole resource, 59.8 s upload, 2026-09-20), and both halves were then flashed in both directions MEASURED.
Scale: across the 25 stable releases there are 63 distinct firmware files: 26 keyboard
MCUboot images (both generations), 1 d_fw.bin MCUboot image, 6 distinct LittleFS module bundles
and 30 .sfb module apps. The beta channel adds 6 keyboard images (three beta-only left and right
pairs); its other 41 carried images are byte-identical copies of stable images (39) or repeats of an
earlier beta (2) STATIC13.
Encryption¶
Each image has its own AES content key: the ENC_RSA2048 TLVs of all images differ
STATIC12.
The host never holds a key. No installer of any release contains PEM blocks, private keys or crypto-library strings; NayaCore uploads the encrypted resource as it is, and the bootloader verifies the signature and decrypts the payload on the device, during the swap STATIC712. That is why stock firmware can be flashed without any key.
In MCUboot, encryption is a per-image header flag, so the format itself allows a signed but unencrypted image DOC17. Whether this bootloader would boot one is unknown and cannot be tested without Naya's signing key INFERRED (open question).
With MCUboot's RSA key wrapping, the private key that unwraps each image's AES key lives inside the bootloader, and an image builder needs only the matching public key INFERRED17. Recovery methods are out of scope for this site.
Flash generations A and B¶
_64 means flash generation B. It is not a word size and not a "dual-bank slot":
kb_fwl_64.bin is the left image (img_size 328 880) and kb_fwr_64.bin the right image
(226 000). Files without the suffix are generation A STATIC1.
NayaCore picks the image from the half's USB product id
(Naya_Device::setCreateFlashGenerationFromPid: bit 0x1000 of the PID set means generation B),
refuses to flash when the generation is unknown, and logs a mismatch when a stored generation
disagrees with the PID STATIC10. The PID table is on Bootloader.
Each physical half therefore matches exactly one of the four current images (left or right, generation A or B); the four are not interchangeable STATIC110.
Generation A and B images of one side have the same img_size but different plaintext
hashes. Generation B images first ship in NayaFlow 1.25.0 (2026-07-17), and only with keyboard
3.41.0. The vendor's note for NayaCore 6.11.0 calls them "64-bit Create Firmware Updates" (support
for updating 64-bit Create left and right firmware images); the beta release title misprints it as
"4-bit" STATIC DOC11416.
What physically distinguishes generation-B hardware is unknown. The nRF52840 is a 32-bit part, so "64-bit" cannot be a word size; a larger QSPI flash part (for example 64 Mbit) is a guess. No generation-B half has been measured by anyone we know of; all three of the owner's boards are generation A INFERRED (open question).
Naming the image a half runs¶
TLV 0x10 (the plaintext SHA-256) is exactly what the bootloader's image state read
reports for each slot. Matching it against a table of stock images names the side, the generation
and the firmware version without trusting the USB product id MEASURED STATIC. Measured on the owner's board:
slot 0 matched kb_fwl.bin of 3.41.0 (2026-09-01), and both halves matched after the 2026-09-20
flashes111.
The four 3.41.0 images STATIC1:
| File | Side | Generation | img_size |
Plaintext SHA-256 | Matched on hardware |
|---|---|---|---|---|---|
kb_fwl.bin |
left | A | 328 880 | 479e89ba6c92ead9c46d1228d5033437caf63d72db89364081b9b0a321b31283 |
yes MEASURED |
kb_fwr.bin |
right | A | 226 000 | 2abb2695b9b6e94883553e101ea5edb8e1934e3919d62b14e589331fe9f5bcfc |
yes MEASURED |
kb_fwl_64.bin |
left | B | 328 880 | 07dd2523bf87ee1385fdfb4dbaacd57c8f928e3751306a3e63eac748b2454297 |
no |
kb_fwr_64.bin |
right | B | 226 000 | 87f63fd3be514538f099803c0912516a54bbb256facf2c50df6c0c85c16a6677 |
no |
Every other release's hashes are on Versions and, in full,
in create-legacy-firmware's per-release manifest.json files.
How to name what a half runs
Put the half into its bootloader, read image state on the port that answers SMP, and look up
the slot 0 hash in the table. Slot 1 holds the image the last update replaced. The procedure and
the read-only probe are on Bootloader.
Other image families¶
d_fw.bin (NayaFlow 0.1.0 to 1.6.10) is a separate MCUboot image family: encrypted,
img_size 175 136, MCUboot length 176 244, in a 331 776-byte resource, signed with the same KEYHASH
and carrying the same pre-armed trailer STATIC1. create-legacy-firmware calls it the "dial" image. It arrived
in 0.1.0 together with dongle upgrade commands (mcb_dongle_*, dongle_upgrade_mcb_*) and the
vendor's slot-5 reservation for the dongle, so it may be dongle firmware INFERRED715.
Which one it is stays open OPEN (open question). It is not the
module firmware.
Module firmware is not an MCUboot image at all. From NayaFlow 1.11.0 it ships as
FlashMemory.bin, a 1 MiB LittleFS image with no MCUboot magic, no TLVs and no trailer, holding
encrypted .sfb module apps (see Module firmware) STATIC1. Whether any key signs
the .sfb files is open OPEN (open question).
Custom firmware: consequences only¶
This section states consequences. It does not describe, and this site does not publish, any method to read, bypass or extract keys.
Self-built firmware cannot boot through the stock bootloader: MCUboot verifies the RSA-2048-PSS signature against Naya's key before it swaps an image in. The two routes are Naya's signing key or replacing the bootloader over SWD STATIC DOC71.
What an SWD mass erase would cost: the carved stock images are encrypted, and the only key that unwraps them is inside the stock bootloader. An erase (the only SWD action available if the debug port is locked) removes that key, so the stock images could never be restored from the archive afterwards INFERRED. Whether shipped halves have the debug port locked (APPROTECT) is open OPEN (details).
Beta-channel images¶
The beta channel's images are carved into create-legacy-firmware's firmware-history-beta/ folder
(commit 7b511ca). The three keyboard firmwares that shipped only on the beta channel, 3.39.4, 3.40.0
and 3.40.4, are stored as kb_fwl.bin and kb_fwr.bin pairs: img_size 327 008 to 327 808 (left)
and 224 464 to 225 024 (right), generation A only, the same KEYHASH, the same header placeholder and
the same pre-armed permanent-swap trailer. Beta 1.10.0 carries no image. The beta tree is kept apart
from firmware-history/, which is the only tree OpenFlow's firmware fetcher and catalog read
STATIC32. Per-version sizes and hashes are on Versions.
The beta carve found nothing that changes a statement on this page about the stable channel.
How this was established¶
The archive was built from the public installers, and anyone can re-derive it: find the
MCUboot magic 0x96f3b83d in the NayaCore binary (header size 32 or 512), walk the TLVs, and bind Qt
RCC resource names to blobs (a name entry is a 16-bit length, a 32-bit hash and a UTF-16BE name; a
file node holds the name offset as a big-endian 32-bit value at +0 and the data offset at +10; the
data is preceded by a big-endian 32-bit length). create-legacy-firmware's tools/carve_fw.py and
tools/extract_history.py do this for every release STATIC4. The images themselves are hosted in
create-legacy-firmware, whose Apache-2.0 license covers its own documents, manifests and tools, not the vendor
firmware images or installers it archives. This page publishes structure, sizes and hashes only.
Open questions¶
- OPEN What physically distinguishes generation B, and how a generation-B half behaves (details).
- OPEN Whether
d_fw.binis dial or dongle firmware (details). - OPEN The module MCU's bootloader, and whether any key signs the
.sfbapps (details). - OPEN Whether this bootloader accepts a signed but unencrypted image; untestable without the key (details).
- OPEN Whether shipped halves have the debug port locked (APPROTECT) (details).
- OPEN The firmware versions of the images in NayaFlow 0.0.1 to 1.11.11, whose headers carry only the placeholder (details).
Sources¶
-
create-legacy-firmware,
FIRMWARE-HISTORY.md(catalog of every carved image with sizes, generations and plaintext hashes) and the per-releasefirmware-history/<version>/manifest.jsonfiles. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
create-legacy-firmware,
FIRMWARE-HISTORY.md, "Beta channel". ↩ -
create-legacy-firmware,
firmware-history-beta/MANIFEST.json(commit 7b511ca): every beta release and every image it carried, withimg_size, plaintext SHA-256, KEYHASH, header version and trailer. ↩↩↩↩↩↩ -
create-legacy-firmware,
tools/carve_fw.pyandtools/extract_history.py. ↩↩↩↩ -
create-legacy-firmware,
FLASHING-PROCEDURE.md, "Firmware bundle layout". ↩↩↩ -
create-legacy-firmware,
FLASHING-PROCEDURE.md, "Native service binary". ↩ -
create-legacy-firmware,
FLASHING-PROCEDURE.md, "Bottom line" and "Command vocabulary by era" (L140-L154). ↩↩↩ -
create-legacy-firmware,
FLASHING-PROCEDURE.md, "Slot ids, vendor-exact". ↩ -
create-legacy-firmware,
FLASHING-PROCEDURE.md, "The resource is a whole slot, trailer included" (flow corrected in commit 6ef80e2). ↩↩↩ -
create-legacy-firmware,
FLASHING-PROCEDURE.md, "Product ids, vendor-exact" (NayaCore 6.11.0,Naya_Device::setCreateFlashGenerationFromPid). ↩↩ -
create-legacy-firmware,
FLASHING-PROCEDURE.md, "Measured on hardware, both halves (2026-09-20)" (added in commit cdd897c). ↩↩↩ -
create-legacy-firmware,
SECRET-HUNT.md, "Findings" sections 1 to 3 (encryption flag, entropy, per-image wrapped keys, no key material in any installer). ↩↩↩↩ -
NayaFlow 1.25.1, main-process bundle:
NAYA_CREATE_FW_VERSION"3.41.0" andNAYA_MODULE_FW_VERSION"2.3.3". ↩ -
create-legacy-firmware,
CHANGELOG.mdL113 (vendor release notes, NayaFlow 1.25.0, NayaCore 6.11.0). ↩ -
create-legacy-firmware,
CHANGELOG.mdL453-L455 (vendor release notes: dongle support preparation, slot reservation). ↩ -
Vendor release notes of the beta channel, NayaTech/NayaFlow-beta-releases, v1.25.0. ↩
-
MCUboot source: image format and encryption design,
bootutil_public.c(swap table) andboot_serial.c. ↩↩↩