From 3846eeac3f3777c35e61f479958015062f4275af Mon Sep 17 00:00:00 2001 From: varvaruc Date: Tue, 8 Nov 2022 21:48:31 +0200 Subject: [PATCH] feat(HLS): Support for HLS key rotation (#4568) Fixes #741 --- lib/hls/hls_parser.js | 122 +++++++++++++++++++++++--------------- test/hls/hls_live_unit.js | 39 ++++++++++++ 2 files changed, 112 insertions(+), 49 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index b147560412..a231b07f1b 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -373,6 +373,19 @@ shaka.hls.HlsParser = class { const mediaSequenceToStartTime = this.getMediaSequenceToStartTimeFor_(streamInfo); + const {keyIds, drmInfos} = this.parseDrmInfo_(playlist, stream.mimeType); + + const keysAreEqual = + (a, b) => a.size === b.size && [...a].every((value) => b.has(value)); + + if (!keysAreEqual(stream.keyIds, keyIds)) { + stream.keyIds = keyIds; + stream.drmInfos = drmInfos; + if (this.manifest_) { + this.playerInterface_.filter(this.manifest_); + } + } + const segments = this.createSegments_( streamInfo.verbatimMediaPlaylistUri, playlist, stream.type, stream.mimeType, mediaSequenceToStartTime, mediaVariables); @@ -1851,55 +1864,8 @@ shaka.hls.HlsParser = class { mediaVariables); } - /** @type {!Array.} */ - const drmTags = []; - if (playlist.segments) { - for (const segment of playlist.segments) { - const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags, - 'EXT-X-KEY'); - drmTags.push(...segmentKeyTags); - } - } - - let encrypted = false; - let aesEncrypted = false; - - /** @type {!Array.}*/ - const drmInfos = []; - const keyIds = new Set(); - - // TODO: May still need changes to support key rotation. - for (const drmTag of drmTags) { - const method = drmTag.getRequiredAttrValue('METHOD'); - if (method != 'NONE') { - encrypted = true; - - if (method == 'AES-128') { - // These keys are handled separately. - aesEncrypted = true; - } else { - // 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 - const keyFormat = - drmTag.getAttributeValue('KEYFORMAT') || 'identity'; - const drmParser = - shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat]; - - const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null; - if (drmInfo) { - if (drmInfo.keyIds) { - for (const keyId of drmInfo.keyIds) { - keyIds.add(keyId); - } - } - drmInfos.push(drmInfo); - } else { - shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat); - } - } - } - } + const {drmInfos, keyIds, encrypted, aesEncrypted} = + this.parseDrmInfo_(playlist, mimeType); if (encrypted && !drmInfos.length && !aesEncrypted) { throw new shaka.util.Error( @@ -2016,6 +1982,64 @@ shaka.hls.HlsParser = class { }; } + /** + * @param {!shaka.hls.Playlist} playlist + * @param {string} mimeType + * @private + */ + parseDrmInfo_(playlist, mimeType) { + /** @type {!Array.} */ + const drmTags = []; + if (playlist.segments) { + for (const segment of playlist.segments) { + const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags, + 'EXT-X-KEY'); + drmTags.push(...segmentKeyTags); + } + } + + let encrypted = false; + let aesEncrypted = false; + + /** @type {!Array.}*/ + const drmInfos = []; + const keyIds = new Set(); + + // TODO: May still need changes to support key rotation. + for (const drmTag of drmTags) { + const method = drmTag.getRequiredAttrValue('METHOD'); + if (method != 'NONE') { + encrypted = true; + + if (method == 'AES-128') { + // These keys are handled separately. + aesEncrypted = true; + } else { + // 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 + const keyFormat = + drmTag.getAttributeValue('KEYFORMAT') || 'identity'; + const drmParser = + shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat]; + + const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null; + if (drmInfo) { + if (drmInfo.keyIds) { + for (const keyId of drmInfo.keyIds) { + keyIds.add(keyId); + } + } + drmInfos.push(drmInfo); + } else { + shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat); + } + } + } + } + + return {drmInfos, keyIds, encrypted, aesEncrypted}; + } /** * @param {!shaka.hls.Tag} drmTag diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 64fffe87e7..82e18285c5 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -1005,6 +1005,45 @@ describe('HlsParser live', () => { await testUpdate( manifest, mediaWithSkippedSegments2, [ref1, ref2, ref3, ref4]); }); + + it('updates encryption keys', async () => { + const initialKey = 'abc123'; + const media = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:6\n', + '#EXT-X-PLAYLIST-TYPE:EVENT\n', + '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', + 'KEYID=0X' + initialKey + ',', + 'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",', + 'URI="data:text/plain;base64,', + 'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE', '",\n', + '#EXT-X-MAP:URI="init.mp4"\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.mp4', + ].join(''); + + const updatedKey = 'xyz345'; + const updatedMedia = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:6\n', + '#EXT-X-PLAYLIST-TYPE:EVENT\n', + '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', + 'KEYID=0X' + updatedKey + ',', + 'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",', + 'URI="data:text/plain;base64,', + 'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE', '",\n', + '#EXT-X-MAP:URI="init.mp4"\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.mp4', + ].join(''); + + const manifest = await testInitialManifest(master, media, null); + await testUpdate(manifest, updatedMedia, null); + const keys = Array.from(manifest.variants[0].video.keyIds); + expect(keys[0]).toBe(updatedKey); + }); }); // describe('update') describe('createSegmentIndex', () => {