Source: lib/mss/content_protection.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.mss.ContentProtection');
  7. goog.require('shaka.log');
  8. goog.require('shaka.util.BufferUtils');
  9. goog.require('shaka.util.ManifestParserUtils');
  10. goog.require('shaka.util.Pssh');
  11. goog.require('shaka.util.StringUtils');
  12. goog.require('shaka.util.TXml');
  13. goog.require('shaka.util.Uint8ArrayUtils');
  14. /**
  15. * @summary A set of functions for parsing and interpreting Protection
  16. * elements.
  17. */
  18. shaka.mss.ContentProtection = class {
  19. /**
  20. * Parses info from the Protection elements.
  21. *
  22. * @param {!Array.<!shaka.extern.xml.Node>} elems
  23. * @param {!Object.<string, string>} keySystemsBySystemId
  24. * @return {!Array.<shaka.extern.DrmInfo>}
  25. */
  26. static parseFromProtection(elems, keySystemsBySystemId) {
  27. const ContentProtection = shaka.mss.ContentProtection;
  28. const TXml = shaka.util.TXml;
  29. /** @type {!Array.<!shaka.extern.xml.Node>} */
  30. let protectionHeader = [];
  31. for (const elem of elems) {
  32. protectionHeader = protectionHeader.concat(
  33. TXml.findChildren(elem, 'ProtectionHeader'));
  34. }
  35. if (!protectionHeader.length) {
  36. return [];
  37. }
  38. return ContentProtection.convertElements_(
  39. protectionHeader, keySystemsBySystemId);
  40. }
  41. /**
  42. * Parses an Array buffer starting at byteOffset for PlayReady Object Records.
  43. * Each PRO Record is preceded by its PlayReady Record type and length in
  44. * bytes.
  45. *
  46. * PlayReady Object Record format: https://goo.gl/FTcu46
  47. *
  48. * @param {!DataView} view
  49. * @param {number} byteOffset
  50. * @return {!Array.<shaka.mss.ContentProtection.PlayReadyRecord>}
  51. * @private
  52. */
  53. static parseMsProRecords_(view, byteOffset) {
  54. const records = [];
  55. while (byteOffset < view.byteLength - 1) {
  56. const type = view.getUint16(byteOffset, true);
  57. byteOffset += 2;
  58. const byteLength = view.getUint16(byteOffset, true);
  59. byteOffset += 2;
  60. if ((byteLength & 1) != 0 || byteLength + byteOffset > view.byteLength) {
  61. shaka.log.warning('Malformed MS PRO object');
  62. return [];
  63. }
  64. const recordValue = shaka.util.BufferUtils.toUint8(
  65. view, byteOffset, byteLength);
  66. records.push({
  67. type: type,
  68. value: recordValue,
  69. });
  70. byteOffset += byteLength;
  71. }
  72. return records;
  73. }
  74. /**
  75. * Parses a buffer for PlayReady Objects. The data
  76. * should contain a 32-bit integer indicating the length of
  77. * the PRO in bytes. Following that, a 16-bit integer for
  78. * the number of PlayReady Object Records in the PRO. Lastly,
  79. * a byte array of the PRO Records themselves.
  80. *
  81. * PlayReady Object format: https://goo.gl/W8yAN4
  82. *
  83. * @param {BufferSource} data
  84. * @return {!Array.<shaka.mss.ContentProtection.PlayReadyRecord>}
  85. * @private
  86. */
  87. static parseMsPro_(data) {
  88. let byteOffset = 0;
  89. const view = shaka.util.BufferUtils.toDataView(data);
  90. // First 4 bytes is the PRO length (DWORD)
  91. const byteLength = view.getUint32(byteOffset, /* littleEndian= */ true);
  92. byteOffset += 4;
  93. if (byteLength != data.byteLength) {
  94. // Malformed PRO
  95. shaka.log.warning('PlayReady Object with invalid length encountered.');
  96. return [];
  97. }
  98. // Skip PRO Record count (WORD)
  99. byteOffset += 2;
  100. // Rest of the data contains the PRO Records
  101. const ContentProtection = shaka.mss.ContentProtection;
  102. return ContentProtection.parseMsProRecords_(view, byteOffset);
  103. }
  104. /**
  105. * Parse a PlayReady Header format: https://goo.gl/dBzxNA
  106. * a try to find the LA_URL value.
  107. *
  108. * @param {!shaka.extern.xml.Node} xml
  109. * @return {string}
  110. * @private
  111. */
  112. static getLaurl_(xml) {
  113. const TXml = shaka.util.TXml;
  114. // LA_URL element is optional and no more than one is
  115. // allowed inside the DATA element. Only absolute URLs are allowed.
  116. // If the LA_URL element exists, it must not be empty.
  117. for (const elem of TXml.getElementsByTagName(xml, 'DATA')) {
  118. const laUrl = TXml.findChild(elem, 'LA_URL');
  119. if (laUrl) {
  120. return /** @type {string} */ (shaka.util.TXml.getTextContents(laUrl));
  121. }
  122. }
  123. // Not found
  124. // We return a empty string instead null because is the default value for
  125. // a License in our model.
  126. return '';
  127. }
  128. /**
  129. * Gets a PlayReady license URL from a protection element
  130. * containing a PlayReady Header Object
  131. *
  132. * @param {!shaka.extern.xml.Node} element
  133. * @return {string}
  134. */
  135. static getPlayReadyLicenseUrl(element) {
  136. const ContentProtection = shaka.mss.ContentProtection;
  137. const rootElement = ContentProtection.getPlayReadyHeaderObject_(element);
  138. if (!rootElement) {
  139. return '';
  140. }
  141. return ContentProtection.getLaurl_(rootElement);
  142. }
  143. /**
  144. * Parse a PlayReady Header format: https://goo.gl/dBzxNA
  145. * a try to find the KID value.
  146. *
  147. * @param {!shaka.extern.xml.Node} xml
  148. * @return {?string}
  149. * @private
  150. */
  151. static getKID_(xml) {
  152. const TXml = shaka.util.TXml;
  153. // KID element is optional and no more than one is
  154. // allowed inside the DATA element.
  155. for (const elem of TXml.getElementsByTagName(xml, 'DATA')) {
  156. const kid = TXml.findChild(elem, 'KID');
  157. if (kid) {
  158. // GUID: [DWORD, WORD, WORD, 8-BYTE]
  159. const guidBytes =
  160. shaka.util.Uint8ArrayUtils.fromBase64(
  161. /** @type{string} */ (shaka.util.TXml.getTextContents(kid)));
  162. // Reverse byte order from little-endian to big-endian
  163. const kidBytes = new Uint8Array([
  164. guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0],
  165. guidBytes[5], guidBytes[4],
  166. guidBytes[7], guidBytes[6],
  167. ...guidBytes.slice(8),
  168. ]);
  169. return shaka.util.Uint8ArrayUtils.toHex(kidBytes);
  170. }
  171. }
  172. // Not found
  173. return null;
  174. }
  175. /**
  176. * Gets a PlayReady KID from a protection element
  177. * containing a PlayReady Header Object
  178. *
  179. * @param {!shaka.extern.xml.Node} element
  180. * @return {?string}
  181. * @private
  182. */
  183. static getPlayReadyKID_(element) {
  184. const ContentProtection = shaka.mss.ContentProtection;
  185. const rootElement = ContentProtection.getPlayReadyHeaderObject_(element);
  186. if (!rootElement) {
  187. return null;
  188. }
  189. return ContentProtection.getKID_(rootElement);
  190. }
  191. /**
  192. * Gets a PlayReady Header Object from a protection element
  193. *
  194. * @param {!shaka.extern.xml.Node} element
  195. * @return {?shaka.extern.xml.Node}
  196. * @private
  197. */
  198. static getPlayReadyHeaderObject_(element) {
  199. const ContentProtection = shaka.mss.ContentProtection;
  200. const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES;
  201. const bytes = shaka.util.Uint8ArrayUtils.fromBase64(
  202. /** @type{string} */ (shaka.util.TXml.getTextContents(element)));
  203. const records = ContentProtection.parseMsPro_(bytes);
  204. const record = records.filter((record) => {
  205. return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT;
  206. })[0];
  207. if (!record) {
  208. return null;
  209. }
  210. const xml = shaka.util.StringUtils.fromUTF16(record.value, true);
  211. const rootElement = shaka.util.TXml.parseXmlString(xml, 'WRMHEADER');
  212. if (!rootElement) {
  213. return null;
  214. }
  215. return rootElement;
  216. }
  217. /**
  218. * Gets a initData from a protection element.
  219. *
  220. * @param {!shaka.extern.xml.Node} element
  221. * @param {string} systemID
  222. * @param {?string} keyId
  223. * @return {?Array.<shaka.extern.InitDataOverride>}
  224. * @private
  225. */
  226. static getInitDataFromPro_(element, systemID, keyId) {
  227. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  228. const data = Uint8ArrayUtils.fromBase64(
  229. /** @type{string} */ (shaka.util.TXml.getTextContents(element)));
  230. const systemId = Uint8ArrayUtils.fromHex(systemID.replace(/-/g, ''));
  231. const keyIds = new Set();
  232. const psshVersion = 0;
  233. const pssh =
  234. shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion);
  235. return [
  236. {
  237. initData: pssh,
  238. initDataType: 'cenc',
  239. keyId: keyId,
  240. },
  241. ];
  242. }
  243. /**
  244. * Creates DrmInfo objects from an array of elements.
  245. *
  246. * @param {!Array.<!shaka.extern.xml.Node>} elements
  247. * @param {!Object.<string, string>} keySystemsBySystemId
  248. * @return {!Array.<shaka.extern.DrmInfo>}
  249. * @private
  250. */
  251. static convertElements_(elements, keySystemsBySystemId) {
  252. const ContentProtection = shaka.mss.ContentProtection;
  253. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  254. const licenseUrlParsers = ContentProtection.licenseUrlParsers_;
  255. /** @type {!Array.<shaka.extern.DrmInfo>} */
  256. const out = [];
  257. for (let i = 0; i < elements.length; i++) {
  258. const element = elements[i];
  259. const systemID = element.attributes['SystemID'].toLowerCase();
  260. const keySystem = keySystemsBySystemId[systemID];
  261. if (keySystem) {
  262. const KID = ContentProtection.getPlayReadyKID_(element);
  263. const initData = ContentProtection.getInitDataFromPro_(
  264. element, systemID, KID);
  265. const info = ManifestParserUtils.createDrmInfo(
  266. keySystem, /* encryptionScheme= */ 'cenc', initData);
  267. if (KID) {
  268. info.keyIds.add(KID);
  269. }
  270. const licenseParser = licenseUrlParsers.get(keySystem);
  271. if (licenseParser) {
  272. info.licenseServerUri = licenseParser(element);
  273. }
  274. out.push(info);
  275. }
  276. }
  277. return out;
  278. }
  279. };
  280. /**
  281. * @typedef {{
  282. * type: number,
  283. * value: !Uint8Array
  284. * }}
  285. *
  286. * @description
  287. * The parsed result of a PlayReady object record.
  288. *
  289. * @property {number} type
  290. * Type of data stored in the record.
  291. * @property {!Uint8Array} value
  292. * Record content.
  293. */
  294. shaka.mss.ContentProtection.PlayReadyRecord;
  295. /**
  296. * Enum for PlayReady record types.
  297. * @enum {number}
  298. */
  299. shaka.mss.ContentProtection.PLAYREADY_RECORD_TYPES = {
  300. RIGHTS_MANAGEMENT: 0x001,
  301. RESERVED: 0x002,
  302. EMBEDDED_LICENSE: 0x003,
  303. };
  304. /**
  305. * A map of key system name to license server url parser.
  306. *
  307. * @const {!Map.<string, function(!shaka.extern.xml.Node)>}
  308. * @private
  309. */
  310. shaka.mss.ContentProtection.licenseUrlParsers_ = new Map()
  311. .set('com.microsoft.playready',
  312. shaka.mss.ContentProtection.getPlayReadyLicenseUrl)
  313. .set('com.microsoft.playready.recommendation',
  314. shaka.mss.ContentProtection.getPlayReadyLicenseUrl)
  315. .set('com.microsoft.playready.software',
  316. shaka.mss.ContentProtection.getPlayReadyLicenseUrl)
  317. .set('com.microsoft.playready.hardware',
  318. shaka.mss.ContentProtection.getPlayReadyLicenseUrl);