Skip to content

Commit

Permalink
feat(HLS): Add AES-256 and AES-256-CTR support (#6002)
Browse files Browse the repository at this point in the history
Closes #6001
  • Loading branch information
avelad authored Jan 9, 2024
1 parent a94a602 commit c3380ce
Show file tree
Hide file tree
Showing 17 changed files with 189 additions and 13 deletions.
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ module.exports = (config) => {
{pattern: 'test/test/assets/dash-multi-codec/*', included: false},
{pattern: 'test/test/assets/3675/*', included: false},
{pattern: 'test/test/assets/dash-aes-128/*', included: false},
{pattern: 'test/test/assets/hls-aes-256/*', included: false},
{pattern: 'test/test/assets/hls-raw-aac/*', included: false},
{pattern: 'test/test/assets/hls-raw-ac3/*', included: false},
{pattern: 'test/test/assets/hls-raw-ec3/*', included: false},
Expand Down
49 changes: 37 additions & 12 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,7 @@ shaka.hls.HlsParser = class {
if (sessionKeyTags.length > 0) {
for (const drmTag of sessionKeyTags) {
const method = drmTag.getRequiredAttrValue('METHOD');
if (method != 'NONE' && method != 'AES-128') {
if (method != 'NONE' && !this.isAesMethod_(method)) {
// According to the HLS spec, KEYFORMAT is optional and implicitly
// defaults to "identity".
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
Expand Down Expand Up @@ -2737,7 +2737,7 @@ shaka.hls.HlsParser = class {
if (method != 'NONE') {
encrypted = true;

if (method == 'AES-128') {
if (this.isAesMethod_(method)) {
// These keys are handled separately.
aesEncrypted = true;
} else {
Expand Down Expand Up @@ -2775,11 +2775,11 @@ shaka.hls.HlsParser = class {
* @return {!shaka.extern.aesKey}
* @private
*/
parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
parseAESDrmTag_(drmTag, playlist, getUris, variables) {
// Check if the Web Crypto API is available.
if (!window.crypto || !window.crypto.subtle) {
shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
'AES-128. (Web Crypto only exists in secure origins like https)');
'AES. (Web Crypto only exists in secure origins like https)');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
Expand Down Expand Up @@ -2810,13 +2810,25 @@ shaka.hls.HlsParser = class {
}
}

// Default AES-128
const keyInfo = {
bitsKey: 128,
blockCipherMode: 'CBC',
iv,
firstMediaSequenceNumber,
};

const method = drmTag.getRequiredAttrValue('METHOD');
switch (method) {
case 'AES-256':
keyInfo.bitsKey = 256;
break;
case 'AES-256-CTR':
keyInfo.bitsKey = 256;
keyInfo.blockCipherMode = 'CTR';
break;
}

// Don't download the key object until the segment is parsed, to avoid a
// startup delay for long manifests with lots of keys.
keyInfo.fetchKey = async () => {
Expand All @@ -2829,15 +2841,17 @@ shaka.hls.HlsParser = class {
const keyResponse = await this.makeNetworkRequest_(request, requestType);

// keyResponse.status is undefined when URI is "data:text/plain;base64,"
if (!keyResponse.data || keyResponse.data.byteLength != 16) {
if (!keyResponse.data ||
keyResponse.data.byteLength != (keyInfo.bitsKey / 8)) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.AES_128_INVALID_KEY_LENGTH);
}

const algorithm = {
name: 'AES-CBC',
name: keyInfo.blockCipherMode == 'CTR' ? 'AES-CTR' : 'AES-CBC',
length: keyInfo.bitsKey,
};
keyInfo.cryptoKey = await window.crypto.subtle.importKey(
'raw', keyResponse.data, algorithm, true, ['decrypt']);
Expand Down Expand Up @@ -3029,10 +3043,10 @@ shaka.hls.HlsParser = class {
let byteRangeTag = null;
for (const tag of tags) {
if (tag.name == 'EXT-X-KEY') {
if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
if (this.isAesMethod_(tag.getRequiredAttrValue('METHOD')) &&
tag.id < mapTag.id) {
aesKey =
this.parseAES128DrmTag_(tag, playlist, getUris, variables);
this.parseAESDrmTag_(tag, playlist, getUris, variables);
}
} else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
byteRangeTag = tag;
Expand Down Expand Up @@ -3072,7 +3086,7 @@ shaka.hls.HlsParser = class {
endByte = startByte + byteLength - 1;

if (aesKey) {
// MAP segment encrypted with method 'AES-128', when served with
// MAP segment encrypted with method AES, when served with
// HTTP Range, has the unencrypted size specified in the range.
// See: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
const length = (endByte + 1) - startByte;
Expand Down Expand Up @@ -3478,12 +3492,12 @@ shaka.hls.HlsParser = class {
discontinuitySequence++;
}

// Apply new AES-128 tags as you see them, keeping a running total.
// Apply new AES tags as you see them, keeping a running total.
for (const drmTag of item.tags) {
if (drmTag.name == 'EXT-X-KEY') {
if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
if (this.isAesMethod_(drmTag.getRequiredAttrValue('METHOD'))) {
aesKey =
this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
this.parseAESDrmTag_(drmTag, playlist, getUris, variables);
} else {
aesKey = undefined;
}
Expand Down Expand Up @@ -3988,6 +4002,17 @@ shaka.hls.HlsParser = class {
return op.promise;
}

/**
* @param {string} method
* @return {boolean}
* @private
*/
isAesMethod_(method) {
return method == 'AES-128' ||
method == 'AES-256' ||
method == 'AES-256-CTR';
}

/**
* @param {!shaka.hls.Tag} drmTag
* @param {string} mimeType
Expand Down
4 changes: 3 additions & 1 deletion lib/media/streaming_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -1567,7 +1567,9 @@ shaka.media.StreamingEngine = class {
algorithm = {
name: 'AES-CTR',
counter: iv,
length: aesKey.bitsKey,
// NIST SP800-38A standard suggests that the counter should occupy half
// of the counter block
length: 64,
};
}
return window.crypto.subtle.decrypt(algorithm, key.cryptoKey, rawResult);
Expand Down
105 changes: 105 additions & 0 deletions test/hls/hls_parser_integration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* For unknown reasons, these tests fail in the test labs for Edge on Windows,
* in ways that do not seem to be unrelated to HLS parser.
* Practical testing has not found any sign that playback is actually broken in
* Edge, so these tests are disabled on Edge for the time being.
* TODO(#5834): Remove this filter once the tests are fixed.
* @return {boolean}
*/
function checkNoBrokenEdgeHls() {
const chromeVersion = shaka.util.Platform.chromeVersion();
if (shaka.util.Platform.isWindows() && shaka.util.Platform.isEdge() &&
chromeVersion && chromeVersion <= 122) {
// When the tests fail, it's due to the manifest parser failing to find a
// factory. Attempt to find a factory first, to avoid filtering the tests
// when running in a non-broken Edge environment.
const uri = 'fakeuri.m3u8';
const mimeType = 'application/x-mpegurl';
/* eslint-disable no-restricted-syntax */
try {
shaka.media.ManifestParser.getFactory(uri, mimeType);
return true;
} catch (error) {
return false;
}
/* eslint-enable no-restricted-syntax */
}
return true;
}

filterDescribe('HlsParser', checkNoBrokenEdgeHls, () => {
const Util = shaka.test.Util;

/** @type {!jasmine.Spy} */
let onErrorSpy;

/** @type {!HTMLVideoElement} */
let video;
/** @type {shaka.Player} */
let player;
/** @type {!shaka.util.EventManager} */
let eventManager;

let compiledShaka;

/** @type {!shaka.test.Waiter} */
let waiter;

beforeAll(async () => {
video = shaka.test.UiUtils.createVideoElement();
document.body.appendChild(video);
compiledShaka =
await shaka.test.Loader.loadShaka(getClientArg('uncompiled'));
});

beforeEach(async () => {
await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
player = new compiledShaka.Player();
await player.attach(video);

player.configure('streaming.useNativeHlsOnSafari', false);

// Disable stall detection, which can interfere with playback tests.
player.configure('streaming.stallEnabled', false);

// Grab event manager from the uncompiled library:
eventManager = new shaka.util.EventManager();
waiter = new shaka.test.Waiter(eventManager);
waiter.setPlayer(player);

onErrorSpy = jasmine.createSpy('onError');
onErrorSpy.and.callFake((event) => fail(event.detail));
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
});

afterEach(async () => {
eventManager.release();
await player.destroy();
});

afterAll(() => {
document.body.removeChild(video);
});

it('supports AES-256 streaming', async () => {
await player.load('/base/test/test/assets/hls-aes-256/index.m3u8');
await video.play();
expect(player.isLive()).toBe(false);

// Wait for the video to start playback. If it takes longer than 10
// seconds, fail the test.
await waiter.waitForMovementOrFailOnTimeout(video, 10);

// Play for 10 seconds, but stop early if the video ends. If it takes
// longer than 30 seconds, fail the test.
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 10, 30);

await player.unload();
});
});
6 changes: 6 additions & 0 deletions test/test/assets/hls-aes-256/index.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS

#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=829339,BANDWIDTH=901770,CODECS="avc1.4D400D",RESOLUTION=320x180
media.m3u8
1 change: 1 addition & 0 deletions test/test/assets/hls-aes-256/init.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
��l�t� �^z`vpE_�Kʇ2`b�A�p�T�z
Binary file added test/test/assets/hls-aes-256/init.mp4
Binary file not shown.
31 changes: 31 additions & 0 deletions test/test/assets/hls-aes-256/media.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:7
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-KEY:METHOD=NONE
#EXT-X-KEY:METHOD=AES-256,URI="init.key",IV=0xF9EBE119F65FCF2E5F3FDEEB4A3EB3D5
#EXT-X-MAP:URI="init.mp4"

#EXT-X-KEY:METHOD=AES-256,URI="seg0.key",IV=0x3F6DE518DE9A0D8B0FC5EA114ACEDEFA
#EXTINF:4.0
segment_0.m4s

#EXT-X-KEY:METHOD=AES-256,URI="seg1.key",IV=0x9DF0B659F52F23C3676C08FE0049500F
#EXTINF:4.0
segment_1.m4s

#EXT-X-KEY:METHOD=AES-256-CTR,URI="seg2.key",IV=0xDB67562D1B5F959D1DBA9DD50BF87A52
#EXTINF:4.0
segment_2.m4s

#EXT-X-KEY:METHOD=AES-256-CTR,URI="seg3_and_4.key",IV=0x5161DD0995837650DE5E495DEF6BFBE5
#EXTINF:4.0
segment_3.m4s

#EXT-X-KEY:METHOD=AES-256-CTR,URI="seg3_and_4.key",IV=0x035022245B6DBA858C56F2F0079BAC96
#EXTINF:4.0
segment_4.m4s

#EXT-X-ENDLIST
1 change: 1 addition & 0 deletions test/test/assets/hls-aes-256/seg0.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
�d������-�EE�I)$���vf@$H���2�I
1 change: 1 addition & 0 deletions test/test/assets/hls-aes-256/seg1.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
���m��]$��_��\݈��fz��C�CX!k��
2 changes: 2 additions & 0 deletions test/test/assets/hls-aes-256/seg2.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
��pf�/`~
G���41�ZKm"A=qyk��H
1 change: 1 addition & 0 deletions test/test/assets/hls-aes-256/seg3_and_4.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
��������C�`��IYS�<jQ��c�J����
Binary file added test/test/assets/hls-aes-256/segment_0.m4s
Binary file not shown.
Binary file added test/test/assets/hls-aes-256/segment_1.m4s
Binary file not shown.
Binary file added test/test/assets/hls-aes-256/segment_2.m4s
Binary file not shown.
Binary file added test/test/assets/hls-aes-256/segment_3.m4s
Binary file not shown.
Binary file added test/test/assets/hls-aes-256/segment_4.m4s
Binary file not shown.

0 comments on commit c3380ce

Please sign in to comment.