Reverse Engineering Samsung's HD+ TVkey Cloud

TIZEN NAGRA DVB-S2 REVERSE ENGINEERING
2026-04-07

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:

  1. Navigate to the Apps screen on the TV
  2. Enter 12345 on the remote — Developer Mode dialog appears
  3. Set the Host PC IP
  4. SDB port opens
SDB Connection
$ 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:

LibrarySizePurpose
libnagsam-mw.so6.3 MBCore middleware — ECM processing, key management
libnagsamsi.soSecurity interface — HSM abstraction
libnagsamasn.soASN.1 codec for license blob parsing
libnagsam_drm.soDRM integration with Tizen's content protection
libnagsamtransferagent.soNetwork transport — HTTPS to HD+ servers
libiptuner.soIP 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 Info
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):

ChunkSizeContent
main.js3.2 MBCore application logic
545.js4.9 MBUI components
910.js2.4 MBStreaming / player logic
897.js2.3 MBChannel management
558.js2.3 MBSettings / configuration
747.js1.6 MBInstallation / 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.

Decryption Flow
Satellite (Astra 19.2E)
  ↓  encrypted TS + ECMs
TV Tunernagsam 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:

bc_license (ASN.1 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)

Operator App — Retry Configuration
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:

Operator App — Channel Tune Check
// Called on every channel tune
if (!this.hasValidProduct()) {
  this.handleExpirationPage();  // → error overlay
  return false;
}

3. Network Errors Are Swallowed

Operator App — Player Configuration
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

Operator App — DRM Status Handler
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

Operator App — Azure IoT Hub Configuration
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

Failure Timeline
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)

Hidden Debug Codes

The operator app defines secret codes as React component defaultProps. Of these, only 59731 is publicly documented (it appears on the HD+ support portal as a troubleshooting step). The remaining codes — 95137, 73738, 882425, and 13795 — do not appear anywhere in public documentation, forums, or tech blogs. They are defined in the source but never surfaced to users:

Operator App — Secret Code Definitions
(0, n.A)(Qs, "defaultProps", {
  scrollAmount: 36,
  secretCodes: {
    unlink: "13795",
    licenseRenew: "59731",
    licenseReset: "95137",
    activationRenew: "73738",
    refreshProduct: "882425"
  }
});

Entering these on the remote's number pad while viewing a satellite channel triggers the corresponding action. The key input handler:

Operator App — Code Dispatch
switch(t) {
  case this.props.secretCodes.licenseRenew:
    this.runHiddenFunction("licenseRenew");
    break;
  case this.props.secretCodes.licenseReset:
    this.runHiddenFunction("licenseReset");
    break;
  case this.props.secretCodes.activationRenew:
    this.runHiddenFunction("activationRenew");
    break;
  case this.props.secretCodes.refreshProduct:
    this.update();
    break;
}

59731: Force License Renewal

License Renewal Call Chain
case "licenseRenew":
  e = function() { n.props.delegate.updateLicense(true) };
  break;

Traces to:

Call Chain
updateLicense(true)
  → opApp.acquireKeys(true)
    → caManager.update("user-request", true)
      → POST https://be.tvengine.hd-plus-cloud.de/license/pcs/online/uniqueEntitlement/v1

The true parameter bypasses the local cache, forcing an immediate server round-trip. This is the fix for the "Probleme bei HD+" error.

95137: License Reset

License Reset Handler
case "licenseReset":
  e = function() { n.props.delegate.resetLicense() };
  break;

Calls caManager.reset() which wipes the local bc_license blob from cloud_op_db, clears activationRenewalDate and licenseRenewalDate, and forces a complete re-acquisition on next channel tune.

73738: Activation Renewal

Activation Renewal Handler
case "activationRenew":
  e = function() {
    n.props.delegate.renewDeviceActivation(function(){}, function(){})
  };
  break;

Re-authenticates the Samsung account ↔ HD+ device binding via:

Registration Endpoints
POST https://appreg.hd-plus-cloud.de/api/tv/register/v1
POST https://be.tvengine.hd-plus-cloud.de/tv/product/
POST https://be.tvengine.hd-plus-cloud.de/api/v1/iot/registration

Note the empty success/failure callbacks — fire and forget.

> Quick Reference: All Codes
CodeFunctionWhen to use
59731Force license renewalMain fix for "Probleme bei HD+"
73738Re-authenticate device activationWhen 59731 doesn't help
95137Complete license resetLast resort — wipes all local state
882425Refresh subscription statusAfter subscription renewal/change
13795Unlink voucherOnly relevant for prepaid cards

Backend Infrastructure

EndpointPurpose
be.tvengine.hd-plus-cloud.deMain backend — license exchange, product status
betvip.hd-plus-cloud.deAlternative/VIP endpoint (same API)
appreg.hd-plus-cloud.de/api/tv/register/v1Device registration
init-gn.prd.tvengine.hd-plus-cloud.de/satInitial satellite config bootstrap
init-gn.stg.tvengine.hd-plus-cloud.de/satStaging environment
clientassets.prd.tvengine.hd-plus-cloud.de/opapp/Operator app updates (OTA)
app-service.prod.hd-plus-iptv-cloud.deIPTV variant (not used for satellite)
Azure IoT Hub (MQTT)Push notifications, 12h keepalive, 5-day authTTL

The critical endpoint for license renewal is:

License Renewal Endpoint
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:

Operator App — Error Code Definitions
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:

ErrorCodeMeaning
9activationRenewalDate not set
10licenseRenewalDate not set
>100 and <=999HTTP error from backend
100Install file not found

TVkey Cloud Configuration

Extracted from tvkey_list_downloaded.json and base64-decoded:

TVkeyCloudConfiguration (XML)
<?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

AspectDetail
TargetSamsung GQ50QN90AATXZG, Tizen 6.0, firmware T-NKM2DEUC-2301.1
AccessSDB (Samsung Debug Bridge) via Developer Mode, port 26101
Extractionsdb pull of /home/owner/, /opt/usr/apps/org.tizen.nagsam/, /opt/usr/data/
AnalysisJavaScript deobfuscation (webpack, unminified identifiers preserved), ASN.1 decoding, SQLite database analysis, LevelDB extraction
ToolsPython 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.