Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(HLS): Add AES-256 and AES-256-CTR support #6002

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1298,7 +1298,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 @@ -2735,7 +2735,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 @@ -2773,11 +2773,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 @@ -2808,13 +2808,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 @@ -2827,15 +2839,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 @@ -3027,10 +3041,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 @@ -3070,7 +3084,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 @@ -3476,12 +3490,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 @@ -3984,6 +3998,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.
Loading