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

Refactor protection and fix key rotation #3805

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
21 changes: 11 additions & 10 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ declare namespace dashjs {

clearMediaInfoArray(): void;

createKeySession(initData: ArrayBuffer, cdmData: Uint8Array): void;
createKeySession(ksInfo: KeySystemInfo): void;

loadKeySession(sessionId: string, initData: ArrayBuffer): void;
loadKeySession(ksInfo: KeySystemInfo): void;

removeKeySession(session: SessionToken): void;

Expand All @@ -63,7 +63,7 @@ declare namespace dashjs {

setProtectionData(protDataSet: ProtectionDataSet): void;

getSupportedKeySystemsFromContentProtection(cps: any[]): SupportedKeySystem[];
getSupportedKeySystemsFromContentProtection(cps: any[]): KeySystemInfo[];

getKeySystems(): KeySystem[];

Expand Down Expand Up @@ -1362,16 +1362,17 @@ declare namespace dashjs {

getLicenseServerURLFromInitData(initData: ArrayBuffer): string | null;

getCDMData(): ArrayBuffer | null;

getSessionId(): string | null;
getCDMData(cdmData: string | null): ArrayBuffer | null;
}

export interface SupportedKeySystem {
export interface KeySystemInfo {
ks: KeySystem;
initData: ArrayBuffer;
cdmData: ArrayBuffer | null;
sessionId: string | null;
sessionId?: string,
sessionType?: string,
keyId?: string,
initData?: ArrayBuffer;
cdmData?: ArrayBuffer;
protData?: ProtectionData
}

export interface LicenseRequest {
Expand Down
9 changes: 6 additions & 3 deletions src/dash/DashAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1026,9 +1026,12 @@ function DashAdapter() {
mediaInfo.selectionPriority = dashManifestModel.getSelectionPriority(realAdaptation);

if (mediaInfo.contentProtection) {
mediaInfo.contentProtection.forEach(function (item) {
item.KID = dashManifestModel.getKID(item);
});
// Get the default key ID and apply it to all key systems
const keyIds = mediaInfo.contentProtection.map(cp => dashManifestModel.getKID(cp)).filter(kid => kid !== null);
if (keyIds.length) {
const keyId = keyIds[0];
mediaInfo.contentProtection.forEach(cp => { cp.keyId = keyId; });
}
}

mediaInfo.isText = dashManifestModel.getIsText(realAdaptation);
Expand Down
105 changes: 62 additions & 43 deletions src/streaming/protection/controllers/ProtectionController.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function ProtectionController(config) {

// ContentProtection elements are specified at the AdaptationSet level, so the CP for audio
// and video will be the same. Just use one valid MediaInfo object
let supportedKS = protectionKeyController.getSupportedKeySystemsFromContentProtection(mediaInfo.contentProtection);
let supportedKS = protectionKeyController.getSupportedKeySystemsFromContentProtection(mediaInfo.contentProtection, protDataSet, sessionType);

// Reorder key systems according to priority order provided in protectionData
supportedKS = supportedKS.sort((ksA, ksB) => {
Expand Down Expand Up @@ -181,7 +181,7 @@ function ProtectionController(config) {

// Add all key systems to our request list since we have yet to select a key system
for (let i = 0; i < supportedKS.length; i++) {
const keySystemConfiguration = _getKeySystemConfiguration(supportedKS[i].ks);
const keySystemConfiguration = _getKeySystemConfiguration(supportedKS[i]);
requestedKeySystems.push({
ks: supportedKS[i].ks,
configs: [keySystemConfiguration]
Expand Down Expand Up @@ -218,7 +218,7 @@ function ProtectionController(config) {
for (ksIdx = 0; ksIdx < pendingKeySystemData[i].length; ksIdx++) {
if (selectedKeySystem === pendingKeySystemData[i][ksIdx].ks) {
const current = pendingKeySystemData[i][ksIdx]
_loadOrCreateKeySession(protData, current)
_loadOrCreateKeySession(current)
break;
}
}
Expand All @@ -241,7 +241,7 @@ function ProtectionController(config) {
* @param {array} supportedKS
* @private
*/
function _initiateWithExistingKeySystem(supportedKS,) {
function _initiateWithExistingKeySystem(supportedKS) {
const ksIdx = supportedKS.findIndex((entry) => {
return entry.ks === selectedKeySystem;
});
Expand All @@ -251,59 +251,62 @@ function ProtectionController(config) {
return;
}


// we only need to create or load a new key session if the key id has changed
if (_isKeyIdDuplicate(current.keyId)) {
return;
}

// we only need to create or load a new key session if the init data has changed
const initDataForKs = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, current.initData);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this, we have the keyId check above

Copy link
Contributor Author

@bbert bbert Nov 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an open question. Do we still need to check initData if we rely on key id?
I was especially thinking about contents that do not signal key id in manifest, i.e. for contents with in-band key rotation.
For example test stream "DRM (Modern) / "Multiperiod - Number + Timeline - Compact manifest - Thumbnails (1 track) - Encryption (1 key) PlayReady/Widevine (DRMtoday) - Key rotation (60s)"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to handle streams without default_kid in the manifest as if the kid did not change.

We then rely on the kid to change in the segments and handle this as part of needkey handler in the ProtectionController. There we extract the kid from sgpd and check if it has changed.

if (_isInitDataDuplicate(initDataForKs)) {
return;
}

const protData = _getProtDataForKeySystem(selectedKeySystem);
_loadOrCreateKeySession(protData, current);
_loadOrCreateKeySession(current);
}

/**
* Loads an existing key session if we already have a session id. Otherwise we create a new key session
* @param {object} protData
* @param {object} keySystemInfo
* @private
*/
function _loadOrCreateKeySession(protData, keySystemInfo) {
function _loadOrCreateKeySession(keySystemInfo) {
// Clearkey
if (protectionKeyController.isClearKey(selectedKeySystem)) {
// For Clearkey: if parameters for generating init data was provided by the user, use them for generating
// initData and overwrite possible initData indicated in encrypted event (EME)
if (protData && protData.hasOwnProperty('clearkeys')) {
const initData = { kids: Object.keys(protData.clearkeys) };
if (keySystemInfo.protData && keySystemInfo.protData.hasOwnProperty('clearkeys')) {
const initData = { kids: Object.keys(keySystemInfo.protData.clearkeys) };
keySystemInfo.initData = new TextEncoder().encode(JSON.stringify(initData));
}
}

// Reuse existing KeySession
if (keySystemInfo.sessionId) {
// Load MediaKeySession with sessionId
loadKeySession(keySystemInfo.sessionId, keySystemInfo.initData);
loadKeySession(keySystemInfo);
}

// Create a new KeySession
else if (keySystemInfo.initData !== null) {
// Create new MediaKeySession with initData
createKeySession(keySystemInfo.initData, keySystemInfo.cdmData);
createKeySession(keySystemInfo);
}
}

/**
* Loads a key session with the given session ID from persistent storage. This essentially creates a new key session
*
* @param {string} sessionID
* @param {string} initData
* @param {object} ksInfo
* @memberof module:ProtectionController
* @instance
* @fires ProtectionController#KeySessionCreated
* @ignore
*/
function loadKeySession(sessionID, initData) {
function loadKeySession(keySystemInfo) {
checkConfig();
protectionModel.loadKeySession(sessionID, initData, _getSessionType(selectedKeySystem));
protectionModel.loadKeySession(keySystemInfo);
}

/**
Expand All @@ -316,29 +319,32 @@ function ProtectionController(config) {
* @fires ProtectionController#KeySessionCreated
* @ignore
*/
function createKeySession(initData, cdmData) {
const initDataForKS = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, initData);
const protData = _getProtDataForKeySystem(selectedKeySystem);
function createKeySession(keySystemInfo) {
const initDataForKS = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, keySystemInfo ? keySystemInfo.initData : null);

if (initDataForKS) {

// Check for duplicate key id
if (_isKeyIdDuplicate(keySystemInfo.keyId)) {
return;
}

// Check for duplicate initData
if (_isInitDataDuplicate(initDataForKS)) {
dsilhavy marked this conversation as resolved.
Show resolved Hide resolved
return;
}

try {
const sessionType = _getSessionType(selectedKeySystem)
protectionModel.createKeySession(initDataForKS, protData, sessionType, cdmData);
keySystemInfo.initData = initDataForKS;
protectionModel.createKeySession(keySystemInfo);
} catch (error) {
eventBus.trigger(events.KEY_SESSION_CREATED, {
data: null,
error: new DashJSError(ProtectionErrors.KEY_SESSION_CREATED_ERROR_CODE, ProtectionErrors.KEY_SESSION_CREATED_ERROR_MESSAGE + error.message)
});
}
} else if (initData) {
const sessionType = _getSessionType(selectedKeySystem)
protectionModel.createKeySession(initData, protData, sessionType, cdmData);
} else if (keySystemInfo && keySystemInfo.initData) {
protectionModel.createKeySession(keySystemInfo);
} else {
eventBus.trigger(events.KEY_SESSION_CREATED, {
data: null,
Expand All @@ -364,18 +370,6 @@ function ProtectionController(config) {
return null;
}

/**
* Returns the session type either from the protData or as defined via setSessionType()
* @param keySystem
* @return {*}
* @private
*/
function _getSessionType(keySystem) {
const protData = _getProtDataForKeySystem(keySystem);

return (protData && protData.sessionType) ? protData.sessionType : sessionType;
}

/**
* Removes all entries from the mediaInfoArr
*/
Expand All @@ -400,7 +394,32 @@ function ProtectionController(config) {
*/
function getSupportedKeySystemsFromContentProtection(cps) {
checkConfig();
return protectionKeyController.getSupportedKeySystemsFromContentProtection(cps);
return protectionKeyController.getSupportedKeySystemsFromContentProtection(cps, protDataSet, sessionType);
}

/**
* Checks if a session has already created for the provided key id
* @param {string} keyId
* @return {boolean}
* @private
*/
function _isKeyIdDuplicate(keyId) {

if (!keyId) {
return false;
}

try {
const sessions = protectionModel.getSessions();
for (let i = 0; i < sessions.length; i++) {
if (sessions[i].getKeyId() === keyId) {
return true;
}
}
return false;
} catch (e) {
return false;
}
}

/**
Expand Down Expand Up @@ -595,13 +614,13 @@ function ProtectionController(config) {
* @return {KeySystemConfiguration}
* @private
*/
function _getKeySystemConfiguration(keySystem) {
const protData = _getProtDataForKeySystem(keySystem);
function _getKeySystemConfiguration(keySystemData) {
const protData = keySystemData;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be changed to keySystemData.protData

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

const audioCapabilities = [];
const videoCapabilities = [];
const audioRobustness = (protData && protData.audioRobustness && protData.audioRobustness.length > 0) ? protData.audioRobustness : robustnessLevel;
const videoRobustness = (protData && protData.videoRobustness && protData.videoRobustness.length > 0) ? protData.videoRobustness : robustnessLevel;
const ksSessionType = _getSessionType(keySystem);
const ksSessionType = keySystemData.sessionType;
const distinctiveIdentifier = (protData && protData.distinctiveIdentifier) ? protData.distinctiveIdentifier : 'optional';
const persistentState = (protData && protData.persistentState) ? protData.persistentState : (ksSessionType === 'temporary') ? 'optional' : 'required';

Expand Down Expand Up @@ -658,7 +677,7 @@ function ProtectionController(config) {

// Message not destined for license server
if (!licenseServerModelInstance) {
logger.debug('DRM: License server request not required for this message (type = ' + e.data.messageType + '). Session ID = ' + sessionToken.getSessionID());
logger.debug('DRM: License server request not required for this message (type = ' + e.data.messageType + '). Session ID = ' + sessionToken.getSessionId());
_sendLicenseRequestCompleteEvent(eventData);
return;
}
Expand Down Expand Up @@ -770,7 +789,7 @@ function ProtectionController(config) {
const reqMethod = licenseServerData.getHTTPMethod(messageType);
const responseType = licenseServerData.getResponseType(keySystemString, messageType);
const timeout = protData && !isNaN(protData.httpTimeout) ? protData.httpTimeout : LICENSE_SERVER_REQUEST_DEFAULT_TIMEOUT;
const sessionId = sessionToken.getSessionID() || null;
const sessionId = sessionToken.getSessionId() || null;

let licenseRequest = new LicenseRequest(url, reqMethod, responseType, reqHeaders, withCredentials, messageType, sessionId, reqPayload);
const retryAttempts = !isNaN(settings.get().streaming.retryAttempts[HTTPRequest.LICENSE]) ? settings.get().streaming.retryAttempts[HTTPRequest.LICENSE] : LICENSE_SERVER_REQUEST_RETRIES;
Expand Down Expand Up @@ -1021,7 +1040,7 @@ function ProtectionController(config) {

logger.debug('DRM: initData:', String.fromCharCode.apply(null, new Uint8Array(abInitData)));

const supportedKS = protectionKeyController.getSupportedKeySystems(abInitData, protDataSet);
const supportedKS = protectionKeyController.getSupportedKeySystems(abInitData, protDataSet, sessionType);
if (supportedKS.length === 0) {
logger.debug('DRM: Received needkey event with initData, but we don\'t support any of the key systems!');
return;
Expand Down
Loading