<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Quarkslab's blog - Xiaomi</title><link href="http://blog.quarkslab.com/" rel="alternate"></link><link href="http://blog.quarkslab.com/feeds/xiaomi.rss.xml" rel="self"></link><id>http://blog.quarkslab.com/</id><updated>2026-06-18T00:00:00+02:00</updated><entry><title>Black Box Probing: a Security Analysis of Xiaomi's MJA1 Secure Chip</title><link href="http://blog.quarkslab.com/black-box-probing-a-security-analysis-of-xiaomis-mja1-secure-chip.html" rel="alternate"></link><published>2026-06-18T00:00:00+02:00</published><updated>2026-06-18T00:00:00+02:00</updated><author><name>Mengsi Wu</name></author><id>tag:blog.quarkslab.com,2026-06-18:/black-box-probing-a-security-analysis-of-xiaomis-mja1-secure-chip.html</id><summary type="html">&lt;p&gt;Xiaomi's MJA1 is a proprietary secure chip used in their recent cameras to protect sensitive data and device communications. With no public documentation available, we conducted a black-box security analysis covering hardware identification, I2C sniffing, flash dumping, and firmware reverse engineering. This post walks through how we mapped the chip's command protocol, brute-forced undocumented commands, and assessed its security properties.&lt;/p&gt;</summary><content type="html">&lt;h2 id="discovery-of-mja1-secure-chip"&gt;Discovery of MJA1 secure chip&lt;/h2&gt;
&lt;p&gt;When navigating on Xiaomi's online store to find a new R&amp;amp;D target, we ended up on a description of the MJA1 secure chip, available on their recent cameras.&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/official_desc.png" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;This chip claims to provide hardware-level protection for sensitive data and device communications. It is designed to be resistant to a variety of common attacks, including replay attacks, MITM attacks, brute force, ... And each chip has its own private key and certificate.&lt;/p&gt;
&lt;p&gt;However, it is a proprietary chip: no documentation or datasheet is present on the Internet.
At the time of writing, no public research work has been done on it, only a picture from RoboCoffee&lt;sup id="fnref:RBCF"&gt;&lt;a class="footnote-ref" href="#fn:RBCF"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/robocoffee.png" width="70%"/&gt;&lt;/p&gt;
&lt;p&gt;With no public information available, our goal for this project was to understand how the MJA1 secure chip works and evaluate its security properties through black-box analysis.&lt;/p&gt;
&lt;h2 id="target-identification"&gt;Target identification&lt;/h2&gt;
&lt;p&gt;The first step was to find a target satisfying the following criteria:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Integrating the MJA1 secure chip;&lt;/li&gt;
&lt;li&gt;Reasonable price (&amp;lt; 50-100&amp;euro;);&lt;/li&gt;
&lt;li&gt;Allowing firmware upgrades interception, via mobile app for example, in case we fail to dump the firmware.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We ended up purchasing two devices for the price of one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.mi.com/global/product/xiaomi-outdoor-camera-bw300/"&gt;Xiaomi Outdoor Camera BW300 (60&amp;euro;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mi.com/global/product/xiaomi-outdoor-camera-base-station/"&gt;Xiaomi Camera Base Station (30&amp;euro;)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They both have a MJA1 secure chip integrated and can interact together. This way, we can compare two different devices, check for the easiest target, and cross-validate our findings.&lt;/p&gt;
&lt;h2 id="hardware-analysis"&gt;Hardware analysis&lt;/h2&gt;
&lt;h3 id="mja1-secure-chip-identification"&gt;MJA1 secure chip identification&lt;/h3&gt;
&lt;p&gt;After opening both devices and inspecting their PCBs, we located the MJA1 chip on each board.
Both devices use the same &lt;code&gt;MJA1 C06CW&lt;/code&gt; model.&lt;/p&gt;
&lt;p&gt;The following picture shows the MJA1 chip on the BW300 camera.&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/camera_bw300_pcb.jpg" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;The chip comes in a &lt;em&gt;DFN 2&amp;times;3-8&lt;/em&gt; package, which means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dual Flat No-lead (DFN);&lt;/li&gt;
&lt;li&gt;Size: 2 &amp;times; 3 mm 🤏;&lt;/li&gt;
&lt;li&gt;8 pins.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/tdfn8.jpg" width="30%"/&gt;&lt;/p&gt;
&lt;p&gt;To identify the communication interface, we had to actively probe the chip while the device was running.&lt;/p&gt;
&lt;h3 id="sniffing-communications"&gt;Sniffing communications&lt;/h3&gt;
&lt;p&gt;To observe the communication between the main SoC and the MJA1 chip, we used a PCBite setup combined with a logic analyzer. The PCBite probes can easily fit onto the 0.5 mm pin pitch of the chip.&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/pcbite_sniffing.jpg" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;We then powered the camera base station and started capturing signals on the pins of the chip. The goal at this stage was simple: identify which pins carried data, and from there, figure out what protocol was used.&lt;/p&gt;
&lt;p&gt;Looking at the eight pins, most of them stayed flat, which may indicate VCC/pull-ups (constant high) or GND (constant low). Only two showed active data exchange:&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/i2c_logic_analyzer0.png" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/mjac_two_pins.png" width="45%"/&gt;&lt;/p&gt;
&lt;p&gt;In the embedded world, most common interfaces have characteristic pin counts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;UART&lt;/em&gt; uses two pins (TX, RX), but both are active in different directions and in an asynchronous way;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;SPI&lt;/em&gt; needs at least four pins (CLK, MOSI, MISO, CS), which does not correspond to our case;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I2C&lt;/em&gt; uses exactly two pins (SCL, SDA), shared bidirectionally between master and slave, synchronously.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zooming in on the captures revealed the typical signature of an I2C bus, with a regular clock line and a data line:&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/i2c_logic_analyzer.png" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;By applying the built-in I2C decoder of the logic analyzer, we could read the transaction: &lt;code&gt;Write [0x2A]&lt;/code&gt; to address the chip, then a sequence of data bytes: &lt;code&gt;0x05&lt;/code&gt;, &lt;code&gt;0x00&lt;/code&gt;, &lt;code&gt;0x03&lt;/code&gt;, &lt;code&gt;0x00&lt;/code&gt;, &lt;code&gt;0x02&lt;/code&gt;, &lt;code&gt;0x00&lt;/code&gt;, &lt;code&gt;0x08&lt;/code&gt;, &lt;code&gt;0x58&lt;/code&gt;, &lt;code&gt;0xEF&lt;/code&gt;, each acknowledged by the slave.&lt;/p&gt;
&lt;p&gt;From there, we could conclude:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Communication with the secure chip uses the &lt;strong&gt;I2C&lt;/strong&gt; protocol;&lt;/li&gt;
&lt;li&gt;The clock (SCL) defines when data is read; the data line (SDA) carries the bits;&lt;/li&gt;
&lt;li&gt;The address of the secure chip is &lt;strong&gt;0x2A&lt;/strong&gt; &amp;mdash; I2C being a shared bus, each device on it is identified by such an ID.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/mjac_chip_pinout.png" width="45%"/&gt;&lt;/p&gt;
&lt;p&gt;As a side note, the exact byte sequence captured above (&lt;code&gt;0x05 0x00 0x03 0x00 0x02 0x00 0x08 0x58 0xEF&lt;/code&gt;) is in fact a &lt;code&gt;READ&lt;/code&gt; command, the very same one we will decode in detail later in this post. &lt;/p&gt;
&lt;p&gt;Sniffing gave us the transport layer, but the actual command structure on top of I2C was still unknown. To understand it, we needed to extract and analyze the firmware of the host device that talks to the chip.&lt;/p&gt;
&lt;h3 id="flash-dumping"&gt;Flash dumping&lt;/h3&gt;
&lt;p&gt;We had two devices to work with, each with a different flash chip.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Xiaomi Base Station&lt;/strong&gt; has an &lt;code&gt;MD25Q128&lt;/code&gt; flash memory, supported by the TNM5000 universal programmer we had. The dump was therefore straightforward.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Xiaomi Outdoor Camera BW300&lt;/strong&gt; was equipped with a Winbond &lt;code&gt;25N01KVZEIR&lt;/code&gt; flash memory. This chip was not supported by the TNM5000, so we had to find another way.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;25N01KVZEIR&lt;/code&gt; is a 1 Gbit SPI NAND flash memory, with a WSON 8x6 mm package. Its pinout can be found from the &lt;a href="https://www.winbond.com/hq/support/documentation/downloadV2022.jsp?__locale=en&amp;amp;xmlPath=/support/resources/.content/item/DA00-W25N01KV.html&amp;amp;level=1"&gt;official datasheet&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/winbond-flash-pinout.png" width="60%"/&gt;&lt;/p&gt;
&lt;p&gt;To dump unsupported SPI NAND flash, a Raspberry Pi can often be a good option. It can speak SPI directly to the chip to identify it, read raw pages, and dump the entire NAND.&lt;/p&gt;
&lt;p&gt;The following pins of the Raspberry Pi can be used:&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/raspberry-spi-connected-pins.png" width="70%"/&gt;&lt;/p&gt;
&lt;p&gt;The pins can be connected like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Winbond flash pin&lt;/th&gt;
&lt;th&gt;Raspberry Pi pin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/CS&lt;/td&gt;
&lt;td&gt;GPIO 7 (SPI0 CE1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DO&lt;/td&gt;
&lt;td&gt;GPIO 9 (SPI0 MISO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/WP&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI&lt;/td&gt;
&lt;td&gt;GPIO10 (SPI0 MOSI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLK&lt;/td&gt;
&lt;td&gt;GPIO11 (SPI0 SCLK)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/HOLD&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VCC&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We built a custom setup based on a Raspberry Pi 3 B+, wiring the flash chip directly to the Pi's GPIO pins with a breadboard and Dupont cables.&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/memory_dump_over_spi1.jpg" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;We first read the JEDEC ID of the chip with the &lt;code&gt;spidev&lt;/code&gt; Python module:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;spidev&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;spi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spidev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpiDev&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# bus 0, device 1 (/dev/spidev0.1)&lt;/span&gt;
&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_speed_hz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;
&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# Send JEDEC ID read command (0x9F)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xfer2&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mh"&gt;0x9F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"JEDEC ID:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The ID obtained is good, according to the specification:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;JEDEC ID: ['0x0', '0x0', '0xef', '0xae', '0x21', '0x0', '0x0', '0x0']
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can now proceed with dumping the entire flash binary:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;spidev&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;

&lt;span class="c1"&gt;# === Configuration ===&lt;/span&gt;
&lt;span class="n"&gt;MAX_PAGE_NUMBER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x10000&lt;/span&gt;
&lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="c1"&gt;# Size of data area (without spare/OOB)&lt;/span&gt;
&lt;span class="n"&gt;SPI_BUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;SPI_DEVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# === Setup SPI ===&lt;/span&gt;
&lt;span class="n"&gt;spi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spidev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpiDev&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPI_BUS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SPI_DEVICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_speed_hz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10_000_000&lt;/span&gt;
&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mb"&gt;0b00&lt;/span&gt;

&lt;span class="c1"&gt;# === Helper: Wait until NAND is ready ===&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_ready&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xfer2&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mh"&gt;0x0F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xC0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;        &lt;span class="c1"&gt;# 0x0F: Read status, 0xC0: Status register&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readbytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;# Bit 0 == 0 means "Ready"&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# === 1. Load page into cache using command 0x13 ===&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_page_to_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_number&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'big'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 24-bit address&lt;/span&gt;
    &lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xfer2&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mh"&gt;0x13&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;wait_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# === 2. Read data from cache using 0x03 command ===&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_cache_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'big'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xfer2&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mh"&gt;0x03&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dump.bin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"wb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_PAGE_NUMBER&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Reading page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;load_page_to_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;page_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_cache_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With both firmwares extracted, we could move on to the reverse engineering phase.&lt;/p&gt;
&lt;h2 id="firmware-analysis_1"&gt;Firmware analysis&lt;/h2&gt;
&lt;h3 id="reverse-engineering-miio_client"&gt;Reverse engineering &lt;code&gt;miio_client&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Using &lt;code&gt;binwalk&lt;/code&gt;, &lt;code&gt;unblob&lt;/code&gt;, and &lt;code&gt;SquashFS&lt;/code&gt; tools, we extracted the file systems from both firmwares. As expected, no code specific to the secure chip itself was found, the chip runs its own firmware, which is not exposed to the host.&lt;/p&gt;
&lt;p&gt;Our next goal was to find, somewhere in the extracted file system, the host-side code that talks to the chip over I2C. &lt;/p&gt;
&lt;p&gt;By looking at the &lt;code&gt;strings&lt;/code&gt;, we found many references to the prefix &lt;code&gt;mjac&lt;/code&gt; (MJA Chip?) inside &lt;strong&gt;&lt;code&gt;miio_client&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;strings&lt;span class="w"&gt; &lt;/span&gt;mi_ot/miio_client
...
mjac_reset
mjac_get_did
mjac_get_certificate_pem
mjac_crc16_ccitt
mjac_i2c
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;MIIO refers to a proprietary set of communication protocols and SDK developed by Xiaomi for its IoT ecosystem. This binary is the one in charge of communicating with the MJA1 chip over I2C.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;miio_client&lt;/code&gt; is a MIPS 32-bit binary, dynamically linked against uClibc, typical for an embedded Linux device:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;mi_ot/miio_client
mi_ot/miio_client:&lt;span class="w"&gt; &lt;/span&gt;ELF&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;-bit&lt;span class="w"&gt; &lt;/span&gt;LSB&lt;span class="w"&gt; &lt;/span&gt;executable,&lt;span class="w"&gt; &lt;/span&gt;MIPS,&lt;span class="w"&gt; &lt;/span&gt;MIPS32&lt;span class="w"&gt; &lt;/span&gt;rel2&lt;span class="w"&gt; &lt;/span&gt;version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;SYSV&lt;span class="o"&gt;)&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;dynamically&lt;span class="w"&gt; &lt;/span&gt;linked,&lt;span class="w"&gt; &lt;/span&gt;interpreter&lt;span class="w"&gt; &lt;/span&gt;/lib/ld-uClibc.so.0,&lt;span class="w"&gt; &lt;/span&gt;no&lt;span class="w"&gt; &lt;/span&gt;section&lt;span class="w"&gt; &lt;/span&gt;header
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;no section header&lt;/code&gt; part means the binary has been stripped of its section headers, which usually makes reverse engineering harder. Luckily for us, the function names themselves had been preserved in the symbol table.&lt;/p&gt;
&lt;p&gt;By reverse engineering this binary, we identified several relevant functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mjac_init&lt;/code&gt;: initializes the I2C interface;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mjac_cmd_build_&amp;lt;cmd&amp;gt;&lt;/code&gt;: builds different supported commands;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mjac_resp_parse_data&lt;/code&gt;: parses responses from the chip;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mjac_crc16_ccitt&lt;/code&gt;: computes the CRC16 used for frame integrity.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="identified-commands"&gt;Identified commands&lt;/h4&gt;
&lt;p&gt;By analyzing the &lt;code&gt;mjac_cmd_build_*&lt;/code&gt; family of functions, we mapped each command ID to its purpose:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;Echo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x02&lt;/td&gt;
&lt;td&gt;Generate random&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x05&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x0D&lt;/td&gt;
&lt;td&gt;Hibernate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x11&lt;/td&gt;
&lt;td&gt;Generate key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x14&lt;/td&gt;
&lt;td&gt;Query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x16&lt;/td&gt;
&lt;td&gt;Generate signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x17&lt;/td&gt;
&lt;td&gt;Verify signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x18&lt;/td&gt;
&lt;td&gt;Establish key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These commands fall into three categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Utility commands:&lt;/strong&gt; &lt;code&gt;Echo&lt;/code&gt; for communication testing, &lt;code&gt;Generate random&lt;/code&gt; for cryptographically secure random numbers, &lt;code&gt;Hibernate&lt;/code&gt; to enter low-power mode;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data access commands:&lt;/strong&gt; &lt;code&gt;Read&lt;/code&gt; to read from specific data zones, &lt;code&gt;Query&lt;/code&gt; to retrieve chip information such as serial number or product version;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cryptographic operations:&lt;/strong&gt; &lt;code&gt;Generate key&lt;/code&gt; (ephemeral ECC keypair for ECDH), &lt;code&gt;Establish key&lt;/code&gt; (shared secret derivation), &lt;code&gt;Generate signature&lt;/code&gt;, &lt;code&gt;Verify signature&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="understanding-a-command"&gt;Understanding a command&lt;/h4&gt;
&lt;p&gt;Building the table above was less straightforward than it might look. Even with the function names preserved, reconstructing the exact format of each command was not trivial: each &lt;code&gt;mjac_cmd_build_&amp;lt;cmd&amp;gt;&lt;/code&gt; function packs its arguments into a structure of magic offsets, computes a CRC over a specific range, and returns a length that is not always the same.&lt;/p&gt;
&lt;p&gt;Take &lt;code&gt;mjac_cmd_build_establish_key&lt;/code&gt; as an example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mjac_cmd_build_establish_key&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__mjac_cmd_establish_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_cmd_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_src_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ushort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="mh"&gt;0x0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_src_len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x45&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__mjac_cmd_establish_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="mh"&gt;0x0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x48&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_cmd_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;field0_0x0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x18&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;field1_0x1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;___key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x45&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mjac_crc16_ccitt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;field0_0x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x47&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_crc16_lo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;_cmd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_crc16_hi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x49&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Reading this carefully reveals quite a bit about the command format:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first byte (&lt;code&gt;0x18&lt;/code&gt;) is the command ID, confirming that &lt;strong&gt;Establish key&lt;/strong&gt; is command 0x18;&lt;/li&gt;
&lt;li&gt;The second byte (&lt;code&gt;0xff&lt;/code&gt;) is a constant with unclear meaning;&lt;/li&gt;
&lt;li&gt;The next &lt;code&gt;0x45&lt;/code&gt; bytes are the actual command payload, copied from the caller's buffer;&lt;/li&gt;
&lt;li&gt;The CRC16 is computed over the first &lt;code&gt;0x47&lt;/code&gt; bytes, and appended as two little-endian bytes;&lt;/li&gt;
&lt;li&gt;The total frame length is &lt;code&gt;0x49&lt;/code&gt; bytes: 1 (ID) + 1 (constant) + 0x45 (payload) + 2 (CRC).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Doing this carefully for each command was the only reliable way to recover the protocol.&lt;/p&gt;
&lt;h4 id="rebuilding-the-crc"&gt;Rebuilding the CRC&lt;/h4&gt;
&lt;p&gt;The integrity of every command is protected by &lt;code&gt;mjac_crc16_ccitt&lt;/code&gt;. We needed a working implementation of it before we could send a single valid frame to the secure chip. The function name suggested a standard CRC-16/CCITT variant, but there are many of those in the wild, differing in four parameters: polynomial, initial value, whether input and output bits are reflected, and the final XOR. &lt;/p&gt;
&lt;p&gt;By reading the decompiled function, we identified all these parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The shift-and-XOR pattern, combined with a 256-entry lookup table, pointed to a &lt;strong&gt;reflected&lt;/strong&gt; CRC implementation (low byte first, right shift on the register);&lt;/li&gt;
&lt;li&gt;The lookup table itself encoded a polynomial of &lt;code&gt;0x8408&lt;/code&gt;, which is the bit-reversed form of &lt;code&gt;0x1021&lt;/code&gt;, the canonical CCITT polynomial;&lt;/li&gt;
&lt;li&gt;The register was initialized to &lt;code&gt;0xFFFF&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;The final result was bitwise inverted before being returned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This corresponds to the &lt;a href="https://reveng.sourceforge.io/crc-catalogue/16.htm"&gt;CRC-16/X-25&lt;/a&gt; (also known as CRC-16/IBM-SDLC) variant. It is a standard preset but specifically not the one most people reach for when they hear "CRC-16/CCITT", which is why we had to derive it from the code rather than guess it.&lt;/p&gt;
&lt;p&gt;We reimplemented it in C and used it later on a rooted camera to generate valid frames:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;crc16_table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mh"&gt;0x0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x1189&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x2312&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x329B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x4624&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x57AD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x6536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x74BF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mh"&gt;0x8C48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x9DC1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xAF5A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xBED3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xCA6C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xDBE5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xE97E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xF8F7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* ... 240 more entries ... */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mh"&gt;0x7BC7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x6A4E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x58D5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x495C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x3DE3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x2C6A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x1EF1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0F78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;crc16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;crc16_table&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id="cross-validation-via-public-firmware"&gt;Cross-validation via public firmware&lt;/h4&gt;
&lt;p&gt;Only after having done all of the above, while searching for additional references to these function names starting with &lt;code&gt;mjac_cmd_build_&lt;/code&gt;, we stumbled upon a public GitHub repository: &lt;a href="https://github.com/iomonad/handshow-firmware"&gt;&lt;code&gt;github.com/iomonad/handshow-firmware&lt;/code&gt;&lt;/a&gt;. It belongs to a third-party device that bundles Xiaomi's Mijia BLE SDK, and it contains MJA1 wrapper source code as part of the &lt;a href="https://github.com/iomonad/handshow-firmware/tree/master/components/vendor/common/mijia_ble/libs/cryptography/mja1"&gt;cryptography library&lt;/a&gt;: &lt;code&gt;mjac_wrapper.c|h&lt;/code&gt; and &lt;code&gt;mjac_defs.h&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Finding it earlier would have saved us a lot of effort, but still, this source code confirmed our command list and provided additional details on command arguments and response codes that filled in the last gaps in our understanding.&lt;/p&gt;
&lt;h3 id="command-format"&gt;Command format&lt;/h3&gt;
&lt;h4 id="read-command"&gt;Read command&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;Read&lt;/code&gt; command (0x05), visible in our earlier I2C capture, is a good example to illustrate the general command/response structure.&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/read_cmd.png" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;It reads data from a selected zone, with a given offset and length, and requires a CRC16 for validation. The command parameters are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Index&lt;/b&gt; &amp;rarr; selects the data zone:&lt;/li&gt;
&lt;ul&gt;
&lt;li&gt;0: Device certificate&lt;/li&gt;
&lt;li&gt;1: Manufacturer certificate&lt;/li&gt;
&lt;li&gt;2: Root certificate&lt;/li&gt;
&lt;li&gt;3: Product data&lt;/li&gt;
&lt;li&gt;4: User data&lt;/li&gt;
&lt;/ul&gt;
&lt;li&gt;&lt;b&gt;Offset&lt;/b&gt; &amp;rarr; starting position within the selected zone;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Length&lt;/b&gt; &amp;rarr; number of bytes to read (capped because the response cannot exceed 512 bytes).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that all three certificates available here are &lt;em&gt;public&lt;/em&gt; certificates, the corresponding private keys are never exposed over I2C. The host can read the chip's identity and verify its certificate chain, but the private key material stays inside the chip and is only used internally by the cryptographic commands (&lt;code&gt;Generate signature&lt;/code&gt;, &lt;code&gt;Establish key&lt;/code&gt;). This is exactly the trust boundary a secure element is meant to enforce.&lt;/p&gt;
&lt;p&gt;We tried to read outside of these defined zones, but the chip consistently returned an error in those cases.&lt;/p&gt;
&lt;h4 id="command-response"&gt;Command response&lt;/h4&gt;
&lt;p&gt;All commands return a response with the following structure:&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/mjac_response.png" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;The response status can take several values, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x00&lt;/code&gt; &amp;rarr; OK&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x01&lt;/code&gt; &amp;rarr; Invalid CRC&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x02&lt;/code&gt; &amp;rarr; Invalid arguments&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x04&lt;/code&gt; &amp;rarr; Unsupported command&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x06&lt;/code&gt; &amp;rarr; Length too large&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To make this concrete, here is an actual exchange we observed for a &lt;code&gt;Read&lt;/code&gt; command:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Request:&lt;/strong&gt; &lt;code&gt;READ (Index=3, Offset=2, Length=8, CRC=0x58EF)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raw response:&lt;/strong&gt; &lt;code&gt;[ 00 | 000A | 00 00 00 00 41 FF E5 6F | 71C8 ]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decoded response:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Status: OK (&lt;code&gt;0x00&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;Length: &lt;code&gt;0x000A&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Product ID: &lt;code&gt;0x41FFE56F&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;CRC: &lt;code&gt;0x71C8&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This command reads the product ID of the device.&lt;/p&gt;
&lt;h2 id="testing_1"&gt;Testing&lt;/h2&gt;
&lt;p&gt;Until now we had only passively sniffed the communication with the secure chip. To go further, we needed to actively send our own commands.&lt;/p&gt;
&lt;h3 id="testing-setup"&gt;Testing setup&lt;/h3&gt;
&lt;p&gt;For this, we used a third device that also embeds the MJA1 chip: the &lt;a href="https://www.mi.com/global/product/xiaomi-smart-camera-c301/"&gt;Xiaomi Camera C301&lt;/a&gt;, on which we had root access (see acknowledgments).&lt;/p&gt;
&lt;p&gt;We developed a small C program that interacts directly through Linux's I2C userspace interface. It opens &lt;code&gt;/dev/i2c-0&lt;/code&gt;, sets the slave address with an &lt;code&gt;ioctl(fd, I2C_SLAVE, 0x2A)&lt;/code&gt;, then issues plain &lt;code&gt;write()&lt;/code&gt; and &lt;code&gt;read()&lt;/code&gt; syscalls to exchange frames.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dev/i2c-0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;O_RDWR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;I2C_SLAVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x2A&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cmd_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;resp_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Around this I/O core, we built a small dispatch table mapping each command name to a dedicated builder function (&lt;code&gt;cmd_build_read&lt;/code&gt;, &lt;code&gt;cmd_build_update&lt;/code&gt;, &lt;code&gt;cmd_build_generate_signature&lt;/code&gt;, and so on). Each builder takes the high-level arguments, packs them into the format we had reverse engineered, and appends the CRC. This gave us a clean CLI for interactive testing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;./mjac_send&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# READ zone=3, offset=2, length=8&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;./mjac_send&lt;span class="w"&gt; &lt;/span&gt;query&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="c1"&gt;# query chip info&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;./mjac_send&lt;span class="w"&gt; &lt;/span&gt;custom&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;130000&lt;/span&gt;...&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# send arbitrary frame for fuzzing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The program was cross-compiled using a Buildroot toolchain to match the camera's architecture and copied to the device over SSH.&lt;/p&gt;
&lt;p&gt;One important limitation we ran into: &lt;strong&gt;after each command we sent, the chip became unresponsive and required a full device reboot to talk again&lt;/strong&gt;. This did not happen during passive sniffing, suggesting that the issue is specific to active interaction.&lt;/p&gt;
&lt;p&gt;We spent a fair amount of time trying to track it down. We tweaked our sending code in several ways: changing how the I2C transactions were issued through the kernel interface, splitting or combining read/write phases, adjusting delays between them, and captured each variant with the logic analyzer to compare it against captures of legitimate &lt;code&gt;miio_client&lt;/code&gt; exchanges. Even with the same pattern, the chip still refused to talk after one successful transaction.&lt;/p&gt;
&lt;p&gt;We never figured out the root cause. Despite this limitation, we could still send commands one at a time and observe the responses.&lt;/p&gt;
&lt;h3 id="brute-forcing-command-ids"&gt;Brute-forcing command IDs&lt;/h3&gt;
&lt;p&gt;With an active testing setup in place, the next natural step was to look for &lt;strong&gt;undocumented or hidden commands&lt;/strong&gt; that &lt;code&gt;miio_client&lt;/code&gt; never references.&lt;/p&gt;
&lt;p&gt;We iterated over all possible command IDs (0x00 to 0xFF), sent each one to the chip with a valid CRC, and observed the response code. Commands returning "Unsupported command" (0x04) were considered absent, while any other response was worth investigating.&lt;/p&gt;
&lt;p&gt;To overcome the unresponsive chip limitation, we wrote a small Python controller running on our laptop that automates the full cycle for each command ID:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Connect to the camera over SSH;&lt;/li&gt;
&lt;li&gt;Run our C testing program with the candidate command ID;&lt;/li&gt;
&lt;li&gt;Capture the response and log it;&lt;/li&gt;
&lt;li&gt;If the response indicates the chip is in its unresponsive state, issue a &lt;code&gt;reboot&lt;/code&gt; over SSH;&lt;/li&gt;
&lt;li&gt;Wait for the device to come back online (by repeatedly trying to open an SSH connection);&lt;/li&gt;
&lt;li&gt;Move on to the next command ID.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This revealed two additional commands that were not in the list extracted from &lt;code&gt;miio_client&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;0x06 &amp;rarr; Update&lt;/strong&gt;: writes data to a zone (see below);&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0x13 &amp;rarr; Unknown&lt;/strong&gt;: always returns an undocumented error code &lt;code&gt;0x0F&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Command &lt;code&gt;0x13&lt;/code&gt; is the more intriguing of the two. It does not return "Unsupported command", but rather a custom error code, which suggests that the command exists and is partially handled, but that something prevents it from executing normally. It probably requires specific preconditions or arguments that we have not yet identified. Targeted argument fuzzing on this single command could be interesting for future work.&lt;/p&gt;
&lt;h3 id="update-command-0x06"&gt;Update command (0x06)&lt;/h3&gt;
&lt;p&gt;The name of this command might suggest something related to firmware update, but in reality, it is not related at all. The &lt;code&gt;Update&lt;/code&gt; command has the same structure as the &lt;code&gt;Read&lt;/code&gt; command previously seen:&lt;/p&gt;
&lt;p&gt;&lt;img class="align-center" src="resources/2026-06-18_xiaomi_mja1/update_cmd.png" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;It writes data to a selected zone at a chosen offset, with CRC16 validation. However, after extensive testing we found that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only the &lt;strong&gt;user data&lt;/strong&gt; zone (Index=4) can actually be modified. All other zones (certificates and product data) are read-only;&lt;/li&gt;
&lt;li&gt;The data length is limited by the I2C buffer size;&lt;/li&gt;
&lt;li&gt;The offset is also bounded by the size of the user data zone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, the chip enforces strong write protection on the zones that matter most: the device, manufacturer, and root certificates as well as product data are all immutable from the host side.&lt;/p&gt;
&lt;h2 id="conclusion-next-steps_1"&gt;Conclusion &amp;amp; next steps&lt;/h2&gt;
&lt;p&gt;Starting from a chip with zero public documentation, we identified its communication interface, dumped and reverse engineered the host-side firmware that talks to it, recovered the full format including its CRC variant, and brute-forced the command ID space to find two undocumented commands.&lt;/p&gt;
&lt;p&gt;From the host side, the chip behaves correctly: certificates and product data are read-only, private keys never cross the I2C bus, writes are restricted to a user data zone, and malformed arguments are properly rejected.
No obvious flaw at the protocol level &amp;mdash; but the protocol is only the front door.&lt;/p&gt;
&lt;p&gt;Several directions are now open for further research:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Desoldering the chip&lt;/strong&gt; to test it in isolation on a custom board powered by a microcontroller. This would remove the reboot-after-each-command limitation that hampered our active testing and enable proper &lt;strong&gt;fuzzing&lt;/strong&gt; of all commands and arguments, including a deeper look at the mysterious &lt;code&gt;0x13&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fault injection (voltage glitching, EM pulse, clock glitching).&lt;/strong&gt; The chip enforces strict access control on which zones can be read or written. What happens if a glitch is injected precisely when the chip is checking the zone index in a &lt;code&gt;Read&lt;/code&gt; command? Could the check be bypassed to read outside of the defined zones? For example, reaching the chip's internal memory, private keys, or firmware?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Side-channel analysis (power, EM).&lt;/strong&gt; The chip performs ECC operations as part of &lt;code&gt;Generate key&lt;/code&gt;, &lt;code&gt;Establish key&lt;/code&gt;, and &lt;code&gt;Generate signature&lt;/code&gt;. These are classic targets for power and electromagnetic side-channel attacks aiming to recover private key material.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The protocol-level analysis presented here is, in many ways, the easy part. The more interesting work begins once the chip can be physically isolated and analyzed through its analog side channels.&lt;/p&gt;
&lt;p&gt;We hope this post serves as a useful starting point for anyone interested in extending this work into practical side-channel analysis.&lt;/p&gt;
&lt;h2 id="acknowledgments"&gt;Acknowledgments&lt;/h2&gt;
&lt;p&gt;Many thanks to &lt;a href="https://github.com/AlxCzl"&gt;Alexandre Chazal&lt;/a&gt; for sharing root access on the Xiaomi Camera C301, which made the active testing phase possible. Thanks also to my colleagues for reviewing this blog post, providing valuable feedback, and sharing insightful tips during the research phase.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id="fn:RBCF"&gt;
&lt;p&gt;&lt;em&gt;Hacking Brightway scooters: A case study&lt;/em&gt;, RoboCoffee, February 22, 2023, &lt;a href="https://robocoffee.de/?p=436"&gt;https://robocoffee.de/?p=436&lt;/a&gt;&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:RBCF" title="Jump back to footnote 1 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="Reverse-Engineering"></category><category term="reverse-engineering"></category><category term="hardware"></category><category term="Xiaomi"></category><category term="MJA1"></category><category term="secure element"></category><category term="2026"></category></entry></feed>