Reverse Engineering Samsung's HD+ TVkey Cloud
I pulled 260 MB of firmware from a Samsung Smart TV via SDB and traced the full conditional access pipeline — from satellite ECM to cloud key exchange to screen. What I found is a security architecture designed to kill card sharing, built on assumptions about WiFi reliability that don't hold up in practice.
Background
HD+ is Germany's premium satellite TV service — 25 private channels in HD (RTL, ProSieben, SAT.1, VOX, etc.) delivered via Astra 19.2E. Traditionally this required a CI+ module with a smartcard — hardware that was notoriously vulnerable to card sharing, where one module's decryption keys could be redistributed over the internet to unauthorized receivers.
Since ~2020, Samsung Smart TVs ship with TVkey Cloud — a purely software-based conditional access system built into the Tizen firmware. No hardware, no card. Instead, decryption keys are bound to a specific device (via its DUID) and Samsung account, exchanged over HTTPS, and valid for only ~5 days. The middleware uses NAGRA's on-chip security (NOCS) to decrypt in hardware and re-encrypt locally before passing content to the display pipeline — the control word never appears in a form that could be intercepted. Card sharing is architecturally impossible.
My in-laws' Samsung QN90A (2021, Tizen 6.0) would periodically lose all private HD channels. HD+ officially recommends logging out of the HD+ app and back in (Settings → Abmelden → Anmelden). There's also a known "Speicherbug" (memory bug) with its own troubleshooting PDF. These fixes work because they force the cloud to re-issue device credentials and fresh keys. But I wanted to understand why a system designed to replace bulletproof hardware keeps breaking, and what the actual key exchange looks like end to end.
So I connected via SDB and pulled the firmware.
Access Vector: Samsung Debug Bridge
Samsung Tizen TVs expose SDB (Samsung Debug Bridge) on port 26101 when Developer Mode is enabled. To activate:
- Navigate to the Apps screen on the TV
- Enter
12345on the remote — Developer Mode dialog appears - Set the Host PC IP
- SDB port opens
$ sdb connect 192.168.2.130:26101
connected to 192.168.2.130:26101
$ sdb capability
secure_protocol:enabled
intershell_support:disabled // no shell access
filesync_support:pushpull // but file pull works
profile_name:tv
platform_version:6.0
sdbd_version:2.2.31
Interactive shell is disabled on consumer TVs, but sdb pull works for the entire
accessible filesystem. I pulled /home/owner/ (user data, app storage, databases),
/opt/usr/ (system data, logs), and /etc/ — approximately 260 MB total.
The nagsam Middleware
The conditional access system is implemented by nagsam — a portmanteau of
NAGRA (the Swiss conditional access company) and Samsung. It lives at
/opt/usr/apps/org.tizen.nagsam/ and consists of:
| Library | Size | Purpose |
|---|---|---|
libnagsam-mw.so | 6.3 MB | Core middleware — ECM processing, key management |
libnagsamsi.so | — | Security interface — HSM abstraction |
libnagsamasn.so | — | ASN.1 codec for license blob parsing |
libnagsam_drm.so | — | DRM integration with Tizen's content protection |
libnagsamtransferagent.so | — | Network transport — HTTPS to HD+ servers |
libiptuner.so | — | IP tuner for IPTV variant |
The middleware is native C/C++ (ARM) — amenable to static analysis with Ghidra,
but the faster path was the HD+ Operator App that sits on top of it.
It's a JavaScript webapp that controls the entire CA lifecycle: key requests, retry
logic, error handling, and the hidden debug interface. The middleware itself is accessed
via the standard OIPF DRM Agent API (CAS system ID urn:dvb:casystemid:19300,
the registered DVB identifier for NAGRA conditional access).
The Operator App
Located at /opt/usr/apps/org.tizen.nagsam/op/{operator-uuid}/app/, the HD+ operator
app is a React application served as an HbbTV OpApp:
Build: 10282
Version: 3.1.12-prod
Git: aea37760a5a5e252193b07722b2b5af958cfbce6
Platform: samsung
Environment: prod
Six JavaScript chunks totaling ~16 MB (minified, not obfuscated — standard webpack output):
| Chunk | Size | Content |
|---|---|---|
main.js | 3.2 MB | Core application logic |
545.js | 4.9 MB | UI components |
910.js | 2.4 MB | Streaming / player logic |
897.js | 2.3 MB | Channel management |
558.js | 2.3 MB | Settings / configuration |
747.js | 1.6 MB | Installation / onboarding |
The Decryption Flow
The video payload comes from satellite (full quality, no internet bandwidth needed). Only the ECM → Control Word exchange goes over the internet. This is the key insight of TVkey Cloud: it gets full satellite quality without IPTV bandwidth costs, while making card sharing structurally impossible — the control words are bound to a device DUID and Samsung account JWT, validated server-side on every exchange. A shared key is worthless without the device it was issued to.
Satellite (Astra 19.2E)
↓ encrypted TS + ECMs
TV Tuner
↓
nagsam MW ← extracts ECM from transport stream
↓ ECM
Transfer Agent → HTTPS POST → tvengine.hd-plus-cloud.de
↓ /license/pcs/online/
uniqueEntitlement/v1
Validates:
- Samsung account JWT
- Device DUID
- Product entitlement (3050)
↓ Control Words
Descrambler (software)
↓ clear TS
Display
The bc_license Structure
The broadcast license is stored locally in /opt/usr/apps/org.tizen.nagsam/op/cloud_op_db
as an ASN.1 DER-encoded blob. Decoded:
{
"iad": "13573",
"productRights": [{
"productId": 3050,
"start": "2026-02-16T00:00:00Z",
"end": "2027-02-16T00:00:00Z",
"externalProductId": "3050"
}]
}
Product 3050 = HD+ annual subscription. The blob also contains two ASN.1 date fields defining the current license validity window: approximately 5 days. This short window is the anti-sharing mechanism: even if someone extracted the keys, they'd expire before they could be usefully redistributed. It's the compromise between security (short validity, frequent re-authentication) and offline tolerance (don't require the server on every channel change).
When this window expires, the middleware must contact the HD+ server to refresh. This is where the security design creates fragility.
The Bug: Why It Fails
1. Retry Logic (13 attempts, 4 minutes, give up forever)
retryTimeoutsMs: [1, 2, 5, 15, 30, 30, 30, 30, 30, 30, 30, 30, 30]
Thirteen retries with exponential-then-linear backoff, maxing at 30 seconds. Total window: approximately 4 minutes. After that, the app permanently gives up until user intervention. There is no hourly retry, no daily retry, no background job.
2. No Proactive Renewal
The license is only checked when hasValidProduct() is called — which happens
on every channel change. There is no background timer that renews the license
before expiry. The user discovers the failure when they tune to a channel:
// Called on every channel tune
if (!this.hasValidProduct()) {
this.handleExpirationPage(); // → error overlay
return false;
}
3. Network Errors Are Swallowed
ignoreNetworkErrorCloseTerminationS: 5
During playback, network errors are silently ignored for 5 seconds, then the player terminates. No retry, no graceful degradation.
4. Missing Dates = Instant Failure
if (!this.activationRenewalDate) {
A.Success = false;
A.Result.ErrorCode = 9;
}
if (!this.licenseRenewalDate) {
A.Success = false;
A.Result.ErrorCode = 10;
}
After a firmware update or license reset, these dates may be unset. The status check immediately fails with no recovery path.
5. IoT Hub Auth Window Matches License Window
authTTL: 432e5, // 432,000 ms = 5 days
keepalive: 43200, // 12 hours
connectTimeout: 1e4, // 10 seconds
reconnectTimeMs: 1e4 // 10 seconds
The Azure IoT Hub connection (used for push notifications) has an authTTL of exactly
5 days — matching the license window. If the MQTT connection's auth expires at the same
time as the license, both need renewal simultaneously. Double failure mode.
The Typical Failure Scenario
Day 0: License renewed. TV connected to main router (strong signal).
Day 3: TV roams to WiFi repeater (weaker signal, higher latency).
Day 5: License expires. nagsam attempts renewal.
WiFi roams back to main router mid-request. TCP connection drops.
Retry 1-13 over 4 minutes — all fail due to continued roaming.
nagsam gives up. Permanently.
Day 5+: User changes channel.
hasValidProduct() → false
Error overlay: "Probleme bei HD+"
User reboots TV. (Sometimes fixes it by triggering new DHCP + renewal)
Backend Infrastructure
| Endpoint | Purpose |
|---|---|
be.tvengine.hd-plus-cloud.de | Main backend — license exchange, product status |
betvip.hd-plus-cloud.de | Alternative/VIP endpoint (same API) |
appreg.hd-plus-cloud.de/api/tv/register/v1 | Device registration |
init-gn.prd.tvengine.hd-plus-cloud.de/sat | Initial satellite config bootstrap |
init-gn.stg.tvengine.hd-plus-cloud.de/sat | Staging environment |
clientassets.prd.tvengine.hd-plus-cloud.de/opapp/ | Operator app updates (OTA) |
app-service.prod.hd-plus-iptv-cloud.de | IPTV variant (not used for satellite) |
| Azure IoT Hub (MQTT) | Push notifications, 12h keepalive, 5-day authTTL |
The critical endpoint for license renewal is:
POST /license/pcs/online/uniqueEntitlement/v1
Host: be.tvengine.hd-plus-cloud.de
This is what 59731 triggers directly.
Error Code Mapping
From the application source:
errorCodes: {
generic: {
unknownError: 1e10,
timeoutError: 1000001e4
},
backend: {
missingCredentials: 90030001,
noValidEntitlement: 90810007,
noProductsCreatedForDevice: 90810131
},
conditionalAccess: {
caClient: {
downloadFailed: 620123e5,
profileExist: 620117e5,
noValidProduct: 6201252e4
}
},
player: {
productExpired: 140005e5,
mediaErrNetwork: 140003e5
}
}
Internal ErrorCodes from the DRM status handler:
| ErrorCode | Meaning |
|---|---|
9 | activationRenewalDate not set |
10 | licenseRenewalDate not set |
>100 and <=999 | HTTP error from backend |
100 | Install file not found |
TVkey Cloud Configuration
Extracted from tvkey_list_downloaded.json and base64-decoded:
<?xml version="1.0" encoding="UTF-8"?>
<tvkey:TVkeyCloudConfiguration
xmlns:tvkey="urn:samsung:TVkeyCloud:smarttv:2018">
<tvkey:TVkeyApplicationDescriptor>
<tvkey:TVkeyProfileType>2</tvkey:TVkeyProfileType>
<tvkey:LaunchUrl>
https://samsung-installer.tvengine.hd-plus-cloud.de/index.html
?configuration=samsung_onboarding
</tvkey:LaunchUrl>
</tvkey:TVkeyApplicationDescriptor>
<tvkey:TVkeyApplicationConditionalLaunchCriteria>
<tvkey:PhysicalTuners scanned="true" SatelliteNames="19.2E">
DVB-S
</tvkey:PhysicalTuners>
<tvkey:OperatorUUIDs>
4edc3629-db62-421e-a7cf-85f76c3da6db
</tvkey:OperatorUUIDs>
<tvkey:IncludedCountries>DEU</tvkey:IncludedCountries>
</tvkey:TVkeyApplicationConditionalLaunchCriteria>
</tvkey:TVkeyCloudConfiguration>
TVkey Cloud is not limited to HD+ or Germany. The TVkeyCloudConfiguration schema
(namespace urn:samsung:TVkeyCloud:smarttv:2018) supports any operator UUID and country.
NAGRA deploys this system globally for various satellite and IPTV providers.
The Security / Reliability Tradeoff
TVkey Cloud is fundamentally an anti-piracy architecture. CI+ modules were vulnerable to card sharing — one physical module's keys redistributed to thousands of unauthorized receivers over the internet. NAGRA's cloud-based design eliminates this entirely: keys are device-bound, account-authenticated, short-lived, and server-validated. You cannot share what requires your specific hardware to use.
But every security property creates a reliability constraint:
- Device-bound keys — no fallback if the device can't reach the server
- 5-day validity window — short enough to prevent redistribution, short enough to break on a bad WiFi week
- Server-side validation — every renewal is a network round-trip that can fail
- Account binding — logout/login (the official fix) works precisely because it forces credential re-issuance
The implementation compounds this with aggressive timeouts (4 minutes, then permanent failure), no proactive renewal before expiry, and silent error handling. The result is a house of cards: the security model assumes reliable connectivity, the retry logic assumes transient failures, and the average German household has a WiFi repeater that causes neither-transient-nor-permanent roaming gaps at exactly the wrong moment.
The primary fix (59731) is listed on the
HD+ support portal
but buried in troubleshooting steps. The escalation codes (73738,
95137) are completely undocumented — they exist only in the app source.
The underlying architecture tradeoffs remain unchanged in the 2026 firmware.
Methodology
| Aspect | Detail |
|---|---|
| Target | Samsung GQ50QN90AATXZG, Tizen 6.0, firmware T-NKM2DEUC-2301.1 |
| Access | SDB (Samsung Debug Bridge) via Developer Mode, port 26101 |
| Extraction | sdb pull of /home/owner/, /opt/usr/apps/org.tizen.nagsam/, /opt/usr/data/ |
| Analysis | JavaScript deobfuscation (webpack, unminified identifiers preserved), ASN.1 decoding, SQLite database analysis, LevelDB extraction |
| Tools | Python 3, sqlite3, SDB 4.2.36 (from Tizen Studio standalone package) |
The nagsam native libraries (.so) were not reverse engineered for this article.
All findings are from the JavaScript operator app and the data stores it writes to.
Full firmware extraction was not performed — only user-accessible paths via SDB.
Tested on Samsung GQ50QN90AATXZG (2021). The TVkey Cloud architecture is shared across Samsung Smart TVs from 2020 onwards. Secret codes are defined in the operator app and may vary by version, but 59731 has been consistent across observed builds.