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
# since this is for testing, skip the key salting so the output is stable
# using -nosalt outside of testing is a terrible idea!
echo -n "hello" | pkcs7 | \
openssl enc -aes-128-cbc -nopad -nosalt -k $KEY -iv $IV > hello.encrypted
openssl enc -aes-128-cbc -nopad -nosalt -K $KEY -iv $IV > hello.encrypted
# xxd is a handy way of translating binary into a format easily consumed by
# javascript
......@@ -16,5 +16,5 @@ xxd -i hello.encrypted
Later, you can decrypt it:
```sh
openssl enc -d -nopad -aes-128-cbc -k $KEY -iv $IV
openssl enc -d -nopad -aes-128-cbc -K $KEY -iv $IV
```
......
......@@ -27,6 +27,11 @@
<script src="src/stream.js"></script>
<script src="src/m3u8/m3u8-parser.js"></script>
<script src="src/playlist-loader.js"></script>
<script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="src/decrypter.js"></script>
<script src="src/bin-utils.js"></script>
<!-- example MPEG2-TS segments -->
<!-- bipbop -->
......@@ -58,8 +63,11 @@
width="600"
controls>
<source
src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
src="http://solutions.brightcove.com/jwhisenant/hls/encrypted/brightcove/oceans/oceans_aes-remote-key.m3u8"
type="application/x-mpegURL">
<!--<source-->
<!--src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"-->
<!--type="application/x-mpegURL">-->
</video>
<script>
videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
......
......@@ -4,12 +4,12 @@
var
bytes = Array.prototype.slice.call(data),
step = 16,
formatHexString = function(e) {
formatHexString = function(e, i) {
var value = e.toString(16);
return "00".substring(0, 2 - value.length) + value;
return "00".substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
},
formatAsciiString = function(e) {
if (e > 32 && e < 125) {
if (e >= 0x20 && e < 0x7e) {
return String.fromCharCode(e);
}
return '.';
......@@ -18,9 +18,9 @@
hex,
ascii;
for (var j = 0; j < bytes.length / step; j++) {
hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(' ');
hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
result += hex + ' ' + ascii + '\n';
result += hex + ' ' + ascii + '\n';
}
return result;
},
......
......@@ -214,11 +214,12 @@ decrypt = function(encrypted, key, initVector) {
var
encryptedView = new DataView(encrypted.buffer),
platformEndian = new Uint32Array(encrypted.byteLength / 4),
decipher = new AES(key),
decipher = new AES(Array.prototype.slice.call(key)),
decrypted = new Uint8Array(encrypted.byteLength),
decryptedView = new DataView(decrypted.buffer),
decryptedBlock,
word, byte;
word,
byte;
// convert big-endian input to platform byte order for decryption
for (byte = 0; byte < encrypted.byteLength; byte += 4) {
......
......@@ -379,8 +379,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, function(error, url) {
var tags;
// the segment request is no longer outstanding
tech.segmentXhr_ = null;
......@@ -420,7 +418,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
mediaIndex: tech.mediaIndex,
playlist: tech.playlists.media(),
offset: offset,
bytes: this.response
bytes: new Uint8Array(this.response)
});
tech.drainBuffer();
......@@ -464,6 +462,14 @@ videojs.Hls.prototype.drainBuffer = function(event) {
return segmentBuffer.shift();
} else if (!segment.key.bytes) {
return;
} else {
// if the media sequence is greater than 2^32, the IV will be incorrect
// assuming 10s segments, that would be about 1300 years
bytes = videojs.Hls.decrypt(bytes,
segment.key.bytes,
new Uint32Array([
0, 0, 0,
mediaIndex + playlist.mediaSequence]));
}
}
......@@ -483,7 +489,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
// transmux the segment data from MP2T to FLV
this.segmentParser_.parseSegmentBinaryData(new Uint8Array(bytes));
this.segmentParser_.parseSegmentBinaryData(bytes);
this.segmentParser_.flushTags();
tags = [];
......@@ -528,7 +534,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
};
videojs.Hls.prototype.fetchKeys = function(playlist, index) {
var i, key, tech, player, settings;
var i, key, tech, player, settings, view;
// if there is a pending XHR or no segments, don't do anything
if (keyXhr || !playlist.segments) {
......@@ -559,7 +565,13 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) {
return;
}
key.bytes = this.response || new Uint8Array([1]);
view = new DataView(this.response);
key.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
]);
tech.fetchKeys(playlist, i++, url);
});
break;
......
......@@ -36,7 +36,7 @@ module('Decryption');
test('decrypts a single AES-128 with PKCS7 block', function() {
var
key = [0, 0, 0, 0],
key = new Uint32Array([0, 0, 0, 0]),
initVector = key,
// the string "howdy folks" encrypted
encrypted = new Uint8Array([
......@@ -52,7 +52,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() {
test('decrypts multiple AES-128 blocks with CBC', function() {
var
key = [0, 0, 0, 0],
key = new Uint32Array([0, 0, 0, 0]),
initVector = key,
// the string "0123456789abcdef01234" encrypted
encrypted = new Uint8Array([
......
......@@ -43,10 +43,20 @@
<section>
<h2>Inputs</h2>
<form id="inputs">
<label>
Your original MP2T segment:
<input type="file" id="original">
</label>
<fieldset>
<label>
Your original MP2T segment:
<input type="file" id="original">
</label>
<label>
Key (optional):
<input type="text" id="key">
</label>
<label>
IV (optional):
<input type="text" id="iv">
</label>
</fieldset>
<label>
A working, FLV version of the underlying stream
produced by another tool:
......@@ -105,11 +115,15 @@
<script src="../../src/h264-stream.js"></script>
<script src="../../src/aac-stream.js"></script>
<script src="../../src/segment-parser.js"></script>
<script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="../../src/decrypter.js"></script>
<script src="../../src/bin-utils.js"></script>
<script>
var inputs = document.getElementById('inputs'),
original = document.getElementById('original'),
key = document.getElementById('key'),
iv = document.getElementById('iv'),
working = document.getElementById('working'),
vjsTags = document.querySelector('.vjs-tags'),
......@@ -132,6 +146,7 @@
var parser = new videojs.Hls.SegmentParser(),
tags = [parser.getFlvHeader()],
tag,
bytes,
hex,
li,
byteLength = 0,
......@@ -142,7 +157,20 @@
// clear old tag info
vjsTags.innerHTML = '';
parser.parseSegmentBinaryData(new Uint8Array(reader.result));
// optionally, decrypt the segment
if (key.value && iv.value) {
bytes = videojs.Hls.decrypt(new Uint8Array(reader.result),
key.value.match(/([0-9a-f]{8})/gi).map(function(e) {
return parseInt(e, 16);
}),
iv.value.match(/([0-9a-f]{8})/gi).map(function(e) {
return parseInt(e, 16);
}));
} else {
bytes = new Uint8Array(reader.result);
}
parser.parseSegmentBinaryData(bytes);
// collect all the tags
while (parser.tagsAvailable()) {
......
......@@ -29,6 +29,7 @@ var
oldSourceBuffer,
oldFlashSupported,
oldNativeHlsSupport,
oldDecrypt,
requests,
xhr,
......@@ -135,6 +136,11 @@ module('HLS', {
oldNativeHlsSupport = videojs.Hls.supportsNativeHls;
oldDecrypt = videojs.Hls.decrypt;
videojs.Hls.decrypt = function() {
return new Uint8Array([0]);
};
// fake XHRs
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
......@@ -152,6 +158,7 @@ module('HLS', {
videojs.MediaSource.open = oldMediaSourceOpen;
videojs.Hls.SegmentParser = oldSegmentParser;
videojs.Hls.supportsNativeHls = oldNativeHlsSupport;
videojs.Hls.decrypt = oldDecrypt;
videojs.SourceBuffer = oldSourceBuffer;
window.setTimeout = oldSetTimeout;
xhr.restore();
......@@ -1334,11 +1341,15 @@ test('a new keys XHR is created when a previous key XHR finishes', function() {
}]
};
};
// we're inject the media playlist, so drop the request
requests.shift();
player.hls.playlists.trigger('loadedplaylist');
requests.pop().respond(200, new Uint8Array([1]).buffer);
strictEqual(requests.length, 2, 'a key XHR is created');
strictEqual(requests[1].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri');
// key response
requests[0].response = new Uint32Array([0, 0, 0, 0]).buffer;
requests.shift().respond(200, null, '');
strictEqual(requests.length, 1, 'a key XHR is created');
strictEqual(requests[0].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri');
player.hls.playlists.media = oldMedia;
});
......@@ -1527,7 +1538,9 @@ test('skip segments if key requests fail more than once', function() {
requests.pop().respond(404);
// key for second segment
standardXHRResponse(requests.pop());
requests[0].response = new Uint32Array([0,0,0,0]).buffer;
requests[0].respond(200, null, '');
requests.shift();
equal(bytes.length, 1, 'bytes from the ts segments should not be added');
......@@ -1543,4 +1556,74 @@ test('skip segments if key requests fail more than once', function() {
equal(bytes[1], 1, 'the bytes from the second segment are added and not the first');
});
test('the key is supplied to the decrypter in the correct format', function() {
var keys = [];
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:5\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence52-A.ts\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence52-B.ts\n');
videojs.Hls.decrypt = function(bytes, key) {
keys.push(key);
return new Uint8Array([0]);
};
requests[0].response = new Uint32Array([0,1,2,3]).buffer;
requests[0].respond(200, null, '');
requests.shift();
standardXHRResponse(requests.pop());
equal(keys.length, 1, 'only one call to decrypt was made');
deepEqual(keys[0],
new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
'passed the specified segment key');
});
test('supplies the media sequence of current segment as the IV by default, if no IV is specified', function() {
var ivs = [];
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:5\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence52-A.ts\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence52-B.ts\n');
videojs.Hls.decrypt = function(bytes, key, iv) {
ivs.push(iv);
return new Uint8Array([0]);
};
requests[0].response = new Uint32Array([0,0,0,0]).buffer;
requests[0].respond(200, null, '');
requests.shift();
standardXHRResponse(requests.pop());
equal(ivs.length, 1, 'only one call to decrypt was made');
deepEqual(ivs[0],
new Uint32Array([0, 0, 0, 5]),
'the IV for the segment is the media sequence');
});
})(window, window.videojs);
......