Skip to content

Commit

Permalink
Reject AES-128 HLS content with meaningful error
Browse files Browse the repository at this point in the history
Currently we check encrypt key tags after we parse the segment, so
playing an AES-128 encrypted content results in error message
'MANIFEST.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME'. We should check the
encrypt key before, and give a more clear error message.
Filtering out the contents encrypted with AES-128, and if there's no
valid content left, we'll show 'CONTENT_UNSUPPORTED_BY_BROWSER'.

Closes #1838

Change-Id: I893f57a939e45f2787144dfe311b779aed26ac34
  • Loading branch information
michellezhuogg committed Apr 22, 2019
1 parent ee0cee9 commit cb9decb
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 41 deletions.
119 changes: 78 additions & 41 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ shaka.hls.HlsParser = function() {
* @private {Map.<string, Map.<string, string>>}
*/
this.groupIdToClosedCaptionsMap_ = new Map();

/** True if some of the variants in the playlist is encrypted with AES-128.
* @private {boolean} */
this.aesEncrypted_ = false;
};


Expand Down Expand Up @@ -391,6 +395,17 @@ shaka.hls.HlsParser.prototype.parseManifest_ = async function(data) {
shaka.util.Error.Code.OPERATION_ABORTED);
}

if (this.aesEncrypted_ && period.variants.length == 0) {
// We do not support AES-128 encryption with HLS yet. Variants is null
// when the playlist is encrypted with AES-128.
shaka.log.info('No stream is created, because we don\'t support AES-128',
'encryption yet');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_AES_128_ENCRYPTION_NOT_SUPPORTED);
}

// HLS has no notion of periods. We're treating the whole presentation as
// one period.
this.playerInterface_.filterAllPeriods([period]);
Expand Down Expand Up @@ -546,6 +561,9 @@ shaka.hls.HlsParser.prototype.createPeriod_ = function(playlist) {

return Promise.all(variantsPromises).then(function(allVariants) {
let variants = allVariants.reduce(Functional.collapseArrays, []);
// Filter out null variants.
variants = variants.filter((variant) => variant != null);

return {
startTime: 0,
variants: variants,
Expand Down Expand Up @@ -659,6 +677,8 @@ shaka.hls.HlsParser.prototype.createVariantsForTag_ = function(tag, playlist) {
let videoStreamInfos = [];

return Promise.all(promises).then(function(data) {
// Filter out null streamInfo.
data = data.filter((streamInfo) => streamInfo != null);
if (audioGroupId) {
audioStreamInfos = data;
} else if (videoGroupId) {
Expand Down Expand Up @@ -733,8 +753,12 @@ shaka.hls.HlsParser.prototype.createVariantsForTag_ = function(tag, playlist) {
} else {
videoStreamInfos = [streamInfo];
}
} else if (streamInfo === null) {
shaka.log.debug('stream info is null');
return null;
}
goog.asserts.assert(videoStreamInfos || audioStreamInfos,

goog.asserts.assert(videoStreamInfos.length || audioStreamInfos.length,
'We should have created a stream!');

if (videoStreamInfos) {
Expand Down Expand Up @@ -987,6 +1011,7 @@ shaka.hls.HlsParser.prototype.createStreamInfoFromMediaTag_ =
return this.createStreamInfo_(
verbatimMediaPlaylistUri, allCodecs, type, language, primary, name,
channelsCount, /* closedCaptions */ null).then(function(streamInfo) {
if (streamInfo == null) return null;
// TODO: This check is necessary because of the possibility of multiple
// calls to createStreamInfoFromMediaTag_ before either has resolved.
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
Expand Down Expand Up @@ -1059,6 +1084,7 @@ shaka.hls.HlsParser.prototype.createStreamInfoFromVariantTag_ =
return this.createStreamInfo_(verbatimMediaPlaylistUri, allCodecs, type,
/* language */ 'und', /* primary */ false, /* name */ null,
/* channelcount */ null, closedCaptions).then(function(streamInfo) {
if (streamInfo == null) return null;
// TODO: This check is necessary because of the possibility of multiple
// calls to createStreamInfoFromVariantTag_ before either has resolved.
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
Expand All @@ -1084,12 +1110,11 @@ shaka.hls.HlsParser.prototype.createStreamInfoFromVariantTag_ =
* @throws shaka.util.Error
* @private
*/
shaka.hls.HlsParser.prototype.createStreamInfo_ = function(
shaka.hls.HlsParser.prototype.createStreamInfo_ = async function(
verbatimMediaPlaylistUri, allCodecs, type, language, primary, name,
channelsCount, closedCaptions) {
// TODO: Refactor, too many parameters
const Utils = shaka.hls.Utils;
const HlsParser = shaka.hls.HlsParser;

let absoluteMediaPlaylistUri = Utils.constructAbsoluteUri(
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
Expand All @@ -1101,7 +1126,7 @@ shaka.hls.HlsParser.prototype.createStreamInfo_ = function(
/** @type {string} */
let mimeType;

return this.requestManifest_(absoluteMediaPlaylistUri).then((response) => {
const response = await this.requestManifest_(absoluteMediaPlaylistUri);
// Record the final URI after redirects.
absoluteMediaPlaylistUri = response.uri;

Expand All @@ -1117,56 +1142,37 @@ shaka.hls.HlsParser.prototype.createStreamInfo_ = function(
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
}

goog.asserts.assert(playlist.segments != null,
'Media playlist should have segments!');

this.determinePresentationType_(playlist);

codecs = this.guessCodecs_(type, allCodecs);
return this.guessMimeType_(type, codecs, playlist);
}).then((mimeTypeArg) => {
mimeType = mimeTypeArg;

let mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
'EXT-X-MEDIA-SEQUENCE');

let startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;

return this.createSegments_(
verbatimMediaPlaylistUri, playlist, startPosition, mimeType, codecs);
}).then((segments) => {
let minTimestamp = segments[0].startTime;
let lastEndTime = segments[segments.length - 1].endTime;
let duration = lastEndTime - minTimestamp;
let segmentIndex = new shaka.media.SegmentIndex(segments);

const initSegmentReference = this.createInitSegmentReference_(playlist);

let kind = undefined;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
if (type == ManifestParserUtils.ContentType.TEXT) {
kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
}

/** @type {!Array.<!shaka.hls.Tag>} */
let drmTags = [];
playlist.segments.forEach(function(segment) {
let segmentKeyTags = Utils.filterTagsByName(segment.tags,
const segmentKeyTags = Utils.filterTagsByName(segment.tags,
'EXT-X-KEY');
drmTags.push.apply(drmTags, segmentKeyTags);
});

let encrypted = false;
/** @type {!Array.<shaka.extern.DrmInfo>}*/
let drmInfos = [];
let keyId = null;

// TODO: May still need changes to support key rotation.
drmTags.forEach(function(drmTag) {
let method = HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
for (const drmTag of drmTags) {
let method =
shaka.hls.HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
if (method != 'NONE') {
encrypted = true;

// We do not support AES-128 encryption with HLS yet. So, do not create
// StreamInfo for the playlist encrypted with AES-128.
// TODO: Remove the error message once we add support for AES-128.
if (method == 'AES-128') {
shaka.log.warning('Unsupported HLS Encryption', method);
this.aesEncrypted_ = true;
return null;
}

let keyFormat =
HlsParser.getRequiredAttributeValue_(drmTag, 'KEYFORMAT');
shaka.hls.HlsParser.getRequiredAttributeValue_(drmTag, 'KEYFORMAT');
let drmParser =
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];

Expand All @@ -1180,7 +1186,7 @@ shaka.hls.HlsParser.prototype.createStreamInfo_ = function(
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
}
}
});
}

if (encrypted && !drmInfos.length) {
throw new shaka.util.Error(
Expand All @@ -1189,6 +1195,38 @@ shaka.hls.HlsParser.prototype.createStreamInfo_ = function(
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
}


goog.asserts.assert(playlist.segments != null,
'Media playlist should have segments!');

this.determinePresentationType_(playlist);

codecs = this.guessCodecs_(type, allCodecs);
const mimeTypeArg = await this.guessMimeType_(type, codecs, playlist);

mimeType = mimeTypeArg;

let mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
'EXT-X-MEDIA-SEQUENCE');

let startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;

const segments = await this.createSegments_(
verbatimMediaPlaylistUri, playlist, startPosition, mimeType, codecs);

let minTimestamp = segments[0].startTime;
let lastEndTime = segments[segments.length - 1].endTime;
let duration = lastEndTime - minTimestamp;
let segmentIndex = new shaka.media.SegmentIndex(segments);

const initSegmentReference = this.createInitSegmentReference_(playlist);

let kind = undefined;
if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
}


/** @type {shaka.extern.Stream} */
let stream = {
id: this.globalId_++,
Expand Down Expand Up @@ -1229,7 +1267,6 @@ shaka.hls.HlsParser.prototype.createStreamInfo_ = function(
maxTimestamp: lastEndTime,
duration: duration,
};
});
};


Expand Down
5 changes: 5 additions & 0 deletions lib/util/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,11 @@ shaka.util.Error.Code = {
*/
'CANNOT_ADD_EXTERNAL_TEXT_TO_LIVE_STREAM': 4033,

/**
* We do not support AES-128 encryption with HLS yet.
*/
'HLS_AES_128_ENCRYPTION_NOT_SUPPORTED': 4034,

// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
// RETIRED: 'INVALID_SEGMENT_INDEX': 5001,
// RETIRED: 'SEGMENT_DOES_NOT_EXIST': 5002,
Expand Down
91 changes: 91 additions & 0 deletions test/hls/hls_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,69 @@ describe('HlsParser', function() {
await testHlsParser(master, media, manifest);
});

it('drops variants encrypted with AES-128', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
'#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
'video2\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'URI="audio"\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
'URI="audio2"\n',
].join('');

const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');

const mediaWithAesEncryption = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-KEY:METHOD=AES-128,',
'URI="800k.key\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');

let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.bandwidth(200)
.addPartialStream(ContentType.VIDEO)
.size(960, 540)
.addPartialStream(ContentType.AUDIO)
.language('en')
.build();

fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/audio2', media)
.setResponseText('test:/video', media)
.setResponseText('test:/video2', mediaWithAesEncryption)
.setResponseText('test:/main.vtt', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/main.test', segmentData)
.setResponseValue('test:/selfInit.mp4', selfInitializingSegmentData);

let actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
return actual;
});

it('constructs DrmInfo for Widevine', async () => {
const master = [
'#EXTM3U\n',
Expand Down Expand Up @@ -1342,6 +1405,34 @@ describe('HlsParser', function() {
verifyError(master, media, error, done);
});

it('if all variants are encrypted with AES-128', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
].join('');

const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:6\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-KEY:METHOD=AES-128,',
'URI="data:text/plain;base64\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');

const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
Code.HLS_AES_128_ENCRYPTION_NOT_SUPPORTED);

verifyError(master, media, error, done);
});

describe('if required attributes are missing', function() {
/**
* @param {string} master
Expand Down

0 comments on commit cb9decb

Please sign in to comment.