Skip to content

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, _64 names 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.bin is dial or dongle firmware (details).
  • OPEN The module MCU's bootloader, and whether any key signs the .sfb apps (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


  1. create-legacy-firmware, FIRMWARE-HISTORY.md (catalog of every carved image with sizes, generations and plaintext hashes) and the per-release firmware-history/<version>/manifest.json files. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  2. create-legacy-firmware, FIRMWARE-HISTORY.md, "Beta channel". ↩

  3. create-legacy-firmware, firmware-history-beta/MANIFEST.json (commit 7b511ca): every beta release and every image it carried, with img_size, plaintext SHA-256, KEYHASH, header version and trailer. ↩↩↩↩↩↩

  4. create-legacy-firmware, tools/carve_fw.py and tools/extract_history.py. ↩↩↩↩

  5. create-legacy-firmware, FLASHING-PROCEDURE.md, "Firmware bundle layout". ↩↩↩

  6. create-legacy-firmware, FLASHING-PROCEDURE.md, "Native service binary". ↩

  7. create-legacy-firmware, FLASHING-PROCEDURE.md, "Bottom line" and "Command vocabulary by era" (L140-L154). ↩↩↩

  8. create-legacy-firmware, FLASHING-PROCEDURE.md, "Slot ids, vendor-exact". ↩

  9. create-legacy-firmware, FLASHING-PROCEDURE.md, "The resource is a whole slot, trailer included" (flow corrected in commit 6ef80e2). ↩↩↩

  10. create-legacy-firmware, FLASHING-PROCEDURE.md, "Product ids, vendor-exact" (NayaCore 6.11.0, Naya_Device::setCreateFlashGenerationFromPid). ↩↩

  11. create-legacy-firmware, FLASHING-PROCEDURE.md, "Measured on hardware, both halves (2026-09-20)" (added in commit cdd897c). ↩↩↩

  12. create-legacy-firmware, SECRET-HUNT.md, "Findings" sections 1 to 3 (encryption flag, entropy, per-image wrapped keys, no key material in any installer). ↩↩↩↩

  13. NayaFlow 1.25.1, main-process bundle: NAYA_CREATE_FW_VERSION "3.41.0" and NAYA_MODULE_FW_VERSION "2.3.3". ↩

  14. create-legacy-firmware, CHANGELOG.md L113 (vendor release notes, NayaFlow 1.25.0, NayaCore 6.11.0). ↩

  15. create-legacy-firmware, CHANGELOG.md L453-L455 (vendor release notes: dongle support preparation, slot reservation). ↩

  16. Vendor release notes of the beta channel, NayaTech/NayaFlow-beta-releases, v1.25.0. ↩

  17. MCUboot source: image format and encryption design, bootutil_public.c (swap table) and boot_serial.c. ↩↩↩