<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Tom Tildavaan</title>
    <link>https://infosec.press/tildavaan/</link>
    <description>asdf</description>
    <pubDate>Sat, 20 Jun 2026 11:47:22 +0000</pubDate>
    <item>
      <title>More ATJ21XX stuff</title>
      <link>https://infosec.press/tildavaan/more-atj21xx-stuff</link>
      <description>&lt;![CDATA[I would like to report on what we have learned during our research into ATJ21XX-SoCs.&#xA;&#xA;Have you ever come across a device such as AGPTEK, WOLFANG, YOTON, or RUIZU? These devices seem to all be built by the same company. All of them support MP3/OGG/FLAC/AAC/APE formats, have the &#xA;same menu structures, and sometimes even may be capable of playing videos or count your steps.&#xA;&#xA;We have confirmed that RUIZU and AGPTEK are the same company. That&#39;s written right on the box, but many other players use the same chip, the ATJ2157 from Actions Semiconductor. These OEMs do not start with just the data sheet but instead use an SDK based on uC/OS-II.&#xA;&#xA;It is unfortunate that some of these devices are built so cheap - low-speed memory, a poor FM tuner, and random glitches in the OEM operating system lead to devices with little polish, given that these chips are very powerful.&#xA;&#xA;ATJ212X are MIPS-based and were found in your SanDisk Clip Sport and Jam devices as well as the RUIZU X02 (see Ruizu X02 Partial Disassembly and Notes). The data sheet &#xA;calls the available SRAM &#34;from ten to several hundred KB&#34;.&#xA;&#xA;ATJ215X are ARM Cortex M4F-based and are now used in almost all &#34;budget&#34; devices. CPU runs at 288MHz and has only 224KB RAM. This is less than the Raspberry Pi RP2040 with 256K.&#xA;&#xA;These chips are all-in-one SoCs - lithium-ion battery protection, microphone input, USB 2.0 interface, SPI and SD interfaces, NOR/NAND flash controller, many GPIO pins, stereo headphone output for headphones, I²S up to 192kHz.&#xA;&#xA;The SDK for the MIPS version was leaked - https://github.com/Suber/PD196ATJ2127, and we can look into the wonders of UI built on an RTOS. Apart from data sheets and pinouts, we have found nothing for the ARM variant, which is unfortunate. We can buy chips on Alibaba, maybe then we can get SDK?&#xA;&#xA;With such a rich set of supported media and so much versatility in a small package, an open SDK would allow users to address various software shortcomings with these devices (such as the strange fonts I mentioned earlier) or issues related to metadata processing where file &#xA;names and order are incorrectly displayed.&#xA;&#xA;So far, we have only been able to correct font types and adjust embedded string entries in the .STY files. While searching for information online, we found some repositories dealing with the device flashing process:&#xA;&#xA;https://github.com/nfd/atj2127decrypt (Re-implementing the flash process)&#xA;https://github.com/Rockbox/rockbox (Implementing unpacking with atjboottool)&#xA;&#xA;People from Rockbox have checked whether a custom operating system can be integrated into https://forums.rockbox.org/index.php?topic=51281.0, but 200K is simply too small.&#xA;&#xA;We also found some people selling proprietary Actions Semiconductor firmware tools for ATJ2127 on a Chinese website that we do not want to include here, but you can find them.&#xA;&#xA;Looking for ADFUS.BIN? PD196ATJ2127 has ADFUS.BIN inside case/fwpkg/US212ADEMO.fw sqlite3 database after you decrypt it with atjboottool for ATJ2127 and the ARM version of ADFUS.BIN is in all ATJ2157 firmwares you can download from RUIZU, AGPTEK etc.&#xA;&#xA;SELECT writefile(FileName, File) FROM FileTable WHERE FileName = &#39;ADFUS.BIN&#39;;&#xA;&#xA;Updates:&#xA;&#xA;Somebody got much further than us with arbitrary code execution - https://www.reddit.com/r/hacking/comments/1hss4k3/ifinallygotarbitrarycoderunningonruizux02/ and patched AP - https://gitlab.com/reverse2682701/ruizu-x02-rev&#xA;A post showing how to flash SanDisk Sport using reverse-engineered Actions Media Tool scripts from the repo we linked earlier - https://gist.github.com/roman-yepishev/737dfda3a0a853fe730286d3ce49fccd. The author links to a reverse-engineered ADFUS.BIN but you don&#39;t have to do that - take PD196_ATJ2127 version.]]&gt;</description>
      <content:encoded><![CDATA[<p>I would like to report on what we have learned during our research into ATJ21XX-SoCs.</p>

<p>Have you ever come across a device such as AGPTEK, WOLFANG, YOTON, or RUIZU? These devices seem to all be built by the same company. All of them support MP3/OGG/FLAC/AAC/APE formats, have the
same menu structures, and sometimes even may be capable of playing videos or count your steps.</p>

<p>We have confirmed that RUIZU and AGPTEK are the same company. That&#39;s written right on the box, but many other players use the same chip, the ATJ2157 from Actions Semiconductor. These OEMs do not start with just the data sheet but instead use an SDK based on <a href="https://osrtos.com/rtos/uc-os-ii/" rel="nofollow">uC/OS-II</a>.</p>

<p>It is unfortunate that some of these devices are built so cheap – low-speed memory, a poor FM tuner, and random glitches in the OEM operating system lead to devices with little polish, given that these chips are very powerful.</p>
<ul><li><p>ATJ212X are MIPS-based and were found in your SanDisk Clip Sport and Jam devices as well as the RUIZU X02 (see <a href="https://www.audiosciencereview.com/forum/index.php?threads/ruizu-x02-partial-disassembly-and-notes.7697/" rel="nofollow">Ruizu X02 Partial Disassembly and Notes</a>). The data sheet
calls the available SRAM “from ten to several hundred KB”.</p></li>

<li><p>ATJ215X are ARM Cortex M4F-based and are now used in almost all “budget” devices. CPU runs at 288MHz and has only 224KB RAM. This is less than the Raspberry Pi RP2040 with 256K.</p></li></ul>

<p>These chips are all-in-one SoCs – lithium-ion battery protection, microphone input, USB 2.0 interface, SPI and SD interfaces, NOR/NAND flash controller, many GPIO pins, stereo headphone output for headphones, I²S up to 192kHz.</p>

<p>The SDK for the MIPS version was leaked – <a href="https://github.com/Suber/PD196_ATJ2127" rel="nofollow">https://github.com/Suber/PD196_ATJ2127</a>, and we can look into the wonders of UI built on an RTOS. Apart from data sheets and pinouts, we have found nothing for the ARM variant, which is unfortunate. We can buy chips on Alibaba, maybe then we can get SDK?</p>

<p>With such a rich set of supported media and so much versatility in a small package, an open SDK would allow users to address various software shortcomings with these devices (such as the strange fonts I mentioned earlier) or issues related to metadata processing where file
names and order are incorrectly displayed.</p>

<p>So far, we have only been able to correct font types and adjust embedded string entries in the .STY files. While searching for information online, we found some repositories dealing with the device flashing process:</p>
<ul><li><a href="https://github.com/nfd/atj2127decrypt" rel="nofollow">https://github.com/nfd/atj2127decrypt</a> (Re-implementing the flash process)</li>
<li><a href="https://github.com/Rockbox/rockbox" rel="nofollow">https://github.com/Rockbox/rockbox</a> (Implementing unpacking with atjboottool)</li></ul>

<p>People from Rockbox have checked whether a custom operating system can be integrated into <a href="https://forums.rockbox.org/index.php?topic=51281.0" rel="nofollow">https://forums.rockbox.org/index.php?topic=51281.0</a>, but 200K is simply too small.</p>

<p>We also found some people selling proprietary Actions Semiconductor firmware tools for ATJ2127 on a Chinese website that we do not want to include here, but you can find them.</p>

<p>Looking for ADFUS.BIN? PD196<em>ATJ2127 has ADFUS.BIN inside case/fwpkg/US212A</em>DEMO.fw sqlite3 database after you decrypt it with atjboottool for ATJ2127 and the ARM version of ADFUS.BIN is in all ATJ2157 firmwares you can download from RUIZU, AGPTEK etc.</p>

<pre><code>SELECT writefile(FileName, File) FROM FileTable WHERE FileName = &#39;ADFUS.BIN&#39;;
</code></pre>

<p>Updates:</p>
<ol><li>Somebody got much further than us with arbitrary code execution – <a href="https://www.reddit.com/r/hacking/comments/1hss4k3/i_finally_got_arbitrary_code_running_on_ruizu_x02/" rel="nofollow">https://www.reddit.com/r/hacking/comments/1hss4k3/i_finally_got_arbitrary_code_running_on_ruizu_x02/</a> and patched AP – <a href="https://gitlab.com/reverse2682701/ruizu-x02-rev" rel="nofollow">https://gitlab.com/reverse2682701/ruizu-x02-rev</a></li>
<li>A post showing how to flash SanDisk Sport using reverse-engineered Actions Media Tool scripts from the repo we linked earlier – <a href="https://gist.github.com/roman-yepishev/737dfda3a0a853fe730286d3ce49fccd" rel="nofollow">https://gist.github.com/roman-yepishev/737dfda3a0a853fe730286d3ce49fccd</a>. The author links to a reverse-engineered ADFUS.BIN but you don&#39;t have to do that – take PD196_ATJ2127 version.</li></ol>
]]></content:encoded>
      <guid>https://infosec.press/tildavaan/more-atj21xx-stuff</guid>
      <pubDate>Sun, 08 Mar 2026 22:27:25 +0000</pubDate>
    </item>
    <item>
      <title>Announcing Project Waterjet</title>
      <link>https://infosec.press/tildavaan/announcing-project-waterjet</link>
      <description>&lt;![CDATA[Regardless of what&#39;s your take on Apple, they do make products that are beautiful. Beauty in design, beauty in simplicity. As I am typing this on my Macbook, I see crisp fonts, I see gorgeous icons.&#xA;&#xA;Now, mass-produced gadgets from China usually lack that design fine-tuning even when the hardware is amazing.&#xA;&#xA;Starting from serif fonts which make your 24-bit FLAC-playing DAP look like it is a typewriter from 90s, to the hodgepodge of icons and backgrounds.&#xA;&#xA;Usually these devices do not support customer theming, but we are going to change this a bit with Waterjet.&#xA;&#xA;In the coming months we will be releasing docs and tools allowing decrypting, unpacking, updating, and re-packing firmware resources for devices running on Actions Semiconductor ATJ212X, ATJ215X, and others that use μC/OS-based SDK, allowing everytone to personalize their devices without the need for SDK from Actions.&#xA;&#xA;And to the vendors who ship these devices -- you will have a better customer experience if you run the fonts and designs past a designer, then we would not need to do all this.&#xA;&#xA;And to start us up, here&#39;s the format of FWIMAGE.FW for ATJ212X devices.&#xA;&#xA;Actions Semiconductor FWIMAGE.FW Specification&#xA;&#xA;1. File Structure&#xA;&#xA;The firmware image is a sector-based container (512 bytes per sector) with a fixed-size header area of 16 sectors (8192 bytes).&#xA;&#xA;| Section | Size | Description |&#xA;| :--- | :--- | :--- |&#xA;| Global Header | 512 bytes | Basic metadata (Magic, VID/PID, Ver) |&#xA;| LDIR Table | 240  32 bytes | Fixed-size Logical Directory entries for all files |&#xA;| Component Data | Variable | Raw binary data for drivers, APs, and STY files |&#xA;&#xA;2. Global Header (Sector 0)&#xA;&#xA;The first 512 bytes contain the system metadata.&#xA;&#xA;| Offset | Size | Description |&#xA;| :--- | :--- | :--- |&#xA;| 0x00 | 4 | Magic: 0x0FF0AA55 |&#xA;| 0x04 | 4 | SDK Version (ASCII) |&#xA;| 0x08 | 4 | Firmware Version (ASCII) |&#xA;| 0x0C | 2 | Vendor ID (VID) |&#xA;| 0x0E | 2 | Product ID (PID) |&#xA;| 0x10 | 4 | LDIR Checksum (Stride 4) |&#xA;| 0x50 | 48 | USB Setup Info (ASCII) |&#xA;| 0x80 | 336 | SDK Description (ASCII) |&#xA;| 0x1FA | 4 | R3 Config Sector Offset (Pointer to DEVINFO.BIN) |&#xA;| 0x1FE | 2 | Global Header Checksum (Sum of first 510 bytes) |&#xA;&#xA;3. Logical Directory (LDIR) Table&#xA;&#xA;Starting at offset 0x200 (Sector 1) and ending at 0x2000 (Sector 16). This is a static table of exactly 240 entries. Unused entries are null-padded.&#xA;&#xA;| Offset | Size | Description |&#xA;| :--- | :--- | :--- |&#xA;| 0x00 | 8 | Filename (8.3 format, space padded) |&#xA;| 0x08 | 3 | Extension (ASCII) |&#xA;| 0x0B | 5 | Padding |&#xA;| 0x10 | 4 | Sector Offset: Start position in sectors (absolute position = offset  512) |&#xA;| 0x14 | 4 | File Size: Size in bytes |&#xA;| 0x18 | 4 | Reserved |&#xA;| 0x1C | 4 | File Checksum (Stride 4 sums) |&#xA;&#xA;4. Checksums&#xA;&#xA;Global Header Checksum&#xA;The last two bytes of the Sector 0 header (offset 0x1FE) contain a 16-bit checksum of the first 510 bytes using a 2-byte stride.&#xA;&#xA;uint16t calculateheaderchecksum(const uint8t data, sizet len) {&#xA;    uint16t sum = 0;&#xA;    for (sizet i = 0; i &lt; len; i += 2) {&#xA;        uint16t val = (uint16t)data[i] | ((uint16t)data[i+1] &lt;&lt; 8);&#xA;        sum += val;&#xA;    }&#xA;    return sum;&#xA;}&#xA;&#xA;LDIR &amp; File Checksum Algorithm (Stride 4)&#xA;Accumulates 32-bit words interpretated as little-endian. The sum naturally wraps at 32 bits.&#xA;&#xA;include stdint.h&#xA;include stddef.h&#xA;&#xA;/&#xA; Calculates the Actions Stride-4 checksum.&#xA; @param data Pointer to the buffer (must be 4-byte aligned for some platforms)&#xA; @param len  Length of data in bytes (should be multiple of 4)&#xA; @return 32-bit unsigned checksum&#xA; /&#xA;uint32t calculatechecksums4(const uint8t *data, sizet len) {&#xA;    uint32t sum = 0;&#xA;    for (sizet i = 0; i &lt; len; i += 4) {&#xA;        uint32t val = (uint32t)data[i] |&#xA;                       ((uint32t)data[i+1] &lt;&lt; 8) |&#xA;                       ((uint32t)data[i+2] &lt;&lt; 16) |&#xA;                       ((uint32t)data[i+3] &lt;&lt; 24);&#xA;        sum += val;&#xA;    }&#xA;    return sum;&#xA;}&#xA;&#xA;Sector Alignment&#xA;Every file within the image must start on a 512-byte boundary. When packing, files must be padded with null bytes to reach the next sector.&#xA;&#xA;Boot Sequence&#xA;The firmware expects KERNEL.DRV and CONFIG.BIN to be present at specific LDIR indices or offsets defined by bootloader. Just put them at the same location as where you took them.&#xA;&#xA;----&#xA;&#xA;Interested in the format of ATJ215X firmware? It is an encrypted sqlite3 database. And encryption has already been reverse-engineered -- see rockbox sources for atjboottool.]]&gt;</description>
      <content:encoded><![CDATA[<p>Regardless of what&#39;s your take on Apple, they do make products that are beautiful. Beauty in design, beauty in simplicity. As I am typing this on my Macbook, I see crisp fonts, I see gorgeous icons.</p>

<p>Now, mass-produced gadgets from China usually lack that design fine-tuning even when the hardware is amazing.</p>

<p>Starting from serif fonts which make your 24-bit FLAC-playing DAP look like it is a typewriter from 90s, to the hodgepodge of icons and backgrounds.</p>

<p>Usually these devices do not support customer theming, but we are going to change this a bit with Waterjet.</p>

<p>In the coming months we will be releasing docs and tools allowing decrypting, unpacking, updating, and re-packing firmware resources for devices running on Actions Semiconductor ATJ212X, ATJ215X, and others that use μC/OS-based SDK, allowing everytone to personalize their devices without the need for SDK from Actions.</p>

<p>And to the vendors who ship these devices — you will have a better customer experience if you run the fonts and designs past a designer, then we would not need to do all this.</p>

<p>And to start us up, here&#39;s the format of FWIMAGE.FW for ATJ212X devices.</p>

<h1 id="actions-semiconductor-fwimage-fw-specification">Actions Semiconductor FWIMAGE.FW Specification</h1>

<h2 id="1-file-structure">1. File Structure</h2>

<p>The firmware image is a sector-based container (512 bytes per sector) with a fixed-size header area of 16 sectors (8192 bytes).</p>

<table>
<thead>
<tr>
<th align="left">Section</th>
<th align="left">Size</th>
<th align="left">Description</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left"><strong>Global Header</strong></td>
<td align="left">512 bytes</td>
<td align="left">Basic metadata (Magic, VID/PID, Ver)</td>
</tr>

<tr>
<td align="left"><strong>LDIR Table</strong></td>
<td align="left">240 * 32 bytes</td>
<td align="left"><strong>Fixed-size</strong> Logical Directory entries for all files</td>
</tr>

<tr>
<td align="left"><strong>Component Data</strong></td>
<td align="left">Variable</td>
<td align="left">Raw binary data for drivers, APs, and STY files</td>
</tr>
</tbody>
</table>

<h2 id="2-global-header-sector-0">2. Global Header (Sector 0)</h2>

<p>The first 512 bytes contain the system metadata.</p>

<table>
<thead>
<tr>
<th align="left">Offset</th>
<th align="left">Size</th>
<th align="left">Description</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">0x00</td>
<td align="left">4</td>
<td align="left"><strong>Magic</strong>: <code>0x0FF0AA55</code></td>
</tr>

<tr>
<td align="left">0x04</td>
<td align="left">4</td>
<td align="left">SDK Version (ASCII)</td>
</tr>

<tr>
<td align="left">0x08</td>
<td align="left">4</td>
<td align="left">Firmware Version (ASCII)</td>
</tr>

<tr>
<td align="left">0x0C</td>
<td align="left">2</td>
<td align="left">Vendor ID (VID)</td>
</tr>

<tr>
<td align="left">0x0E</td>
<td align="left">2</td>
<td align="left">Product ID (PID)</td>
</tr>

<tr>
<td align="left">0x10</td>
<td align="left">4</td>
<td align="left"><strong>LDIR Checksum</strong> (Stride 4)</td>
</tr>

<tr>
<td align="left">0x50</td>
<td align="left">48</td>
<td align="left">USB Setup Info (ASCII)</td>
</tr>

<tr>
<td align="left">0x80</td>
<td align="left">336</td>
<td align="left">SDK Description (ASCII)</td>
</tr>

<tr>
<td align="left">0x1FA</td>
<td align="left">4</td>
<td align="left">R3 Config Sector Offset (Pointer to DEVINFO.BIN)</td>
</tr>

<tr>
<td align="left">0x1FE</td>
<td align="left">2</td>
<td align="left"><strong>Global Header Checksum</strong> (Sum of first 510 bytes)</td>
</tr>
</tbody>
</table>

<h2 id="3-logical-directory-ldir-table">3. Logical Directory (LDIR) Table</h2>

<p>Starting at offset <code>0x200</code> (Sector 1) and ending at <code>0x2000</code> (Sector 16). This is a static table of exactly 240 entries. Unused entries are null-padded.</p>

<table>
<thead>
<tr>
<th align="left">Offset</th>
<th align="left">Size</th>
<th align="left">Description</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">0x00</td>
<td align="left">8</td>
<td align="left">Filename (8.3 format, space padded)</td>
</tr>

<tr>
<td align="left">0x08</td>
<td align="left">3</td>
<td align="left">Extension (ASCII)</td>
</tr>

<tr>
<td align="left">0x0B</td>
<td align="left">5</td>
<td align="left">Padding</td>
</tr>

<tr>
<td align="left">0x10</td>
<td align="left">4</td>
<td align="left"><strong>Sector Offset</strong>: Start position in sectors (absolute position = offset * 512)</td>
</tr>

<tr>
<td align="left">0x14</td>
<td align="left">4</td>
<td align="left"><strong>File Size</strong>: Size in bytes</td>
</tr>

<tr>
<td align="left">0x18</td>
<td align="left">4</td>
<td align="left">Reserved</td>
</tr>

<tr>
<td align="left">0x1C</td>
<td align="left">4</td>
<td align="left"><strong>File Checksum</strong> (Stride 4 sums)</td>
</tr>
</tbody>
</table>

<h2 id="4-checksums">4. Checksums</h2>

<h3 id="global-header-checksum">Global Header Checksum</h3>

<p>The last two bytes of the Sector 0 header (offset <code>0x1FE</code>) contain a 16-bit checksum of the first 510 bytes using a 2-byte stride.</p>

<pre><code class="language-c">uint16_t calculate_header_checksum(const uint8_t *data, size_t len) {
    uint16_t sum = 0;
    for (size_t i = 0; i &lt; len; i += 2) {
        uint16_t val = (uint16_t)data[i] | ((uint16_t)data[i+1] &lt;&lt; 8);
        sum += val;
    }
    return sum;
}
</code></pre>

<h3 id="ldir-file-checksum-algorithm-stride-4">LDIR &amp; File Checksum Algorithm (Stride 4)</h3>

<p>Accumulates 32-bit words interpretated as little-endian. The sum naturally wraps at 32 bits.</p>

<pre><code class="language-c">#include &lt;stdint.h&gt;
#include &lt;stddef.h&gt;

/**
 * Calculates the Actions Stride-4 checksum.
 * @param data Pointer to the buffer (must be 4-byte aligned for some platforms)
 * @param len  Length of data in bytes (should be multiple of 4)
 * @return 32-bit unsigned checksum
 */
uint32_t calculate_checksum_s4(const uint8_t *data, size_t len) {
    uint32_t sum = 0;
    for (size_t i = 0; i &lt; len; i += 4) {
        uint32_t val = (uint32_t)data[i] |
                       ((uint32_t)data[i+1] &lt;&lt; 8) |
                       ((uint32_t)data[i+2] &lt;&lt; 16) |
                       ((uint32_t)data[i+3] &lt;&lt; 24);
        sum += val;
    }
    return sum;
}
</code></pre>

<h2 id="sector-alignment">Sector Alignment</h2>

<p>Every file within the image must start on a <strong>512-byte boundary</strong>. When packing, files must be padded with null bytes to reach the next sector.</p>

<h2 id="boot-sequence">Boot Sequence</h2>

<p>The firmware expects <code>KERNEL.DRV</code> and <code>CONFIG.BIN</code> to be present at specific LDIR indices or offsets defined by bootloader. Just put them at the same location as where you took them.</p>

<hr>

<p>Interested in the format of ATJ215X firmware? It is an encrypted sqlite3 database. And encryption has already been reverse-engineered — see rockbox sources for <code>atjboottool</code>.</p>
]]></content:encoded>
      <guid>https://infosec.press/tildavaan/announcing-project-waterjet</guid>
      <pubDate>Wed, 11 Feb 2026 23:51:14 +0000</pubDate>
    </item>
    <item>
      <title>Eaton 2nd Gen Smart WiFi Devices</title>
      <link>https://infosec.press/tildavaan/eaton-2nd-gen-smart-wifi-devices</link>
      <description>&lt;![CDATA[I bought one so you don&#39;t have to. (Edit: at least until Eaton supports Matter over WiFi)&#xA;&#xA;Eaton EWSW15&#xA;&#xA;These devices connect to Azure IOT Platform. While I am sure Eaton has a great deal for that, it means that every time I turn the lights on or off, Azure gets paid a small amount of money.&#xA;&#xA;The switch, while not multi-touch capable, will wait 0.5s before turning the load on or off.&#xA;&#xA;In an event of a network connection disruption, when you are back online the switch will take ~5 minutes to become available in the app. There is no local control even though the ESP32-C3-MINI1 (datasheet) module can do this. The unit is provisioned with WiFi credentials over Bluetooth but other than that Bluetooth is not used.&#xA;&#xA;And when you use schedules, the status LED does not correspond to the actual state of the switch.&#xA;&#xA;I am still debating whether to give Schneider Electric Matter-over-WiFi a try, but the more I read the specs the more I become convinced that Z-Wave network I already have is the best.&#xA;&#xA;Edit: https://www.eaton.com/us/en-us/products/wiring-devices-connectivity/Matter.html suggests that at some point these WiFi devices will gain Matter support. If/when that happens, these switches, dimmers, and receptacles will become much more useful.]]&gt;</description>
      <content:encoded><![CDATA[<p>I bought <a href="https://www.eaton.com/us/en-us/skuPage.EWFSW15-C2-BX-L.html" rel="nofollow">one</a> so you don&#39;t have to. (Edit: at least until Eaton supports Matter over WiFi)</p>

<p><img src="https://media.infosec.exchange/infosec.exchange/media_attachments/files/114/456/866/597/551/126/original/2e58f39ef439ce2a.jpeg" alt="Eaton EWSW15"></p>

<p>These devices connect to <a href="https://azure.microsoft.com/en-us/solutions/iot" rel="nofollow">Azure IOT Platform</a>. While I am sure Eaton has a great deal for that, it means that every time I turn the lights on or off, Azure gets paid a small amount of money.</p>

<p>The switch, while not multi-touch capable, will wait 0.5s before turning the load on or off.</p>

<p>In an event of a network connection disruption, when you are back online the switch will take ~5 minutes to become available in the app. There is no local control even though the ESP32-C3-MINI1 (<a href="https://www.espressif.com/sites/default/files/documentation/esp32-c3-mini-1_datasheet_en.pdf" rel="nofollow">datasheet</a>) module can do this. The unit is provisioned with WiFi credentials over Bluetooth but other than that Bluetooth is not used.</p>

<p>And when you use schedules, the status LED does not correspond to the actual state of the switch.</p>

<p>I am still debating whether to give <a href="https://shop.se.com/us/en/product-category/switches-outlets" rel="nofollow">Schneider Electric</a> Matter-over-WiFi a try, but the more I read the specs the more I become convinced that Z-Wave network I already have is the best.</p>

<p>Edit: <a href="https://www.eaton.com/us/en-us/products/wiring-devices-connectivity/Matter.html" rel="nofollow">https://www.eaton.com/us/en-us/products/wiring-devices-connectivity/Matter.html</a> suggests that at some point these WiFi devices will gain Matter support. If/when that happens, these switches, dimmers, and receptacles will become much more useful.</p>
]]></content:encoded>
      <guid>https://infosec.press/tildavaan/eaton-2nd-gen-smart-wifi-devices</guid>
      <pubDate>Sat, 10 May 2025 14:15:28 +0000</pubDate>
    </item>
    <item>
      <title>Eaton Smart Breakers</title>
      <link>https://infosec.press/tildavaan/eaton-smart-breakers</link>
      <description>&lt;![CDATA[In case you want more #IOT in your life, Eaton ships remotely actuated circuit breakers.&#xA;&#xA;The breakers are provisioned using a &#34;BlinkUp&#34; system through your phone. You start the provisioning on your device, then put your screen to the sensor on the circuit breaker, your screen blinks a number of times sending WiFi credentials to the device, and then the latter connects to the Electric Imp servers. Eaton is using impOs as the basis of their offering, and Electric Imp is adamant they are secure.&#xA;&#xA;Now, Eaton provides API to these circuit breakers - https://api.em.eaton.com/docs, but there is no true local access - there is apparently a way to get local control, but your device must phone home weekly to receive configuration that would allow you to talk to your device locally.]]&gt;</description>
      <content:encoded><![CDATA[<p>In case you want more <a href="/tildavaan/tag:IOT" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">IOT</span></a> in your life, Eaton <a href="https://www.eaton.com/us/en-us/products/electrical-circuit-protection/circuit-breakers/smart-breakers-use-cases.html" rel="nofollow">ships</a> remotely actuated circuit breakers.</p>

<p>The breakers are provisioned using a “BlinkUp” system through your phone. You start the provisioning on your device, then put your screen to the sensor on the circuit breaker, your screen blinks a number of times sending WiFi credentials to the device, and then the latter connects to the <a href="https://www.electricimp.com" rel="nofollow">Electric Imp</a> servers. Eaton is using impOs as the basis of their offering, and Electric Imp is adamant they are secure.</p>

<p>Now, Eaton provides API to these circuit breakers – <a href="https://api.em.eaton.com/docs" rel="nofollow">https://api.em.eaton.com/docs</a>, but there is no true local access – there is apparently a way to get local control, but your device must phone home weekly to receive configuration that would allow you to talk to your device locally.</p>
]]></content:encoded>
      <guid>https://infosec.press/tildavaan/eaton-smart-breakers</guid>
      <pubDate>Sat, 19 Apr 2025 03:18:12 +0000</pubDate>
    </item>
    <item>
      <title>Smart Customer Mobile API</title>
      <link>https://infosec.press/tildavaan/smart-customer-mobile-api</link>
      <description>&lt;![CDATA[As I was writing this I decided to scan GitHub for the URLs I found so far, and, well, people smarter than me have already written a homeassistant integration against #SEW, but it is a bit different from what I saw in the field:&#xA;&#xA;https://github.com/cnecrea/hidroelectrica&#xA;&#xA;I&#39;d still like to describe how to locate the endpoints and the login process, so here we go...&#xA;&#xA;This is the second post about #SEW SCM API - Smart Customer Mobile API by Smart Energy Water, this time we will learn about different APIs using real world utility websites.&#xA;&#xA;It appears that there are at least two different API &#34;flavors&#34;. The one that uses ModuleName.svc/MethodNameMob naming convention and usually resides under PortalService endpoint, and the newer one, which lives under /API/.&#xA;&#xA;So e.g. Nebraska Public Power District has endpoints at https://onlineaccount.nppd.com/PortalService/, e.g. https://onlineaccount.nppd.com/PortalService/UserLogin.svc/help. Rochester Public Utilities runs a different set of endpoints, with the root at https://connectwith.rpu.com/api.&#xA;&#xA;The endpoints for the latter API can also be browsed at https://scmcx.smartcmobile.com/API/Help/.&#xA;&#xA;Different utilities pay for different set of modules, and here&#39;s some of the modules I have discovered so far:&#xA;&#xA;AdminBilling&#xA;CompareSpending&#xA;ConnectMe&#xA;EnergyEfficiency&#xA;Generation&#xA;Notifications&#xA;Outage&#xA;PaymentGateway&#xA;Usage&#xA;UserAccount&#xA;UserLogin&#xA;&#xA;For /PortalService/ endpoints you can visit BASEURL + /PortalService/ + ModuleName + .svc + /help to get the list of RPC calls you can issue. In order to find out what to send in the requests, you need to look into the calls within the apps for your utility. Note that some utilities opted out of the AES/CBC/PKCS5Padding PasswordPassword encryption, so let&#39;s hope this will be a trend forward. Currently SEW web portals talk to a completely different set of APIs to populate the interface, even though they are querying the same thing.&#xA;&#xA;So to start, here&#39;s how to login to your favorite utility:&#xA;&#xA;from typing import Mapping, Any&#xA;&#xA;import base64&#xA;import json&#xA;import hashlib&#xA;import requests&#xA;import urllib.parse&#xA;&#xA;from Crypto.Cipher import AES&#xA;&#xA;BASEURL = &#34;https://example.com/PortalService&#34;&#xA;&#xA;def encryptquery(&#xA;    params: Mapping[str, str], encryptionkey: str = &#34;PasswordPassword&#34;&#xA;) -  str:&#xA;    &#34;&#34;&#34;Encrypt with AES/CBC/PKCS5Padding.&#34;&#34;&#34;&#xA;    cipher = AES.new(encryptionkey, AES.MODECBC, IV=encryptionkey)&#xA;&#xA;    cleartext = urllib.parse.urlencode(params).encode()&#xA;&#xA;    # PKCS5 Padding - https://www.rfc-editor.org/rfc/rfc8018#appendix-B.2.5&#xA;    paddinglength = 16 - len(cleartext) % 16&#xA;    cleartext += paddinglength * chr(paddinglength).encode()&#xA;&#xA;    return base64.b64encode(cipher.encrypt(cleartext)).decode(&#34;ascii&#34;)&#xA;&#xA;def request(module: str, method: str, data: Mapping[str, Any]) -  Mapping[str, str]:&#xA;    encquery = encryptquery(data)&#xA;    # Or module + &#39;.svc/&#39;&#xA;    url = BASEURL + &#34;/&#34; + module + &#34;/&#34; + method&#xA;&#xA;    resp = requests.post(url, json={&#34;EncType&#34;: &#34;A&#34;, &#34;EncQuery&#34;: encquery})&#xA;    if not resp.ok:&#xA;        raise Exception(resp.statuscode)&#xA;    return resp.json()&#xA;&#xA;passworddigest = hashlib.sha256(&#34;PASSWORD&#34;.encode()).hexdigest()&#xA;Or ValidateUserLoginMob&#xA;response = request(&#xA;    &#34;UserLogin&#34;,&#xA;    &#34;ValidateUserLogin&#34;,&#xA;    {&#34;UserId&#34;: &#34;USERNAME&#34;, &#34;Password&#34;: passworddigest},&#xA;)&#xA;print(response)&#xA;response will contain some object, you will need LoginToken and AccountNumber to proceed with most of the other calls.&#xA;&#xA;It&#39;s a bit awkward that different utilities have different endpoints, which makes creating a universal client challenging, so for now I am researching the ways to get info from the Usage module. The parameters are weird (&#34;type&#34;: &#34;MI&#34;, or &#34;HourlyType&#34;: &#34;H&#34;), but we will get there.]]&gt;</description>
      <content:encoded><![CDATA[<p>As I was writing this I decided to scan GitHub for the URLs I found so far, and, well, people smarter than me have already written a home_assistant integration against <a href="/tildavaan/tag:SEW" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">SEW</span></a>, but it is a bit different from what I saw in the field:</p>
<ul><li><a href="https://github.com/cnecrea/hidroelectrica" rel="nofollow">https://github.com/cnecrea/hidroelectrica</a></li></ul>

<p>I&#39;d still like to describe how to locate the endpoints and the login process, so here we go...</p>

<p>This is the second post about <a href="/tildavaan/tag:SEW" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">SEW</span></a> SCM API – Smart Customer Mobile API by Smart Energy Water, this time we will learn about different APIs using real world utility websites.</p>

<p>It appears that there are at least two different API “flavors”. The one that uses <code>ModuleName.svc/MethodNameMob</code> naming convention and usually resides under <code>PortalService</code> endpoint, and the newer one, which lives under <code>/API/</code>.</p>

<p>So e.g. <a href="https://nppd.com" rel="nofollow">Nebraska Public Power District</a> has endpoints at <code>https://onlineaccount.nppd.com/PortalService/</code>, e.g. <code>https://onlineaccount.nppd.com/PortalService/UserLogin.svc/help</code>. <a href="https://www.rpu.com" rel="nofollow">Rochester Public Utilities</a> runs a different set of endpoints, with the root at <code>https://connectwith.rpu.com/api</code>.</p>

<p>The endpoints for the latter API can also be browsed at <a href="https://scmcx.smartcmobile.com/API/Help/" rel="nofollow">https://scmcx.smartcmobile.com/API/Help/</a>.</p>

<p>Different utilities pay for different set of modules, and here&#39;s some of the modules I have discovered so far:</p>
<ul><li>AdminBilling</li>
<li>CompareSpending</li>
<li>ConnectMe</li>
<li>EnergyEfficiency</li>
<li>Generation</li>
<li>Notifications</li>
<li>Outage</li>
<li>PaymentGateway</li>
<li>Usage</li>
<li>UserAccount</li>
<li>UserLogin</li></ul>

<p>For <code>/PortalService/</code> endpoints you can visit <code>BASE_URL</code> + <code>/PortalService/</code> + <code>ModuleName</code> + <code>.svc</code> + <code>/help</code> to get the list of RPC calls you can issue. In order to find out what to send in the requests, you need to look into the calls within the apps for your utility. Note that some utilities opted out of the AES/CBC/PKCS5Padding <code>PasswordPassword</code> encryption, so let&#39;s hope this will be a trend forward. Currently SEW web portals talk to a completely different set of APIs to populate the interface, even though they are querying the same thing.</p>

<p>So to start, here&#39;s how to login to your favorite utility:</p>

<pre><code class="language-python">from typing import Mapping, Any

import base64
import json
import hashlib
import requests
import urllib.parse

from Crypto.Cipher import AES

BASE_URL = &#34;https://example.com/PortalService&#34;


def _encrypt_query(
    params: Mapping[str, str], encryption_key: str = &#34;PasswordPassword&#34;
) -&gt; str:
    &#34;&#34;&#34;Encrypt with AES/CBC/PKCS5Padding.&#34;&#34;&#34;
    cipher = AES.new(encryption_key, AES.MODE_CBC, IV=encryption_key)

    cleartext = urllib.parse.urlencode(params).encode()

    # PKCS5 Padding - https://www.rfc-editor.org/rfc/rfc8018#appendix-B.2.5
    padding_length = 16 - len(cleartext) % 16
    cleartext += padding_length * chr(padding_length).encode()

    return base64.b64encode(cipher.encrypt(cleartext)).decode(&#34;ascii&#34;)


def request(module: str, method: str, data: Mapping[str, Any]) -&gt; Mapping[str, str]:
    enc_query = _encrypt_query(data)
    # Or module + &#39;.svc/&#39;
    url = BASE_URL + &#34;/&#34; + module + &#34;/&#34; + method

    resp = requests.post(url, json={&#34;EncType&#34;: &#34;A&#34;, &#34;EncQuery&#34;: enc_query})
    if not resp.ok:
        raise Exception(resp.status_code)
    return resp.json()


password_digest = hashlib.sha256(&#34;PASSWORD&#34;.encode()).hexdigest()
# Or ValidateUserLoginMob
response = request(
    &#34;UserLogin&#34;,
    &#34;ValidateUserLogin&#34;,
    {&#34;UserId&#34;: &#34;USERNAME&#34;, &#34;Password&#34;: password_digest},
)
print(response)
</code></pre>

<p><code>response</code> will contain some object, you will need <code>LoginToken</code> and <code>AccountNumber</code> to proceed with most of the other calls.</p>

<p>It&#39;s a bit awkward that different utilities have different endpoints, which makes creating a universal client challenging, so for now I am researching the ways to get info from the <code>Usage</code> module. The parameters are weird (“type”: “MI”, or “HourlyType”: “H”), but we will get there.</p>
]]></content:encoded>
      <guid>https://infosec.press/tildavaan/smart-customer-mobile-api</guid>
      <pubDate>Tue, 15 Apr 2025 23:34:39 +0000</pubDate>
    </item>
    <item>
      <title>Smart Energy Water SCM API Secrets</title>
      <link>https://infosec.press/tildavaan/smart-energy-water-scm-api-secrets</link>
      <description>&lt;![CDATA[Once upon a time I learned about Opower HomeAssistant integration. But my utility does not use Opower, it was using something called &#34;Smart Energy Water&#34;.&#xA;&#xA;Smart Energy Water, or #SEW is a SaaS provider, and they ship the whole thing - the backend, frontend, and the phone apps, the latter under the name SCM, which means Smart Customer Mobile.&#xA;&#xA;So I embarked on a journey to figure out how these phone apps worked and, if successful, get my data out and into homeassistant.&#xA;&#xA;APK&#xA;&#xA;I pulled an APK of my utility from Google Play Store and found that something secret is hidden in a libnative-lib.so binary, under com.sew.scm.gcm.SecureConstant, under a few methods returning String, and some methods that decrypt these strings using a heavily obfuscated set of routines, which essentially XOR&#39;d (in case of Android APK) the values of gcmdefaultsenderid + googleappid + AndroidAppRatingConstantFile, all the values from the strings.xml within the app resources.&#xA;&#xA;One of the decoded tokens contains a key for request encryption. It was ...&#xA;&#xA;PasswordPassword&#xA;&#xA;SCM apps use private APIs. In order to remain private and hard to use the requests are encrypted.&#xA;&#xA;You urlencode the parameters into key=value&amp;key1=value1... form, then encrypt the resulting string using AES-CBC with PKCS5 Padding (16 bytes variant) using PasswordPassword as both the key and IV.&#xA;&#xA;Then you send {&#34;EncType&#34;: &#34;A&#34;, &#34;EncQuery&#34;: &#34;base64-encoded-encrypted-string&#34;}, and receive response from one of the .../API/Module/MethodName endpoints. The response will be JSON with no extra encryption, so it is definitely a deterrent against making requests, not a security feature.&#xA;&#xA;Login&#xA;&#xA;Armed with that knowledge, and some help from exposed API listing on one of the utility websites I found that I need to use ValidateUserLoginMob call expecting userid and password.&#xA;&#xA;However, password had to be base64-encoded result of applying a secret scheme from that SecurityConstant module above. It is always SHA256.&#xA;&#xA;So my first https://utility.example.net/API/UserLogin/ValidateUserLogin was a success, I got LoginToken and AccountNumber, which was all we needed to start poking APIs.&#xA;&#xA;Tada!&#xA;&#xA;If your utility uses SEW SCM, i.e. one of these at https://play.google.com/store/apps/developer?id=Smart+Energy+Water, you should be able to get API listing by visiting the web interface, and appending /API/Help. Or, if your utility runs an older version of SCM, replace /portal/ with /portalservice/UserLogin.svc/help or /portalservice/Usage.svc/help. You may get the .NET API definitions.]]&gt;</description>
      <content:encoded><![CDATA[<p>Once upon a time I learned about <a href="https://github.com/home-assistant/core/tree/dev/homeassistant/components/opower" rel="nofollow">Opower HomeAssistant integration</a>. But my utility does not use Opower, it was using something called “Smart Energy Water”.</p>

<p>Smart Energy Water, or <a href="/tildavaan/tag:SEW" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">SEW</span></a> is a SaaS provider, and they ship the whole thing – the backend, frontend, and the phone apps, the latter under the name SCM, which means Smart Customer Mobile.</p>

<p>So I embarked on a journey to figure out how these phone apps worked and, if successful, get my data out and into homeassistant.</p>

<h1 id="apk">APK</h1>

<p>I pulled an APK of my utility from Google Play Store and found that something secret is hidden in a libnative-lib.so binary, under <code>com.sew.scm.gcm.SecureConstant</code>, under a few methods returning String, and some methods that decrypt these strings using a heavily obfuscated set of routines, which essentially XOR&#39;d (in case of Android APK) the values of <code>gcm_default_sender_id</code> + <code>google_app_id</code> + <code>Android_App_RatingConstant_File</code>, all the values from the <code>strings.xml</code> within the app resources.</p>

<p>One of the decoded tokens contains a key for request encryption. It was ...</p>

<h1 id="passwordpassword"><code>PasswordPassword</code></h1>

<p>SCM apps use private APIs. In order to remain private and hard to use the requests are encrypted.</p>

<p>You urlencode the parameters into <code>key=value&amp;key1=value1...</code> form, then encrypt the resulting string using AES-CBC with PKCS5 Padding (16 bytes variant) using <code>PasswordPassword</code> as both the <code>key</code> and <code>IV</code>.</p>

<p>Then you send <code>{&#34;EncType&#34;: &#34;A&#34;, &#34;EncQuery&#34;: &#34;base64-encoded-encrypted-string&#34;}</code>, and receive response from one of the <code>.../API/Module/MethodName</code> endpoints. The response will be JSON with no extra encryption, so it is definitely a deterrent against making requests, not a security feature.</p>

<h1 id="login">Login</h1>

<p>Armed with that knowledge, and some help from exposed API listing on one of the utility websites I found that I need to use <code>ValidateUserLoginMob</code> call expecting <code>userid</code> and <code>password</code>.</p>

<p>However, <code>password</code> had to be base64-encoded result of applying a secret scheme from that SecurityConstant module above. It is always <code>SHA256</code>.</p>

<p>So my first <code>https://utility.example.net/API/UserLogin/ValidateUserLogin</code> was a success, I got <code>LoginToken</code> and <code>AccountNumber</code>, which was all we needed to start poking APIs.</p>

<h1 id="tada">Tada!</h1>

<p>If your utility uses SEW SCM, i.e. one of these at <a href="https://play.google.com/store/apps/developer?id=Smart+Energy+Water" rel="nofollow">https://play.google.com/store/apps/developer?id=Smart+Energy+Water</a>, you should be able to get API listing by visiting the web interface, and appending /API/Help. Or, if your utility runs an older version of SCM, replace <code>/portal/</code> with <code>/portalservice/UserLogin.svc/help</code> or <code>/portalservice/Usage.svc/help</code>. You may get the .NET API definitions.</p>
]]></content:encoded>
      <guid>https://infosec.press/tildavaan/smart-energy-water-scm-api-secrets</guid>
      <pubDate>Tue, 15 Apr 2025 03:14:26 +0000</pubDate>
    </item>
  </channel>
</rss>