fa251c38 by David LaPalomento

AES-128 decryption

Implement the block level AES decipher and include the routine to do PKCS#7 unpadding.
1 parent 1d78b907
......@@ -11,3 +11,39 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
The AES decryption implementation in this project is derived from the
Stanford Javascript Cryptography Library
(http://bitwiseshiftleft.github.io/sjcl/). That work is covered by the
following copyright and permission notice:
Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation
are those of the authors and should not be interpreted as representing
official policies, either expressed or implied, of the authors.
\ No newline at end of file
......
......@@ -3,15 +3,18 @@ The [HLS spec](http://tools.ietf.org/html/draft-pantos-http-live-streaming-13#se
```sh
# encrypt the text "hello" into a file
echo -n "hello" | pkcs7 | openssl enc -aes-128-cbc > hello.encrypted
# 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
# encrypt some text and get the bytes in a format that can be easily used for
# testing in javascript
echo -n "hello" | pkcs7 | openssl enc -aes-128-cbc | xxd -i
# xxd is a handy way of translating binary into a format easily consumed by
# javascript
xxd -i hello.encrypted
```
Later, you can decrypt it:
```sh
cat hello.encrypted | openssl enc -d -aes-128-cbc
openssl enc -d -nopad -aes-128-cbc -k $KEY -iv $IV
```
......
......@@ -37,6 +37,7 @@
"video.js": "^4.7.2"
},
"dependencies": {
"pkcs7": "^0.2.2",
"videojs-contrib-media-sources": "^0.3.0"
}
}
......
/*
* videojs-hls
*
* Copyright (c) 2014 Brightcove
* All rights reserved.
*
* This file contains an adaptation of the AES decryption algorithm
* from the Standford Javascript Cryptography Library. That work is
* covered by the following copyright and permissions notice:
*
* Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation
* are those of the authors and should not be interpreted as representing
* official policies, either expressed or implied, of the authors.
*/
(function(window, videojs, unpad) {
'use strict';
var AES, decrypt;
/**
* Schedule out an AES key for both encryption and decryption. This
* is a low-level class. Use a cipher mode to do bulk encryption.
*
* @constructor
* @param key {Array} The key as an array of 4, 6 or 8 words.
*/
AES = function (key) {
if (!this._tables[0][0][0]) {
this._precompute();
}
var i, j, tmp,
encKey, decKey,
sbox = this._tables[0][4], decTable = this._tables[1],
keyLen = key.length, rcon = 1;
if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
throw new Error("Invalid aes key size");
}
encKey = key.slice(0);
decKey = [];
this._key = [encKey, decKey];
// schedule encryption keys
for (i = keyLen; i < 4 * keyLen + 28; i++) {
tmp = encKey[i-1];
// apply sbox
if (i%keyLen === 0 || (keyLen === 8 && i%keyLen === 4)) {
tmp = sbox[tmp>>>24]<<24 ^ sbox[tmp>>16&255]<<16 ^ sbox[tmp>>8&255]<<8 ^ sbox[tmp&255];
// shift rows and add rcon
if (i%keyLen === 0) {
tmp = tmp<<8 ^ tmp>>>24 ^ rcon<<24;
rcon = rcon<<1 ^ (rcon>>7)*283;
}
}
encKey[i] = encKey[i-keyLen] ^ tmp;
}
// schedule decryption keys
for (j = 0; i; j++, i--) {
tmp = encKey[j&3 ? i : i - 4];
if (i<=4 || j<4) {
decKey[j] = tmp;
} else {
decKey[j] = decTable[0][sbox[tmp>>>24 ]] ^
decTable[1][sbox[tmp>>16 & 255]] ^
decTable[2][sbox[tmp>>8 & 255]] ^
decTable[3][sbox[tmp & 255]];
}
}
};
AES.prototype = {
/**
* The expanded S-box and inverse S-box tables. These will be computed
* on the client so that we don't have to send them down the wire.
*
* There are two tables, _tables[0] is for encryption and
* _tables[1] is for decryption.
*
* The first 4 sub-tables are the expanded S-box with MixColumns. The
* last (_tables[01][4]) is the S-box itself.
*
* @private
*/
_tables: [[[],[],[],[],[]],[[],[],[],[],[]]],
/**
* Expand the S-box tables.
*
* @private
*/
_precompute: function () {
var encTable = this._tables[0], decTable = this._tables[1],
sbox = encTable[4], sboxInv = decTable[4],
i, x, xInv, d=[], th=[], x2, x4, x8, s, tEnc, tDec;
// Compute double and third tables
for (i = 0; i < 256; i++) {
th[( d[i] = i<<1 ^ (i>>7)*283 )^i]=i;
}
for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) {
// Compute sbox
s = xInv ^ xInv<<1 ^ xInv<<2 ^ xInv<<3 ^ xInv<<4;
s = s>>8 ^ s&255 ^ 99;
sbox[x] = s;
sboxInv[s] = x;
// Compute MixColumns
x8 = d[x4 = d[x2 = d[x]]];
tDec = x8*0x1010101 ^ x4*0x10001 ^ x2*0x101 ^ x*0x1010100;
tEnc = d[s]*0x101 ^ s*0x1010100;
for (i = 0; i < 4; i++) {
encTable[i][x] = tEnc = tEnc<<24 ^ tEnc>>>8;
decTable[i][s] = tDec = tDec<<24 ^ tDec>>>8;
}
}
// Compactify. Considerable speedup on Firefox.
for (i = 0; i < 5; i++) {
encTable[i] = encTable[i].slice(0);
decTable[i] = decTable[i].slice(0);
}
},
/**
* Decrypt an array of 4 big-endian words.
* @param {Array} data The ciphertext.
* @return {Array} The plaintext.
*/
decrypt:function (input) {
if (input.length !== 4) {
throw new Error("Invalid aes block size");
}
var key = this._key[1],
// state variables a,b,c,d are loaded with pre-whitened data
a = input[0] ^ key[0],
b = input[3] ^ key[1],
c = input[2] ^ key[2],
d = input[1] ^ key[3],
a2, b2, c2,
nInnerRounds = key.length/4 - 2,
i,
kIndex = 4,
out = [0,0,0,0],
table = this._tables[1],
// load up the tables
t0 = table[0],
t1 = table[1],
t2 = table[2],
t3 = table[3],
sbox = table[4];
// Inner rounds. Cribbed from OpenSSL.
for (i = 0; i < nInnerRounds; i++) {
a2 = t0[a>>>24] ^ t1[b>>16 & 255] ^ t2[c>>8 & 255] ^ t3[d & 255] ^ key[kIndex];
b2 = t0[b>>>24] ^ t1[c>>16 & 255] ^ t2[d>>8 & 255] ^ t3[a & 255] ^ key[kIndex + 1];
c2 = t0[c>>>24] ^ t1[d>>16 & 255] ^ t2[a>>8 & 255] ^ t3[b & 255] ^ key[kIndex + 2];
d = t0[d>>>24] ^ t1[a>>16 & 255] ^ t2[b>>8 & 255] ^ t3[c & 255] ^ key[kIndex + 3];
kIndex += 4;
a=a2; b=b2; c=c2;
}
// Last round.
for (i = 0; i < 4; i++) {
out[3 & -i] =
sbox[a>>>24 ]<<24 ^
sbox[b>>16 & 255]<<16 ^
sbox[c>>8 & 255]<<8 ^
sbox[d & 255] ^
key[kIndex++];
a2=a; a=b; b=c; c=d; d=a2;
}
return out;
}
};
decrypt = function(encrypted, key) {
var
view = new DataView(encrypted.buffer),
decrypted = new AES(key)
// convert big-endian input to platform byte order for decryption
.decrypt(new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
])),
bytes = new Uint8Array(encrypted.byteLength);
// convert platform byte order back to big-endian for unpadding
view = new DataView(bytes.buffer);
view.setUint32(0, decrypted[0]);
view.setUint32(4, decrypted[1]);
view.setUint32(8, decrypted[2]);
view.setUint32(12, decrypted[3]);
return unpad(bytes);
};
// exports
videojs.hls = videojs.util.mergeOptions(videojs.hls, {
decrypt: decrypt
});
})(window, window.videojs, window.pkcs7.unpad);
(function(window, videojs, undefined) {
'use strict';
/*
======== A Handy Little QUnit Reference ========
http://api.qunitjs.com/
Test methods:
module(name, {[setup][ ,teardown]})
test(name, callback)
expect(numberOfAssertions)
stop(increment)
start(decrement)
Test assertions:
ok(value, [message])
equal(actual, expected, [message])
notEqual(actual, expected, [message])
deepEqual(actual, expected, [message])
notDeepEqual(actual, expected, [message])
strictEqual(actual, expected, [message])
notStrictEqual(actual, expected, [message])
throws(block, [expected], [message])
*/
// see docs/hlse.md for instructions on how test data was generated
module('Decryption');
test('decrypts using AES-128 CBC with PKCS7', function() {
// the string "howdy folks" with key and initialization
// vector
var
key = [0, 0, 0, 0],
initVector = key,
encrypted = new Uint8Array([
0xce, 0x90, 0x97, 0xd0,
0x08, 0x46, 0x4d, 0x18,
0x4f, 0xae, 0x01, 0x1c,
0x82, 0xa8, 0xf0, 0x67]),
length = 'howdy folks'.length,
plaintext = new Uint8Array(length),
i;
i = length;
while (i--) {
plaintext[i] = 'howdy folks'.charCodeAt(i);
}
// decrypt works on the sjcl example site
// correct output: [1752135524, 2032166511, 1818981125, 84215045]
deepEqual(plaintext,
new Uint8Array(videojs.hls.decrypt(encrypted, key, initVector)),
'decrypted with a numeric key');
deepEqual(plaintext,
new Uint8Array(videojs.hls.decrypt(encrypted, key, initVector)),
'decrypted with a byte array key');
});
})(window, window.videojs);
......@@ -77,6 +77,7 @@ module.exports = function(config) {
'../node_modules/sinon/lib/sinon/util/fake_timers.js',
'../node_modules/video.js/dist/video-js/video.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',
'../src/videojs-hls.js',
'../src/xhr.js',
......@@ -88,6 +89,7 @@ module.exports = function(config) {
'../src/stream.js',
'../src/m3u8/m3u8-parser.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
'../tmp/expected.js',
'tsSegment-bc.js',
......
......@@ -41,6 +41,7 @@ module.exports = function(config) {
'../node_modules/sinon/lib/sinon/util/fake_timers.js',
'../node_modules/video.js/dist/video-js/video.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',
'../src/videojs-hls.js',
'../src/xhr.js',
......@@ -52,6 +53,7 @@ module.exports = function(config) {
'../src/stream.js',
'../src/m3u8/m3u8-parser.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
'../tmp/expected.js',
'tsSegment-bc.js',
......
......@@ -31,6 +31,8 @@
<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>
<!-- M3U8 TEST DATA -->
<script src="../tmp/manifests.js"></script>
<script src="../tmp/expected.js"></script>
......@@ -55,6 +57,7 @@
<script src="flv-tag_test.js"></script>
<script src="m3u8_test.js"></script>
<script src="playlist-loader_test.js"></script>
<script src="decrypter_test.js"></script>
</head>
<body>
<div id="qunit"></div>
......