_._     _,-'""`-._
(,-.`._,'(       |\`-/|
    `-.-' \ )-`( , o o)
          `-    \`_`"'-

tpbreak: cracking TP-Link's firmware fleet at scale

2026-05-26 · tp-linkfirmwareiotrsareverse-engineering

This started as "find an IoT target that boots under QEMU and hunt bugs." It ended as tpbreak.afflicted.sh: a point-and-click service that fetches any of 52,609 TP-Link firmware images straight from TP-Link's own CDN and decrypts 96.6% of them in your browser, no keys, no dump, no device. Six distinct crypto formats reversed. Eleven public keys that turn out to be all the keys you ever need. One bucket that should never have been listable.

This is the whole writeup: how the fleet leaks, why "encrypted firmware" is a misnomer for most of it, the two times I confidently declared something "hardware-locked" and was wrong, the cryptanalysis that says you will never factor any of these keys (and why that does not matter), and the honest 3.4% that genuinely needs a soldering iron.

TL;DR

The bucket

Every TP-Link firmware update. cameras, routers, range extenders, switches, access points, smart plugs, the lot. is served from https://download.tplinkcloud.com/<key>. That hostname is a CloudFront distribution, and CloudFront is configured to return AccessDenied for a bucket listing. Standard.

But CloudFront is just a cache in front of an S3 origin, and the origin bucket is named after the hostname. S3's REST API answers ListBucketV2 on the raw origin endpoint even when the CDN in front of it won't:

$ curl -s 'https://s3.amazonaws.com/download.tplinkcloud.com/?list-type=2&prefix=firmware/' | head
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>download.tplinkcloud.com</Name>
  <Contents><Key>firmware/.../Tapo_C210v2_..._up_boot-signed_....bin</Key>...
  ... 52,609 keys later ...

Paginate the continuation token to the end and you have the entire fleet: 52,609 images, 12,012 of them Tapo cameras, across 66+ model families, every historical version still present. Parse the filenames (model / hardware rev / version / build / epoch-ms timestamp) and you have a searchable index. No credentials, no rate limit worth mentioning. (TP-Link's other bucket, static.tp-link.com, where the GPL source tarballs live, is correctly locked down. AccessDenied on the origin too. So this is a specific misconfiguration on the firmware bucket, not a blanket one.)

The trick that breaks most of it

Credit where due: the core insight is watchfulip's C210v2 writeup. TP-Link's camera/IoT images start with a header (magic 00 00 02 00 55 aa), an RSA-2048 signature, and an AES-128-CBC ciphertext body. The naive read is "signed and encrypted, you need the private key to forge and the AES key to decrypt." Both wrong.

The firmware's own updater calls a function with a gloriously honest name:

rsaVerifyPSSSignByBase64EncodePublicKeyBlob(pubkey, keylen,
                                            data, datalen,
                                            sig, siglen, ...)

This is not rsaVerifySign (verify-only). This routine verifies the PSS signature and, as part of the same operation, derives the AES-128-CBC key and IV from the recovered PSS salt/hash. The key material falls out of a public-key operation. So the device, which obviously only carries the public key. can decrypt its own firmware, which means anyone with the public key can too. And the public key is sitting right there in nvrammanager / slpupgrade inside any plaintext image, stored as a Microsoft PUBLICKEYBLOB (base64 BgIAAA..., the RSA1 magic, bit-length little-endian at offset 12).

So the "decrypt" is: pick the right public key, let the verify routine spit out KEY= and IV=, run AES-128-CBC the other direction. That's it. The signature was never the wall; it was the doorbell.

Six formats, one pipeline

Cameras were just the first. The fleet uses a small zoo of formats, and the service sniffs each one and routes it:

Detection is content-based, not filename-based: strong 4-byte magics (hsqs, UBI#, uImage, dtb, ...), an LZMA-header regex, gzip-only-at-offset-0, and a multi-window Shannon-entropy check. AES ciphertext sits at ~7.99 bits/byte everywhere; plaintext firmware with compressed regions still has structural low-entropy windows. That entropy split is also what let me measure, rather than guess, which "encrypted" parts are actually plaintext.

The EAP crack, and being wrong twice

The EAP access points (EAP610, EAP225, EAP670, ...) are Qualcomm IPQ6000 parts. Their images verify through checkFwupRsa2048_Sha256_Pss, and my first two passes concluded the key derivation was tied to the SoC. that this was hardware-locked and not crackable from files alone. I said so, twice, with confidence. I was wrong, twice, and the correct push-back was: if it's secure-boot-locked, how does OpenWrt boot on this exact SoC?

It does. OpenWrt flashes unsigned images to IPQ6000 EAPs over TFTP. If the SoC enforced secure boot, that would be impossible. Therefore the "encryption" is software, the key is in software, and software keys are findable. Re-reading the updater with that prior: checkFwupRsa2048_Sha256_Pss calls the same rsaVerifyPSSSignByBase64EncodePublicKeyBlob the cameras use, with an EAP RSA-2048 key that was already in my keyring. The only differences are framing: the signature is the last 256 bytes, the payload starts at offset 4, and the routine returns a nonzero "Error 3" from an EAP-specific post-check you simply ignore . it has already printed the KEY and IV by then.

# EAP670, derived (not extracted from hardware: derived from the file + public key)
KEY=fd8707d13dd9d0de44e26fa7eb2ee02c
IV =f5226045f5cf22c0fa17e8e50ec95c55
# AES-128-CBC, forward=0 -> squashfs @ 0x42810, ubi @ 0x1810, "support-list"... clean.

Verified on EAP670 and EAP225v3, wired into the pipeline, and the coverage number jumped from 91.8% to 95.8% as 2,131 EAP images moved from "bring your own dump" to "auto." The lesson I keep re-learning: "hardware-locked" is a claim that requires evidence the hardware actually enforces the lock. Usually it doesn't.

Config backups: a 2015 key still in production

The router/AP config-backup .bin is its own format. Reversing libutility_lib.so's md5_getDesKey gave a fixed DES key 478DA50BF9E3D2CF. which turned out to be byte-identical to the key Zibri published in 2015 and that floats around in old gists. Standard format: DES-ECB( MD5(body)[16] || body || nullpad ). The 16-byte MD5 prefix makes it self-verifying: if the MD5 of the decrypted body matches the prefix, you're certain you decrypted it correctly. no device MAC required for this layer.

$ # upload a config backup to /api/config
decrypted ✓ (DES-ECB fixed key, MD5-verified)

Newer per-device backups layer an additional HMAC-SHA256(MAC, chip-target-info) → AES-CBC stage on top, which is why the service offers an optional MAC field. But the overwhelming majority of config backups in the wild still fall to the 2015 DES key. Eleven years.

The key DB, and why you can't factor any of it (and don't need to)

Harvesting nvrammanager / slpupgrade / libcutil.so across every family I extracted yields 11 distinct public keys: 7×RSA-1024 and 4×RSA-2048, every one with e=65537. I grepped 32 key-bearing binaries from diverse families for both raw and base64 blobs. every key found was already in the ring. The harvest is complete for everything that decrypts.

Two questions always come up: can you recover the private keys, and can you make your own? I ran the actual analysis:

=== pairwise GCD (shared-prime hunt) across all moduli ===
no shared primes across any pair -> no batch-GCD factor. 11 unique moduli checked.

=== factoring feasibility ===
1024b  e=65537  -> INFEASIBLE (1024-bit RSA has never been publicly factored)
2048b  e=65537  -> INFEASIBLE

No shared primes (the one cheap Heninger-style win), no duplicate moduli, no small exponents, no short moduli. You will not factor these. But that's the wrong goal. The watchfulip break decrypts with the public key. A private key would only let you forge a signature. i.e. build a firmware the device accepts as genuine. For pulling apart what TP-Link ships, the public keys we already have are sufficient and complete.

The "make your own keypair" question is the re-flash path, not the decrypt path: generate your own RSA keypair, sign/encrypt your own image, and get the device to accept it. That works wherever the device's verification key lives in writable flash and the SoC doesn't enforce secure boot. confirmed open on the EAP series (it boots unsigned, remember), and likely on many consumer parts that check signatures in a software U-Boot. That's the bridge to custom firmware, and it's a separate project from decryption.

Coverage: the honest 96.6%

Classifying all 52,609 images by the method that handles them:

auto      50,838  (96.6%)
  camera / hub / lock RSA       13,651
  router / extender RSA-2048     6,887
  EAP RSA-PSS + AES              2,131
  managed-switch DES             1,152
  smart-home plaintext (signed)    467
  plaintext / identify          26,550
BYO        1,771  (3.4%)
  3irobotix ODM vacuum           1,546
  encrypted Kasa plugs             225

I want to be precise about that 96.6%, because an earlier number over-counted. "Smart-home" looks like one bucket but isn't: 79% of Kasa images are signed plaintext (entropy <7.5, handled by the plaintext path), and only the truly-encrypted families (HS103/HS210/HS220/KP125/EP25) need a key we don't have. I split the bucket by measured entropy rather than by name so the number reflects what actually decrypts, not what I'd like to claim.

The genuine 3.4%: where files stop being enough

Two holdouts are not laziness. they're physics. The key is not in any file TP-Link distributes.

The robot vacuum (Tapo RV). It's a re-badged 3irobotix CRL-200S. Dennis Giese's robot database lists the generic CRL-200S firmware as "sqfs, unencrypted, root easy via usb-adb". nobody decrypts a vacuum firmware file because they don't have to; they ADB-pull the plaintext rootfs off the running device. TP-Link's Tapo variant wraps that base in its own encryption layer: the OTA file is a flat 8.0-bits/byte blob behind a custom ?>ldrobot.scv<?!#001# header, with no recognizable TP-Link magic. The key lives on-device. No file-only break exists, mine included.

The encrypted Kasa plugs. A handful of plug families ship a truly-encrypted RSA-1024 image (entropy 8.0, ~225 files). I harvested every RSA-1024 key from every plaintext Kasa image I could find; none derive for these. The reason is structural: the encrypted models have no plaintext firmware version to harvest a key from. (And no, the famous softScheck "XOR key 0xAB" is the local protocol cipher for the JSON command channel. it has nothing to do with firmware.) The key is in the plug's flash. You need a dump.

The tool

tpbreak.afflicted.sh. Six tabs, click one, go:

It's hardened like something that expects to get attacked: CSP default-src 'none', the decryptor runs fork-isolated behind RLIMIT_CPU/AS/FSIZE as an unprivileged user, download filenames are sanitized to kill header/CRLF/traversal injection, uploads are size-capped, and the key whitelist plus a traversal guard means there's no open proxy / SSRF into the bucket.

WANTED: dumps

To close the last 3.4% I need what no file contains. on-device key material. If you have the hardware and the inclination:

Everything else already cracks with public keys. If you can supply either dump, mail in. I'll wire the format into the service so it decrypts for everyone, and you get the credit. And if this saved you a weekend: 1Ad6r77FGadNZzj2WNEJDjXj6jw149aiw6.

Footnote: the ban

When I first drafted this, the story was a proposal: in October 2025 the US Commerce Department, backed by an interagency review (DHS, DOJ, DOD), recommended barring TP-Link products: a vendor that sells into roughly 65% of US homes. on the argument that the company remains subject to Chinese jurisdiction. It did not stay a proposal. On March 23, 2026 the FCC banned new equipment authorizations for foreign-made consumer routers outright, acting on a White House directive that found imported routers pose "unacceptable risks" to national security and citing the Volt Typhoon, Flax Typhoon and Salt Typhoon campaigns that built botnets out of home and small-office router vulnerabilities.

A few honest caveats, because the headline flattens it. The FCC order is industry-wide. TP-Link, Netgear, Asus, D-Link, Eero, Google, Linksys, Synology and more, not a TP-Link-only ban; it blocks new authorizations, not existing stock; and Netgear and Eero have already secured conditional approvals through October 2027. TP-Link "vigorously disputes" any national-security risk, no backdoor has been confirmed in its products, and the unresolved question is legal. China's National Intelligence Law, not a specific flaw anyone has pinned. The company's answer has been to relocate production: Wi-Fi 7 assembly launched in India on May 25, 2026, fittingly starting with the Omada EAP770 access point.

Which is the part I can't let go. The ban's rationale is supply chain and jurisdiction; this post is about engineering. But the two rhyme. Volt/Flax/Salt Typhoon got in through router security posture, and what you just read is a first-hand tour of that posture: a firmware fleet that lists itself to the world, "encryption" whose decryption key is printed inside the firmware, a config key that has survived since 2015, "secure-boot" access points that boot unsigned , including the very EAP line TP-Link is now proudly assembling in a new country. Moving the solder doesn't re-key the firmware. The government said no to foreign routers; spend an afternoon in the firmware and you get a pretty good idea why, first-hand.

Closing

None of this is a memory-corruption 0-day. It's worse for the vendor, in a way: the fleet is wide open by construction. The bucket lists because someone trusted CloudFront to be the only door. The firmware decrypts because the verification key and the decryption key are the same key, and it's printed inside the firmware. The config backups fall to a key from 2015. The "hardware-locked" access points boot unsigned. At every layer the lock was a convention, not a control, and conventions reverse.

Credits & references

None of this is solo work. The cryptographic break is watchfulip's; the decryptor the service runs is Nate Robbins' maintained fork; the vacuum and config-key knowledge comes from a decade of other people's reversing. Credit where it is due. in rough order of how much this leans on them:

On the ban: The Washington Post (the October 2025 Commerce proposal) and Consumer Reports (the March 2026 FCC order).

What I added on top of all that: the bucket enumeration at fleet scale, the EAP RSA-PSS framing, the config self-verify, the 11-key cryptanalysis (batch-GCD and all), and wiring it into one auto-classifying point-and-click service. The hard cryptographic insight was already done. by the people above. If I've miscredited or missed anyone, mail in and I'll fix it.