4dc56848 by Gary Katsevman Committed by David LaPalomento

Encrypted HLS working on the example page

Used an adaptive, encrypted m3u8 on the example page and included hex dump utilities. Fixed formatting issues with the hex dump utility. Passed a 16-byte IV to the decrypter in drainBuffer(). Swapped byte order for the keys so they are not misintrepreted as little-endian. Added fields to the muxer test page to mux encrypted segments. Updated tests. Fixed docs on using openssl to test encryption and decryption.
1 parent 2ce0fbcb
...@@ -6,7 +6,7 @@ The [HLS spec](http://tools.ietf.org/html/draft-pantos-http-live-streaming-13#se ...@@ -6,7 +6,7 @@ The [HLS spec](http://tools.ietf.org/html/draft-pantos-http-live-streaming-13#se
6 # since this is for testing, skip the key salting so the output is stable 6 # since this is for testing, skip the key salting so the output is stable
7 # using -nosalt outside of testing is a terrible idea! 7 # using -nosalt outside of testing is a terrible idea!
8 echo -n "hello" | pkcs7 | \ 8 echo -n "hello" | pkcs7 | \
9 openssl enc -aes-128-cbc -nopad -nosalt -k $KEY -iv $IV > hello.encrypted 9 openssl enc -aes-128-cbc -nopad -nosalt -K $KEY -iv $IV > hello.encrypted
10 10
11 # xxd is a handy way of translating binary into a format easily consumed by 11 # xxd is a handy way of translating binary into a format easily consumed by
12 # javascript 12 # javascript
...@@ -16,5 +16,5 @@ xxd -i hello.encrypted ...@@ -16,5 +16,5 @@ xxd -i hello.encrypted
16 Later, you can decrypt it: 16 Later, you can decrypt it:
17 17
18 ```sh 18 ```sh
19 openssl enc -d -nopad -aes-128-cbc -k $KEY -iv $IV 19 openssl enc -d -nopad -aes-128-cbc -K $KEY -iv $IV
20 ``` 20 ```
......
...@@ -28,6 +28,11 @@ ...@@ -28,6 +28,11 @@
28 <script src="src/m3u8/m3u8-parser.js"></script> 28 <script src="src/m3u8/m3u8-parser.js"></script>
29 <script src="src/playlist-loader.js"></script> 29 <script src="src/playlist-loader.js"></script>
30 30
31 <script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
32 <script src="src/decrypter.js"></script>
33
34 <script src="src/bin-utils.js"></script>
35
31 <!-- example MPEG2-TS segments --> 36 <!-- example MPEG2-TS segments -->
32 <!-- bipbop --> 37 <!-- bipbop -->
33 <!-- <script src="test/tsSegment.js"></script> --> 38 <!-- <script src="test/tsSegment.js"></script> -->
...@@ -58,8 +63,11 @@ ...@@ -58,8 +63,11 @@
58 width="600" 63 width="600"
59 controls> 64 controls>
60 <source 65 <source
61 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8" 66 src="http://solutions.brightcove.com/jwhisenant/hls/encrypted/brightcove/oceans/oceans_aes-remote-key.m3u8"
62 type="application/x-mpegURL"> 67 type="application/x-mpegURL">
68 <!--<source-->
69 <!--src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"-->
70 <!--type="application/x-mpegURL">-->
63 </video> 71 </video>
64 <script> 72 <script>
65 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf'; 73 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
......
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
4 var 4 var
5 bytes = Array.prototype.slice.call(data), 5 bytes = Array.prototype.slice.call(data),
6 step = 16, 6 step = 16,
7 formatHexString = function(e) { 7 formatHexString = function(e, i) {
8 var value = e.toString(16); 8 var value = e.toString(16);
9 return "00".substring(0, 2 - value.length) + value; 9 return "00".substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
10 }, 10 },
11 formatAsciiString = function(e) { 11 formatAsciiString = function(e) {
12 if (e > 32 && e < 125) { 12 if (e >= 0x20 && e < 0x7e) {
13 return String.fromCharCode(e); 13 return String.fromCharCode(e);
14 } 14 }
15 return '.'; 15 return '.';
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
18 hex, 18 hex,
19 ascii; 19 ascii;
20 for (var j = 0; j < bytes.length / step; j++) { 20 for (var j = 0; j < bytes.length / step; j++) {
21 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(' '); 21 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
22 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 22 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
23 result += hex + ' ' + ascii + '\n'; 23 result += hex + ' ' + ascii + '\n';
24 } 24 }
......
...@@ -214,11 +214,12 @@ decrypt = function(encrypted, key, initVector) { ...@@ -214,11 +214,12 @@ decrypt = function(encrypted, key, initVector) {
214 var 214 var
215 encryptedView = new DataView(encrypted.buffer), 215 encryptedView = new DataView(encrypted.buffer),
216 platformEndian = new Uint32Array(encrypted.byteLength / 4), 216 platformEndian = new Uint32Array(encrypted.byteLength / 4),
217 decipher = new AES(key), 217 decipher = new AES(Array.prototype.slice.call(key)),
218 decrypted = new Uint8Array(encrypted.byteLength), 218 decrypted = new Uint8Array(encrypted.byteLength),
219 decryptedView = new DataView(decrypted.buffer), 219 decryptedView = new DataView(decrypted.buffer),
220 decryptedBlock, 220 decryptedBlock,
221 word, byte; 221 word,
222 byte;
222 223
223 // convert big-endian input to platform byte order for decryption 224 // convert big-endian input to platform byte order for decryption
224 for (byte = 0; byte < encrypted.byteLength; byte += 4) { 225 for (byte = 0; byte < encrypted.byteLength; byte += 4) {
......
...@@ -379,8 +379,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -379,8 +379,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
379 responseType: 'arraybuffer', 379 responseType: 'arraybuffer',
380 withCredentials: settings.withCredentials 380 withCredentials: settings.withCredentials
381 }, function(error, url) { 381 }, function(error, url) {
382 var tags;
383
384 // the segment request is no longer outstanding 382 // the segment request is no longer outstanding
385 tech.segmentXhr_ = null; 383 tech.segmentXhr_ = null;
386 384
...@@ -420,7 +418,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -420,7 +418,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
420 mediaIndex: tech.mediaIndex, 418 mediaIndex: tech.mediaIndex,
421 playlist: tech.playlists.media(), 419 playlist: tech.playlists.media(),
422 offset: offset, 420 offset: offset,
423 bytes: this.response 421 bytes: new Uint8Array(this.response)
424 }); 422 });
425 tech.drainBuffer(); 423 tech.drainBuffer();
426 424
...@@ -464,6 +462,14 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -464,6 +462,14 @@ videojs.Hls.prototype.drainBuffer = function(event) {
464 return segmentBuffer.shift(); 462 return segmentBuffer.shift();
465 } else if (!segment.key.bytes) { 463 } else if (!segment.key.bytes) {
466 return; 464 return;
465 } else {
466 // if the media sequence is greater than 2^32, the IV will be incorrect
467 // assuming 10s segments, that would be about 1300 years
468 bytes = videojs.Hls.decrypt(bytes,
469 segment.key.bytes,
470 new Uint32Array([
471 0, 0, 0,
472 mediaIndex + playlist.mediaSequence]));
467 } 473 }
468 } 474 }
469 475
...@@ -483,7 +489,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -483,7 +489,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
483 } 489 }
484 490
485 // transmux the segment data from MP2T to FLV 491 // transmux the segment data from MP2T to FLV
486 this.segmentParser_.parseSegmentBinaryData(new Uint8Array(bytes)); 492 this.segmentParser_.parseSegmentBinaryData(bytes);
487 this.segmentParser_.flushTags(); 493 this.segmentParser_.flushTags();
488 494
489 tags = []; 495 tags = [];
...@@ -528,7 +534,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -528,7 +534,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
528 }; 534 };
529 535
530 videojs.Hls.prototype.fetchKeys = function(playlist, index) { 536 videojs.Hls.prototype.fetchKeys = function(playlist, index) {
531 var i, key, tech, player, settings; 537 var i, key, tech, player, settings, view;
532 538
533 // if there is a pending XHR or no segments, don't do anything 539 // if there is a pending XHR or no segments, don't do anything
534 if (keyXhr || !playlist.segments) { 540 if (keyXhr || !playlist.segments) {
...@@ -559,7 +565,13 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) { ...@@ -559,7 +565,13 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) {
559 return; 565 return;
560 } 566 }
561 567
562 key.bytes = this.response || new Uint8Array([1]); 568 view = new DataView(this.response);
569 key.bytes = new Uint32Array([
570 view.getUint32(0),
571 view.getUint32(4),
572 view.getUint32(8),
573 view.getUint32(12)
574 ]);
563 tech.fetchKeys(playlist, i++, url); 575 tech.fetchKeys(playlist, i++, url);
564 }); 576 });
565 break; 577 break;
......
...@@ -36,7 +36,7 @@ module('Decryption'); ...@@ -36,7 +36,7 @@ module('Decryption');
36 36
37 test('decrypts a single AES-128 with PKCS7 block', function() { 37 test('decrypts a single AES-128 with PKCS7 block', function() {
38 var 38 var
39 key = [0, 0, 0, 0], 39 key = new Uint32Array([0, 0, 0, 0]),
40 initVector = key, 40 initVector = key,
41 // the string "howdy folks" encrypted 41 // the string "howdy folks" encrypted
42 encrypted = new Uint8Array([ 42 encrypted = new Uint8Array([
...@@ -52,7 +52,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() { ...@@ -52,7 +52,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() {
52 52
53 test('decrypts multiple AES-128 blocks with CBC', function() { 53 test('decrypts multiple AES-128 blocks with CBC', function() {
54 var 54 var
55 key = [0, 0, 0, 0], 55 key = new Uint32Array([0, 0, 0, 0]),
56 initVector = key, 56 initVector = key,
57 // the string "0123456789abcdef01234" encrypted 57 // the string "0123456789abcdef01234" encrypted
58 encrypted = new Uint8Array([ 58 encrypted = new Uint8Array([
......
...@@ -43,11 +43,21 @@ ...@@ -43,11 +43,21 @@
43 <section> 43 <section>
44 <h2>Inputs</h2> 44 <h2>Inputs</h2>
45 <form id="inputs"> 45 <form id="inputs">
46 <fieldset>
46 <label> 47 <label>
47 Your original MP2T segment: 48 Your original MP2T segment:
48 <input type="file" id="original"> 49 <input type="file" id="original">
49 </label> 50 </label>
50 <label> 51 <label>
52 Key (optional):
53 <input type="text" id="key">
54 </label>
55 <label>
56 IV (optional):
57 <input type="text" id="iv">
58 </label>
59 </fieldset>
60 <label>
51 A working, FLV version of the underlying stream 61 A working, FLV version of the underlying stream
52 produced by another tool: 62 produced by another tool:
53 <input type="file" id="working"> 63 <input type="file" id="working">
...@@ -105,11 +115,15 @@ ...@@ -105,11 +115,15 @@
105 <script src="../../src/h264-stream.js"></script> 115 <script src="../../src/h264-stream.js"></script>
106 <script src="../../src/aac-stream.js"></script> 116 <script src="../../src/aac-stream.js"></script>
107 <script src="../../src/segment-parser.js"></script> 117 <script src="../../src/segment-parser.js"></script>
118 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
119 <script src="../../src/decrypter.js"></script>
108 120
109 <script src="../../src/bin-utils.js"></script> 121 <script src="../../src/bin-utils.js"></script>
110 <script> 122 <script>
111 var inputs = document.getElementById('inputs'), 123 var inputs = document.getElementById('inputs'),
112 original = document.getElementById('original'), 124 original = document.getElementById('original'),
125 key = document.getElementById('key'),
126 iv = document.getElementById('iv'),
113 working = document.getElementById('working'), 127 working = document.getElementById('working'),
114 128
115 vjsTags = document.querySelector('.vjs-tags'), 129 vjsTags = document.querySelector('.vjs-tags'),
...@@ -132,6 +146,7 @@ ...@@ -132,6 +146,7 @@
132 var parser = new videojs.Hls.SegmentParser(), 146 var parser = new videojs.Hls.SegmentParser(),
133 tags = [parser.getFlvHeader()], 147 tags = [parser.getFlvHeader()],
134 tag, 148 tag,
149 bytes,
135 hex, 150 hex,
136 li, 151 li,
137 byteLength = 0, 152 byteLength = 0,
...@@ -142,7 +157,20 @@ ...@@ -142,7 +157,20 @@
142 // clear old tag info 157 // clear old tag info
143 vjsTags.innerHTML = ''; 158 vjsTags.innerHTML = '';
144 159
145 parser.parseSegmentBinaryData(new Uint8Array(reader.result)); 160 // optionally, decrypt the segment
161 if (key.value && iv.value) {
162 bytes = videojs.Hls.decrypt(new Uint8Array(reader.result),
163 key.value.match(/([0-9a-f]{8})/gi).map(function(e) {
164 return parseInt(e, 16);
165 }),
166 iv.value.match(/([0-9a-f]{8})/gi).map(function(e) {
167 return parseInt(e, 16);
168 }));
169 } else {
170 bytes = new Uint8Array(reader.result);
171 }
172
173 parser.parseSegmentBinaryData(bytes);
146 174
147 // collect all the tags 175 // collect all the tags
148 while (parser.tagsAvailable()) { 176 while (parser.tagsAvailable()) {
......
...@@ -29,6 +29,7 @@ var ...@@ -29,6 +29,7 @@ var
29 oldSourceBuffer, 29 oldSourceBuffer,
30 oldFlashSupported, 30 oldFlashSupported,
31 oldNativeHlsSupport, 31 oldNativeHlsSupport,
32 oldDecrypt,
32 requests, 33 requests,
33 xhr, 34 xhr,
34 35
...@@ -135,6 +136,11 @@ module('HLS', { ...@@ -135,6 +136,11 @@ module('HLS', {
135 136
136 oldNativeHlsSupport = videojs.Hls.supportsNativeHls; 137 oldNativeHlsSupport = videojs.Hls.supportsNativeHls;
137 138
139 oldDecrypt = videojs.Hls.decrypt;
140 videojs.Hls.decrypt = function() {
141 return new Uint8Array([0]);
142 };
143
138 // fake XHRs 144 // fake XHRs
139 xhr = sinon.useFakeXMLHttpRequest(); 145 xhr = sinon.useFakeXMLHttpRequest();
140 requests = []; 146 requests = [];
...@@ -152,6 +158,7 @@ module('HLS', { ...@@ -152,6 +158,7 @@ module('HLS', {
152 videojs.MediaSource.open = oldMediaSourceOpen; 158 videojs.MediaSource.open = oldMediaSourceOpen;
153 videojs.Hls.SegmentParser = oldSegmentParser; 159 videojs.Hls.SegmentParser = oldSegmentParser;
154 videojs.Hls.supportsNativeHls = oldNativeHlsSupport; 160 videojs.Hls.supportsNativeHls = oldNativeHlsSupport;
161 videojs.Hls.decrypt = oldDecrypt;
155 videojs.SourceBuffer = oldSourceBuffer; 162 videojs.SourceBuffer = oldSourceBuffer;
156 window.setTimeout = oldSetTimeout; 163 window.setTimeout = oldSetTimeout;
157 xhr.restore(); 164 xhr.restore();
...@@ -1334,11 +1341,15 @@ test('a new keys XHR is created when a previous key XHR finishes', function() { ...@@ -1334,11 +1341,15 @@ test('a new keys XHR is created when a previous key XHR finishes', function() {
1334 }] 1341 }]
1335 }; 1342 };
1336 }; 1343 };
1344 // we're inject the media playlist, so drop the request
1345 requests.shift();
1337 1346
1338 player.hls.playlists.trigger('loadedplaylist'); 1347 player.hls.playlists.trigger('loadedplaylist');
1339 requests.pop().respond(200, new Uint8Array([1]).buffer); 1348 // key response
1340 strictEqual(requests.length, 2, 'a key XHR is created'); 1349 requests[0].response = new Uint32Array([0, 0, 0, 0]).buffer;
1341 strictEqual(requests[1].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri'); 1350 requests.shift().respond(200, null, '');
1351 strictEqual(requests.length, 1, 'a key XHR is created');
1352 strictEqual(requests[0].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri');
1342 1353
1343 player.hls.playlists.media = oldMedia; 1354 player.hls.playlists.media = oldMedia;
1344 }); 1355 });
...@@ -1527,7 +1538,9 @@ test('skip segments if key requests fail more than once', function() { ...@@ -1527,7 +1538,9 @@ test('skip segments if key requests fail more than once', function() {
1527 requests.pop().respond(404); 1538 requests.pop().respond(404);
1528 1539
1529 // key for second segment 1540 // key for second segment
1530 standardXHRResponse(requests.pop()); 1541 requests[0].response = new Uint32Array([0,0,0,0]).buffer;
1542 requests[0].respond(200, null, '');
1543 requests.shift();
1531 1544
1532 equal(bytes.length, 1, 'bytes from the ts segments should not be added'); 1545 equal(bytes.length, 1, 'bytes from the ts segments should not be added');
1533 1546
...@@ -1543,4 +1556,74 @@ test('skip segments if key requests fail more than once', function() { ...@@ -1543,4 +1556,74 @@ test('skip segments if key requests fail more than once', function() {
1543 equal(bytes[1], 1, 'the bytes from the second segment are added and not the first'); 1556 equal(bytes[1], 1, 'the bytes from the second segment are added and not the first');
1544 }); 1557 });
1545 1558
1559 test('the key is supplied to the decrypter in the correct format', function() {
1560 var keys = [];
1561
1562 player.src({
1563 src: 'https://example.com/encrypted-media.m3u8',
1564 type: 'application/vnd.apple.mpegurl'
1565 });
1566 openMediaSource(player);
1567
1568 requests.pop().respond(200, null,
1569 '#EXTM3U\n' +
1570 '#EXT-X-MEDIA-SEQUENCE:5\n' +
1571 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
1572 '#EXTINF:2.833,\n' +
1573 'http://media.example.com/fileSequence52-A.ts\n' +
1574 '#EXTINF:15.0,\n' +
1575 'http://media.example.com/fileSequence52-B.ts\n');
1576
1577
1578 videojs.Hls.decrypt = function(bytes, key) {
1579 keys.push(key);
1580 return new Uint8Array([0]);
1581 };
1582
1583 requests[0].response = new Uint32Array([0,1,2,3]).buffer;
1584 requests[0].respond(200, null, '');
1585 requests.shift();
1586 standardXHRResponse(requests.pop());
1587
1588 equal(keys.length, 1, 'only one call to decrypt was made');
1589 deepEqual(keys[0],
1590 new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
1591 'passed the specified segment key');
1592
1593 });
1594 test('supplies the media sequence of current segment as the IV by default, if no IV is specified', function() {
1595 var ivs = [];
1596
1597 player.src({
1598 src: 'https://example.com/encrypted-media.m3u8',
1599 type: 'application/vnd.apple.mpegurl'
1600 });
1601 openMediaSource(player);
1602
1603 requests.pop().respond(200, null,
1604 '#EXTM3U\n' +
1605 '#EXT-X-MEDIA-SEQUENCE:5\n' +
1606 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
1607 '#EXTINF:2.833,\n' +
1608 'http://media.example.com/fileSequence52-A.ts\n' +
1609 '#EXTINF:15.0,\n' +
1610 'http://media.example.com/fileSequence52-B.ts\n');
1611
1612
1613 videojs.Hls.decrypt = function(bytes, key, iv) {
1614 ivs.push(iv);
1615 return new Uint8Array([0]);
1616 };
1617
1618 requests[0].response = new Uint32Array([0,0,0,0]).buffer;
1619 requests[0].respond(200, null, '');
1620 requests.shift();
1621 standardXHRResponse(requests.pop());
1622
1623 equal(ivs.length, 1, 'only one call to decrypt was made');
1624 deepEqual(ivs[0],
1625 new Uint32Array([0, 0, 0, 5]),
1626 'the IV for the segment is the media sequence');
1627 });
1628
1546 })(window, window.videojs); 1629 })(window, window.videojs);
......