How to encrypt WebM or MP4 file using ClearKey and then play it

Xander picture Xander · Nov 10, 2015 · Viewed 9.1k times · Source

I'm currently researching the subject of encrypting and playing encrypted videos in browser. I already have some successes with castlabs' DRMToday and Shaka Player while using Widevine.

Now I'm trying to encrypt video without external services using ClearKey and play it in Chrome (using whatever js player which can handle).

I did manage to encrypt single mp4 file using MP4Box (and mse-eme for creating crypt configuration) but I have no idea how to play it in browser. HTML5's Video didn't even trigger "encrypted" event on it. Encryption itself works fine - I was able to decrypt it back using the same tool with proper key.

I tried to create a DASH out of this encrypted file and play it in Shaka Player. I created manifest using MP4Box. I had to manually add a missing xmlns to this file (xmlns:cenc="urn:mpeg:cenc:2013") so DOMParser parses it properly. I don't know how should I handle the license.

I found few working examples of playing encoded webm files (including Shaka Player's demo page). How can I encrypt webm file? I did found https://github.com/webmproject/webm-tools but it seems to require building entire Chromium in order to work.

Are there any other tools that can encrypt webm files?

Answer

Whome picture Whome · Oct 23, 2017

This is my set of files to test ClearKey DRM playback.

mp4box(gpac) drm.xml specification file where you could give one or more PSSH tables to be generated inside the init.mp4 segments.

<?xml version="1.0" encoding="UTF-8" ?>
<GPACDRM type="CENC AES-CTR">
<!-- 
  kid=0x43215678123412341234123412341234
  key=0x12341234123412341234123412341234
  iv=0x22ee7d4745d3a26a
--> 

<!-- CENC -->
<DRMInfo type="pssh" version="1">
  <BS ID128="1077efecc0b24d02ace33c1e52e2fb4b"/>
  <BS bits="32" value="1"/>
  <BS ID128="43215678123412341234123412341234"/>
</DRMInfo>

<CrypTrack trackID="1" IsEncrypted="1" IV_size="8" first_IV="0x22ee7d4745d3a26a" saiSavedBox="senc">
  <key KID="0x43215678123412341234123412341234" value="0x12341234123412341234123412341234"/>
</CrypTrack>

</GPACDRM>

Command lines to encrypt video+audio and split segments.

MP4Box.exe -crypt gpacdrm.xml temp-v1.mp4 -out ./drm/temp-v1.mp4
MP4Box.exe -crypt gpacdrm.xml temp-a1.mp4 -out ./drm/temp-a1.mp4
MP4Box.exe -dash 6000 -frag 6000 -mem-frags -rap -profile dashavc264:live -profile-ext urn:hbbtv:dash:profile:isoff-live:2012 -min-buffer 3000  -bs-switching no -sample-groups-traf -single-traf -subsegs-per-sidx 1 -segment-name $RepresentationID$_$Number$$Init=i$ -segment-timeline -out manifest.mpd temp-v1.mp4#trackID=1:id=v1:period=p0 temp-a1.mp4#trackID=1:id=a1:period=p0

ShakaPlayer standalone demo for ClearKey playback, use Chrome or Firefox. I found this source code in an internet so credits to whoever did it.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/2.1.0/shaka-player.compiled.js"></script>
    <title>MPEG-DASH Player Test</title>
    <script>
        var manifestUrl = 'https://my.server.com/drm/manifest_clearkey.mpd';
        var laUrl       = 'https://my.server.com/drm/laurl_ck.php';

        function initApp() {
            // Install built-in polyfills to patch browser incompatibilities.
            shaka.polyfill.installAll();

            // Check to see if the browser supports the basic APIs Shaka needs.
            if (shaka.Player.isBrowserSupported()) {
                // Everything looks good!
                initPlayer();
            } else {
                // This browser does not have the minimum set of APIs we need.
                console.error('Browser not supported!');
            }
        }

        function initPlayer() {
            // Create a Player instance.
            var video = document.getElementById('video');
            var player = new shaka.Player(video);

            // Configue
            player.configure({
                drm: {
                    servers: {
                        'org.w3.clearkey': laUrl
                    },
                    clearKeys: {
                        //'kid': 'key'
                    }
                }
            });

            // Attach player to the window to make it easy to access in the JS console.
            window.player = player;

            // Listen for error events.
            player.addEventListener('error', onErrorEvent);

            // Try to load a manifest.
            // This is an asynchronous process.
            player.load(manifestUrl).then(function () {
                // This runs if the asynchronous load is successful.
                console.log('The video has now been loaded!');
            }).catch(onError);  // onError is executed if the asynchronous load fails.
        }

        function onErrorEvent(event) {
            // Extract the shaka.util.Error object from the event.
            onError(event.detail);
        }

        function onError(error) {
            console.error('Error code', error.code, 'object', error);
            alert("ErrorCode="+error.code);
        }

        document.addEventListener('DOMContentLoaded', initApp);
    </script>
</head>
<body>
    <video id="video" autoplay controls></video>
</body>
</html>

ClearKey DRM "license server php script", player sends a json document and this script returns KID=KEY pairing.

<?php
header( "Expires: Mon, 20 Dec 1998 01:00:00 GMT" );
header( "Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT" );
header( "Cache-Control: no-cache, must-revalidate" );
header( "Pragma: no-cache" );
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: origin,range,accept,accept-encoding,referer,content-type, SOAPAction,X-AxDRM-Message');
header('Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST');
header('Access-Control-Expose-Headers: server,range,content-range,content-length,content-type');
// write content-type header after OPTIONS check
// ClearKey DRM server

// some players may submit OPTIONS request(zero-length) before POST drm.xml submit
if ($_SERVER['REQUEST_METHOD']=="OPTIONS") {
    header("Content-Length: 0");
    header("Expires: -1");
    return;
}
//header('Content-Type: text/plain; charset=utf-8');
header('Content-Type: application/json; charset=utf-8');

// Request may have one or more KIDs(base64), read first KID from the request for now.
// KID base64 is without trailing "=" padding chars.
// Request : {"kids":["QyFWeBI0EjQSNBI0EjQSNA"],"type":"temporary"}
// Response: {"keys": [{"k": "EjQSNBI0EjQSNBI0EjQSNA", "kty": "oct", "kid": "QyFWeBI0EjQSNBI0EjQSNA" }], "type": "temporary"}
$req = file_get_contents('php://input'); // read POST bodypart
$json= json_decode($req);
$kidb= $json->{"kids"}[0]; // base64 format
$kid = bin2hex(base64_decode($kidb, true)); // hex format

// KID=KEY lookup table, find KEY and base64(trim trailing "==" chars)
// "EjQSNBI0EjQSNBI0EjQSNA==" -> "EjQSNBI0EjQSNBI0EjQSNA"
$keys = array(
  "43215678123412341234123412341234" => "12341234123412341234123412341234",
  "43215678123412341234123412341235" => "12341234123412341234123412341235",
  "43215678123412341234123412341236" => "12341234123412341234123412341236",
  "43215678123412341234123412341237" => "12341234123412341234123412341237",
  "43215678123412341234123412341238" => "12341234123412341234123412341238"
);
$key = base64_encode(hex2bin($keys[$kid]));
$key = str_replace("=", "", $key);

$data = "{\"keys\": [{\"k\": \$key, \"kty\": \"oct\", \"kid\": \$kid }], \"type\": \"temporary\"}";
$data = str_replace("\$key", "\"".$key."\"", $data);
$data = str_replace("\$kid", "\"".$kidb."\"", $data);

echo $data;

?>

Manifest with CENC and ClearKey contentprotection elements.

<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:cenc="urn:mpeg:cenc:2013" xmlns:mas="urn:marlin:mas:1-0:services:schemas:mpd" xmlns:mspr="urn:microsoft:playready" maxSegmentDuration="PT0H0M6.000S" mediaPresentationDuration="PT0H1M30.000S" minBufferTime="PT3.000S" profiles="urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash264,urn:hbbtv:dash:profile:isoff-live:2012" type="static">


 <Period duration="PT0H1M30.000S" id="p0">
  <AdaptationSet lang="und" maxFrameRate="25" maxHeight="360" maxWidth="640" par="16:9" segmentAlignment="true" startWithSAP="1">
   <ContentProtection cenc:default_KID="43215678-1234-1234-1234-123412341234" schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
  <cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAFDIVZ4EjQSNBI0EjQSNBI0AAAAAA==</cenc:pssh>
</ContentProtection>
   <SegmentTemplate initialization="$RepresentationID$_i.mp4" media="$RepresentationID$_$Number$.m4s" startNumber="1" timescale="1000">
    <SegmentTimeline>
     <S d="6000" r="14" t="0"/>
    </SegmentTimeline>
   </SegmentTemplate>
   <Representation bandwidth="491773" codecs="avc1.4D4028" frameRate="25" height="360" id="v1" mimeType="video/mp4" sar="1:1" width="640">
   </Representation>
  </AdaptationSet>
  <AdaptationSet lang="und" segmentAlignment="true" startWithSAP="1">
   <ContentProtection cenc:default_KID="43215678-1234-1234-1234-123412341234" schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
  <cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAFDIVZ4EjQSNBI0EjQSNBI0AAAAAA==</cenc:pssh>
</ContentProtection>
   <SegmentTemplate initialization="$RepresentationID$_i.mp4" media="$RepresentationID$_$Number$.m4s" startNumber="1" timescale="1000">
    <SegmentTimeline>
     <S d="5973" t="0"/>
     <S d="5995" r="1"/>
     <S d="5994"/>
     <S d="5995" r="1"/>
     <S d="5994"/>
     <S d="5995"/>
     <S d="5994"/>
     <S d="5995" r="2"/>
     <S d="5994"/>
     <S d="5995" r="1"/>
     <S d="101"/>
    </SegmentTimeline>
   </SegmentTemplate>
   <Representation audioSamplingRate="48000" bandwidth="133119" codecs="mp4a.40.2" id="a1" mimeType="audio/mp4">
    <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
   </Representation>
  </AdaptationSet>
 </Period>
</MPD>

Manifest cenc:pssh contains KID values and is easy to generate if just one key is used. See this base64tohexdump of pssh element value.

00 00 00 34 70 73 73 68 01 00 00 00 
10 77 EF EC C0 B2 4D 02 AC E3 3C 1E 52 E2 FB 4B 
00 00 00 01 
43 21 56 78 12 34 12 34 12 34 12 34 12 34 12 34 
00 00 00 00