/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2013, Dash Industry Forum.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Microsoft PlayReady DRM
*
* @class
* @implements KeySystem
*/
import CommonEncryption from '../CommonEncryption';
import ProtectionConstants from '../../constants/ProtectionConstants';
const uuid = '9a04f079-9840-4286-ab92-e65be0885f95';
const systemString = ProtectionConstants.PLAYREADY_KEYSTEM_STRING;
const schemeIdURI = 'urn:uuid:' + uuid;
const PRCDMData = '<PlayReadyCDMData type="LicenseAcquisition"><LicenseAcquisition version="1.0" Proactive="false"><CustomData encoding="base64encoded">%CUSTOMDATA%</CustomData></LicenseAcquisition></PlayReadyCDMData>';
function KeySystemPlayReady(config) {
config = config || {};
let instance;
let messageFormat = 'utf-16';
const BASE64 = config.BASE64;
const settings = config.settings;
function checkConfig() {
if (!BASE64 || !BASE64.hasOwnProperty('decodeArray') || !BASE64.hasOwnProperty('decodeArray') ) {
throw new Error('Missing config parameter(s)');
}
}
function getRequestHeadersFromMessage(message) {
let msg,
xmlDoc;
const headers = {};
const parser = new DOMParser();
if (settings && settings.get().streaming.protection.detectPlayreadyMessageFormat) {
// If message format configured/defaulted to utf-16 AND number of bytes is odd, assume 'unwrapped' raw CDM message.
if (messageFormat === 'utf-16' && message && message.byteLength % 2 === 1) {
headers['Content-Type'] = 'text/xml; charset=utf-8';
return headers;
}
}
const dataview = (messageFormat === 'utf-16') ? new Uint16Array(message) : new Uint8Array(message);
msg = String.fromCharCode.apply(null, dataview);
xmlDoc = parser.parseFromString(msg, 'application/xml');
const headerNameList = xmlDoc.getElementsByTagName('name');
const headerValueList = xmlDoc.getElementsByTagName('value');
for (let i = 0; i < headerNameList.length; i++) {
headers[headerNameList[i].childNodes[0].nodeValue] = headerValueList[i].childNodes[0].nodeValue;
}
// Some versions of the PlayReady CDM return 'Content' instead of 'Content-Type'.
// this is NOT w3c conform and license servers may reject the request!
// -> rename it to proper w3c definition!
if (headers.hasOwnProperty('Content')) {
headers['Content-Type'] = headers.Content;
delete headers.Content;
}
// Set Content-Type header by default if not provided in the the CDM message (<PlayReadyKeyMessage/>)
// or if the message contains directly the challenge itself (Ex: LG SmartTVs)
if (!headers.hasOwnProperty('Content-Type')) {
headers['Content-Type'] = 'text/xml; charset=utf-8';
}
return headers;
}
function getLicenseRequestFromMessage(message) {
let licenseRequest = null;
const parser = new DOMParser();
if (settings && settings.get().streaming.protection.detectPlayreadyMessageFormat) {
// If message format configured/defaulted to utf-16 AND number of bytes is odd, assume 'unwrapped' raw CDM message.
if (messageFormat === 'utf-16' && message && message.byteLength % 2 === 1) {
return message;
}
}
const dataview = (messageFormat === 'utf-16') ? new Uint16Array(message) : new Uint8Array(message);
checkConfig();
const msg = String.fromCharCode.apply(null, dataview);
const xmlDoc = parser.parseFromString(msg, 'application/xml');
if (xmlDoc.getElementsByTagName('PlayReadyKeyMessage')[0]) {
const Challenge = xmlDoc.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue;
if (Challenge) {
licenseRequest = BASE64.decode(Challenge);
}
} else {
// The message from CDM is not a wrapped message as on IE11 and Edge,
// thus it contains direclty the challenge itself
// (note that the xmlDoc at this point may be unreadable since it may have been interpreted as UTF-16)
return message;
}
return licenseRequest;
}
function getLicenseServerURLFromInitData(initData) {
if (initData) {
const data = new DataView(initData);
const numRecords = data.getUint16(4, true);
let offset = 6;
const parser = new DOMParser();
for (let i = 0; i < numRecords; i++) {
// Parse the PlayReady Record header
const recordType = data.getUint16(offset, true);
offset += 2;
const recordLength = data.getUint16(offset, true);
offset += 2;
if (recordType !== 0x0001) {
offset += recordLength;
continue;
}
const recordData = initData.slice(offset, offset + recordLength);
const record = String.fromCharCode.apply(null, new Uint16Array(recordData));
const xmlDoc = parser.parseFromString(record, 'application/xml');
// First try <LA_URL>
if (xmlDoc.getElementsByTagName('LA_URL')[0]) {
const laurl = xmlDoc.getElementsByTagName('LA_URL')[0].childNodes[0].nodeValue;
if (laurl) {
return laurl;
}
}
// Optionally, try <LUI_URL>
if (xmlDoc.getElementsByTagName('LUI_URL')[0]) {
const luiurl = xmlDoc.getElementsByTagName('LUI_URL')[0].childNodes[0].nodeValue;
if (luiurl) {
return luiurl;
}
}
}
}
return null;
}
function getInitData(cpData) {
// * desc@ getInitData
// * generate PSSH data from PROHeader defined in MPD file
// * PSSH format:
// * size (4)
// * box type(PSSH) (8)
// * Protection SystemID (16)
// * protection system data size (4) - length of decoded PROHeader
// * decoded PROHeader data from MPD file
const PSSHBoxType = new Uint8Array([0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00]); //'PSSH' 8 bytes
const playreadySystemID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]);
let byteCursor = 0;
let uint8arraydecodedPROHeader = null;
let PROSize,
PSSHSize,
PSSHBoxBuffer,
PSSHBox,
PSSHData;
checkConfig();
if (!cpData) {
return null;
}
// Handle common encryption PSSH
if ('pssh' in cpData) {
return CommonEncryption.parseInitDataFromContentProtection(cpData, BASE64);
}
// Handle native MS PlayReady ContentProtection elements
if ('pro' in cpData) {
uint8arraydecodedPROHeader = BASE64.decodeArray(cpData.pro.__text);
}
else if ('prheader' in cpData) {
uint8arraydecodedPROHeader = BASE64.decodeArray(cpData.prheader.__text);
}
else {
return null;
}
PROSize = uint8arraydecodedPROHeader.length;
PSSHSize = 0x4 + PSSHBoxType.length + playreadySystemID.length + 0x4 + PROSize;
PSSHBoxBuffer = new ArrayBuffer(PSSHSize);
PSSHBox = new Uint8Array(PSSHBoxBuffer);
PSSHData = new DataView(PSSHBoxBuffer);
PSSHData.setUint32(byteCursor, PSSHSize);
byteCursor += 0x4;
PSSHBox.set(PSSHBoxType, byteCursor);
byteCursor += PSSHBoxType.length;
PSSHBox.set(playreadySystemID, byteCursor);
byteCursor += playreadySystemID.length;
PSSHData.setUint32(byteCursor, PROSize);
byteCursor += 0x4;
PSSHBox.set(uint8arraydecodedPROHeader, byteCursor);
byteCursor += PROSize;
return PSSHBox.buffer;
}
/**
* It seems that some PlayReady implementations return their XML-based CDM
* messages using UTF16, while others return them as UTF8. Use this function
* to modify the message format to expect when parsing CDM messages.
*
* @param {string} format the expected message format. Either "utf-8" or "utf-16".
* @throws {Error} Specified message format is not one of "utf8" or "utf16"
*/
function setPlayReadyMessageFormat(format) {
if (format !== 'utf-8' && format !== 'utf-16') {
throw new Error('Specified message format is not one of "utf-8" or "utf-16"');
}
messageFormat = format;
}
/**
* Get Playready Custom data
*/
function getCDMData(_cdmData) {
let customData,
cdmData,
cdmDataBytes,
i;
checkConfig();
if (!_cdmData) return null;
// Convert custom data into multibyte string
customData = [];
for (i = 0; i < _cdmData.length; ++i) {
customData.push(_cdmData.charCodeAt(i));
customData.push(0);
}
customData = String.fromCharCode.apply(null, customData);
// Encode in Base 64 the custom data string
customData = BASE64.encode(customData);
// Initialize CDM data with Base 64 encoded custom data
// (see https://msdn.microsoft.com/en-us/library/dn457361.aspx)
cdmData = PRCDMData.replace('%CUSTOMDATA%', customData);
// Convert CDM data into multibyte characters
cdmDataBytes = [];
for (i = 0; i < cdmData.length; ++i) {
cdmDataBytes.push(cdmData.charCodeAt(i));
cdmDataBytes.push(0);
}
return new Uint8Array(cdmDataBytes).buffer;
}
instance = {
uuid,
schemeIdURI,
systemString,
getInitData,
getRequestHeadersFromMessage,
getLicenseRequestFromMessage,
getLicenseServerURLFromInitData,
getCDMData,
setPlayReadyMessageFormat
};
return instance;
}
KeySystemPlayReady.__dashjs_factory_name = 'KeySystemPlayReady';
export default dashjs.FactoryMaker.getSingletonFactory(KeySystemPlayReady); /* jshint ignore:line */