Media and DRM
Published
A web player is one of the most commonly obfuscated kinds of JavaScript there is, and the reason is usually a studio contract. That makes this the area where the interaction is worth measuring most carefully, because the playback rules the contract cares about are not in the encrypted stream - they are property names in a configuration object handed to a content decryption module the player did not compile. We protected a file that configures one, renamed the names a group at a time, and looked at what played.
What the sample actually does
The file configures playback the way a service under a studio contract configures it. A real key system is requested rather than the clear-key mechanism. The robustness level demands hardware-backed decryption, so a software module cannot receive 4K keys. Output protection is required. Licenses may not be written to disk, because a rental that persists is a rental that does not end. Falling back to unencrypted playback when DRM is unavailable is refused. A ceiling allows standard definition through an unprotected output but nothing above it. And the service supplies its own license check, which binds the license to this title and this device.
Four devices exercise those guards separately, so each one is decided by the guard it belongs to. The software-module laptop supports the key system but decrypts in software. The set-top box supports no key system at all. The capture-card device is fully hardware-backed but has no output protection. The certified television satisfies everything, so the safe path is measured too.
The player library is copied in unprotected, for the usual reason. Its defaults are the permissive ones this family ships: the clear-key mechanism, any content decryption module, no output protection required, licenses persisted, unencrypted fallback allowed, no resolution ceiling, and a license check that confirms the server sent a non-empty response.
Protection alone was applied first on all five presets and all five behaved identically to the unprotected file. Nothing below is caused by protection on its own.
The 4K title that played with no DRM, on a device that supports it
Renaming the key-system option reverted it to the clear-key mechanism, which is the specification's mechanism for handing the decryption key to the browser in the clear. On the certified television - a device that fully supports real DRM and was about to use it - the title played.
The measured lines read certified-device=playing, protection=none, resolution=2160p, and the record of what was played changed from the title under a real key system to the title in the clear.
Nothing failed and nothing was refused. The player started, the picture appeared, the resolution was correct, and every telemetry line a playback dashboard collects - start time, rebuffer ratio, bitrate, completion - would be normal or better, because the decryption path got simpler. The only thing that changed is that a premium title was delivered to a device with no protection on it, which is the precise condition the contract exists to prevent and the precise condition an audit of playback quality cannot see.
The same edit made the set-top box arm fail loudly instead, because with no key system requested the robustness demand had nothing to satisfy it. That mix is characteristic: the arm that fails closed is on the device that was never going to play the title anyway, and the arm that fails open is on the device with a real customer in front of it.
Two output-protection options, and neither half is safe
Output protection is expressed twice: a required protection level, and a ceiling on the resolution allowed through an output that lacks it. Together they say 4K needs a protected path, standard definition does not.
Renaming the protection level alone opened UHD to the capture-card device. Renaming the ceiling alone opened UHD to the same device by the other route, since the library's default ceiling does not restrict anything. Both halves individually produced uhd-to-unprotected-output=true.
This is the inverse of the pattern this series usually finds, where each half is safe and the pair is open. Here there is no safe half - the rule is a conjunction, and losing either operand satisfies it. Both shapes exist in real configuration objects and they are not distinguishable by inspection; you have to know which way the library combines them, which is exactly the knowledge that does not survive a staff change.
The standard-definition case still played correctly in every arm, which matters: the ceiling is not a check that simply refuses everything, so an arm that plays is not by itself evidence the ceiling survived.
A license that was never checked, and a rental that never ends
Renaming the option carrying the service's own license check substituted the library's builtin, which confirms the response body is not empty. A license lifted from a different session for a different title - real, well-formed, non-empty - was accepted and the title played. This is the eleventh library in this series where a caller-supplied guard was downgraded rather than removed, and as always the configuration still reports license verification as enabled.
Refusing unencrypted fallback reverted to allowing it, and the set-top box with no DRM support at all played the title in the clear rather than being refused. Refusing persistent licenses reverted to requiring them, and the license was written to the customer's disk - which for a 48-hour rental means the rental window is now bounded by whatever clears that store, not by the service.
The robustness demand reverted to accepting any content decryption module, and the software-decryption laptop played the 4K title. That is the arm most likely to appear in a contract dispute rather than a security review: the tier restriction is a commercial term, and the evidence that it was breached is a log line on the studio's side, not yours.
The license request, and the one group that fails closed
The license request the player sends is a small object whose field names are read by the license server - a different program, on a different machine, run by a different team or a different company.
Renaming those field names produced license-request=_0x1=dev-tv-01&_0x2=tt-4417&_0x3=sub-premium. The request is still well-formed. Every value is still present and correct. The server simply receives three keys it has never heard of, and a license server that cannot identify the content, the device or the entitlement issues nothing.
In this fixture that failed closed: playback stopped for everyone, including the legitimate case, which is an outage you find in minutes. That is the direction a field name fails when the receiving side reads it and compares it. It is worth contrasting with the same edit on the way in, elsewhere in this series, where a renamed field is simply accepted and the write lands beside the real one - and worth noting that a license server which tolerates unknown fields and falls back to a default entitlement would turn this loud arm into a silent one.
The device capability object crashed outright when renamed, with a type error from the unprotected library reading a property that no longer exists. That arm was re-run without the colliding name to check it was not hiding a quieter result behind the crash; the remainder failed closed too, refusing playback on every device. A crashing arm is not a safe arm, and the only way to know is to re-run the group without whatever is crashing it.
One arm measured no change: the initialisation data type, pinned deliberately to a value identical to the library default as a control.
What to do about it
The mechanism is not specific to media and it is not a defect in the obfuscator. Member renaming rewrites property names inside the code it is given. A content decryption module is not merely outside that code - it is outside the browser's JavaScript entirely, and the configuration object is the only channel you have to it.
That makes the scoping advice unusually sharp for this area. Scope RenameMembers with a MemberRegexp that excludes the key-system configuration, the playback rules and the license-request shape. There is very little value in renaming them anyway: they are specification-defined names an attacker already knows, and the thing worth protecting in a player is the logic around them, not the names themselves.
Assert the negotiated configuration at start-up rather than trusting the requested one. The Encrypted Media Extensions API hands back the configuration that was actually agreed, which is the reading from the far side of the boundary and the only one with evidential value. Compare the key system, the robustness level and the output-protection requirement in that returned object against what you asked for, and refuse to start playback if they differ.
Then add one behavioural check that no configuration audit can substitute for: play a protected title on a device known not to satisfy the rules, and assert that it does not play. Every arm in this article except the loud ones produces normal-looking playback with normal-looking telemetry, so a dashboard measuring startup time and rebuffering will report every one of them as a healthy stream - several of them as an unusually good one.
Frequently asked questions
Does protecting my JavaScript break DRM playback on its own?
Not in this measurement. The sample was protected on five profiles covering both output targets, the gate profile and the compressed profile, and all five behaved identically to the unprotected file. Every result required member renaming pointed at property names.
What was the worst result?
Renaming the key-system option. It reverted to the clear-key mechanism, and a 4K title played on a certified device with no protection at all. Playback looked completely normal - correct resolution, better startup, clean telemetry - which is why a playback-quality dashboard cannot see it.
Our player is obfuscated because the studio contract requires it. Does this mean we should not obfuscate?
No. It means the configuration object should be outside the rename scope. There is little to gain from renaming specification-defined names an attacker already knows, and a great deal to lose if the content decryption module stops recognising them. Protect the logic around the configuration, not the configuration.
Both output-protection options failed on their own. Is that unusual?
It is the inverse of the more common pattern. The rule is a conjunction - a required protection level and a ceiling for outputs that lack it - so losing either operand satisfies it. There is no safe half here, unlike the paired guards elsewhere in this series where each half holds and only the pair opens.
Was license verification switched off?
Downgraded, not switched off. The service's own check binds a license to a title and a device; the library's builtin confirms the response is non-empty. A license lifted from another session for another title was accepted, and the configuration still reported license verification as enabled.
Would a persistent license really matter?
For a rental, yes. Refusing persistent licenses reverted to requiring them, so the license was written to the customer's disk. The rental window is then bounded by whatever eventually clears that store rather than by the service, and nothing in the playback path reports it as unusual.
What is the smallest change that prevents all of this?
Scope RenameMembers to exclude the key-system configuration, the playback rules and the license-request shape, then assert at start-up that the negotiated configuration the API hands back matches what you requested. Add one test that plays a protected title on a non-compliant device and asserts it does not play.
Related reading