tpbreak: cracking TP-Link's firmware fleet at scale
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
- TP-Link's firmware CDN,
download.tplinkcloud.com, is a CloudFront alias for an S3 bucket. CloudFront blocksListBucket; the raws3.amazonaws.comorigin does not. One?list-type=2walk yields 52,609 firmware keys, downloadable with no auth. - The "encryption" on most images is the watchfulip scheme: a single library call verifies the RSA-PSS signature and derives the AES key/IV as a side effect of that verification. The signature is made with the private key, but verification, and therefore decryption. needs only the public key. The public keys ship inside the firmware.
- Six formats reversed: camera/hub/lock RSA-2048+AES, Cloud
(
fw-type:) RSA-2048, RSA-1024 signed, managed-switch DES-CBC, the EAP access-point RSA-PSS→AES path, and config-backup DES-ECB. All wired into one auto-classifying pipeline. - Harvested 11 distinct public keys (7×RSA-1024,
4×RSA-2048, all
e=65537). Batch-GCD across every pair: no shared primes. None are factorable. You don't need to factor them. - Coverage: 50,838 / 52,609 = 96.6% auto-decrypt. The remaining 3.4% (a robot vacuum ODM and a handful of encrypted Kasa plugs) is genuinely device-locked. the key isn't in any file TP-Link ships.
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:
- Camera / hub / lock: magic
0000020055aa, RSA-2048 verify→derive→AES-128-CBC. The watchfulip path above. - Cloud:
fw-type:ASCII header, RSA-2048, same derive-from-verify shape, different framing. Routers and extenders. - RSA-1024 signed: magic
0000010055aa. Older / smaller parts. Most of these are signed plaintext: the body is a normal squashfs/uImage, the RSA-1024 layer is just an integrity signature you can ignore. - Managed switches: DES-CBC. Strip the trailing
0x80-byte tail, align to the 8-byte block, decrypt with NoPadding. No RSA at all on these. - EAP access points: RSA-PSS→AES, the one I had to fight (next section).
- Config backups: the
.binyou export from the web UI. DES-ECB with a fixed key, 16-byte MD5 prefix for self-verification (more below).
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:
- catalog: search the 52,609-image index, pick a version, fetch→decrypt→download. The analyze button tells you which method will be used and whether decryption is even needed before you commit.
- byo: upload your own image (and optionally a device dump to harvest extra keys from, or a MAC/hw-info), for the device-locked tail.
- mac: TP-Link OUI lookup and the config-key derivation.
- keys: the full, searchable key DB. Public keys, shown in full, with the families each one unlocks.
- coverage: the live tally above.
- about: the what and why.
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:
- Tapo RV vacuum (3irobotix CRL-200S): root is easy over
USB-ADB (see Hypfer's
valetudo-crl200s-root). I need the on-device key for therobot.scvOTA layer, or just anadb pullof the NAND partitions so I can pull the plaintext rootfs and the key together. - Encrypted Kasa plugs (HS103 / HS210 / HS220 / KP125 / EP25): a UART or SPI-flash dump, so I can recover the RSA-1024 firmware key these models never ship in plaintext.
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:
- watchfulip: the original break: TP-Link firmware decryption and the C210 V2 cloud camera, and the watchfulip/tp-link-decrypt tool. The verify-derives-AES insight and the key-harvest script are theirs; everything in the "trick" section traces back to this writeup.
- Nate Robbins: robbins/tp-link-decrypt, the maintained fork (watchfulip's original was deprecated in October 2025) that added Omada / managed-switch DES support. This is the decryptor binary the service actually invokes.
- Zibri: the 2015
config-backup
DES key (
478DA50BF9E3D2CF), still valid eleven years on, plus the shreve gist mirroring the method. - softScheck: Reverse Engineering the TP-Link HS110, the canonical Kasa protocol writeup (and the source of the "XOR 0xAB" cipher I had to point out is protocol, not firmware).
- Dennis Giese: robotinfo.dev and dustcloud: the authoritative robot database that confirmed the generic CRL-200S is unencrypted sqfs and USB-ADB rootable.
- Hypfer: valetudo-crl200s-root, the CRL-200S rooting tooling and the route to the on-device key; rumpeltux: viomi-rooting, the 3irobotix/Viomi format groundwork; dontvacuum.me: DustBuilder for 3iRobotics; codetiger: VacuumRobot hardware RE.
- python-kasa: the project for the Kasa device/protocol map; the OpenWrt forum firmware-decryption thread, and OpenWrt-on-IPQ6000 itself. the proof the EAP SoC boots unsigned.
- SecForce: Reverse engineering router firmware, a solid primer on the general workflow.
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.