Source: lib/drm/fairplay.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.drm.FairPlay');
  7. goog.require('goog.Uri');
  8. goog.require('goog.asserts');
  9. goog.require('shaka.drm.DrmUtils');
  10. goog.require('shaka.net.NetworkingEngine');
  11. goog.require('shaka.util.BufferUtils');
  12. goog.require('shaka.util.Error');
  13. goog.require('shaka.util.StringUtils');
  14. goog.require('shaka.util.Uint8ArrayUtils');
  15. /**
  16. * @summary A set of FairPlay utility functions.
  17. * @export
  18. */
  19. shaka.drm.FairPlay = class {
  20. /**
  21. * Check if FairPlay is supported.
  22. *
  23. * @return {!Promise<boolean>}
  24. * @export
  25. */
  26. static async isFairPlaySupported() {
  27. const config = {
  28. initDataTypes: ['cenc', 'sinf', 'skd'],
  29. videoCapabilities: [
  30. {
  31. contentType: 'video/mp4; codecs="avc1.42E01E"',
  32. },
  33. ],
  34. };
  35. try {
  36. await navigator.requestMediaKeySystemAccess('com.apple.fps', [config]);
  37. return true;
  38. } catch (err) {
  39. return false;
  40. }
  41. }
  42. /**
  43. * Using the default method, extract a content ID from the init data. This is
  44. * based on the FairPlay example documentation.
  45. *
  46. * @param {!BufferSource} initData
  47. * @return {string}
  48. * @export
  49. */
  50. static defaultGetContentId(initData) {
  51. const uriString = shaka.util.StringUtils.fromBytesAutoDetect(initData);
  52. // The domain of that URI is the content ID according to Apple's FPS
  53. // sample.
  54. const uri = new goog.Uri(uriString);
  55. return uri.getDomain();
  56. }
  57. /**
  58. * Transforms the init data buffer using the given data. The format is:
  59. *
  60. * <pre>
  61. * [4 bytes] initDataSize
  62. * [initDataSize bytes] initData
  63. * [4 bytes] contentIdSize
  64. * [contentIdSize bytes] contentId
  65. * [4 bytes] certSize
  66. * [certSize bytes] cert
  67. * </pre>
  68. *
  69. * @param {!BufferSource} initData
  70. * @param {!BufferSource|string} contentId
  71. * @param {?BufferSource} cert The server certificate; this will throw if not
  72. * provided.
  73. * @return {!Uint8Array}
  74. * @export
  75. */
  76. static initDataTransform(initData, contentId, cert) {
  77. if (!cert || !cert.byteLength) {
  78. throw new shaka.util.Error(
  79. shaka.util.Error.Severity.CRITICAL,
  80. shaka.util.Error.Category.DRM,
  81. shaka.util.Error.Code.SERVER_CERTIFICATE_REQUIRED);
  82. }
  83. // From that, we build a new init data to use in the session. This is
  84. // composed of several parts. First, the init data as a UTF-16 sdk:// URL.
  85. // Second, a 4-byte LE length followed by the content ID in UTF-16-LE.
  86. // Third, a 4-byte LE length followed by the certificate.
  87. /** @type {BufferSource} */
  88. let contentIdArray;
  89. if (typeof contentId == 'string') {
  90. contentIdArray =
  91. shaka.util.StringUtils.toUTF16(contentId, /* littleEndian= */ true);
  92. } else {
  93. contentIdArray = contentId;
  94. }
  95. // The init data we get is a UTF-8 string; convert that to a UTF-16 string.
  96. const sdkUri = shaka.util.StringUtils.fromBytesAutoDetect(initData);
  97. const utf16 =
  98. shaka.util.StringUtils.toUTF16(sdkUri, /* littleEndian= */ true);
  99. const rebuiltInitData = new Uint8Array(
  100. 12 + utf16.byteLength + contentIdArray.byteLength + cert.byteLength);
  101. let offset = 0;
  102. /** @param {BufferSource} array */
  103. const append = (array) => {
  104. rebuiltInitData.set(shaka.util.BufferUtils.toUint8(array), offset);
  105. offset += array.byteLength;
  106. };
  107. /** @param {BufferSource} array */
  108. const appendWithLength = (array) => {
  109. const view = shaka.util.BufferUtils.toDataView(rebuiltInitData);
  110. const value = array.byteLength;
  111. view.setUint32(offset, value, /* littleEndian= */ true);
  112. offset += 4;
  113. append(array);
  114. };
  115. appendWithLength(utf16);
  116. appendWithLength(contentIdArray);
  117. appendWithLength(cert);
  118. goog.asserts.assert(
  119. offset == rebuiltInitData.length, 'Inconsistent init data length');
  120. return rebuiltInitData;
  121. }
  122. /**
  123. * Basic initDataTransform configuration.
  124. *
  125. * @param {!Uint8Array} initData
  126. * @param {string} initDataType
  127. * @param {?shaka.extern.DrmInfo} drmInfo
  128. * @return {!Uint8Array}
  129. * @private
  130. */
  131. static basicInitDataTransform_(initData, initDataType, drmInfo) {
  132. if (initDataType !== 'skd') {
  133. return initData;
  134. }
  135. const StringUtils = shaka.util.StringUtils;
  136. const cert = drmInfo.serverCertificate;
  137. const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
  138. const contentId = initDataAsString.split('skd://').pop();
  139. return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert);
  140. }
  141. /**
  142. * Verimatrix initDataTransform configuration.
  143. *
  144. * @param {!Uint8Array} initData
  145. * @param {string} initDataType
  146. * @param {?shaka.extern.DrmInfo} drmInfo
  147. * @return {!Uint8Array}
  148. * @export
  149. */
  150. static verimatrixInitDataTransform(initData, initDataType, drmInfo) {
  151. return shaka.drm.FairPlay.basicInitDataTransform_(
  152. initData, initDataType, drmInfo);
  153. }
  154. /**
  155. * EZDRM initDataTransform configuration.
  156. *
  157. * @param {!Uint8Array} initData
  158. * @param {string} initDataType
  159. * @param {?shaka.extern.DrmInfo} drmInfo
  160. * @return {!Uint8Array}
  161. * @export
  162. */
  163. static ezdrmInitDataTransform(initData, initDataType, drmInfo) {
  164. if (initDataType !== 'skd') {
  165. return initData;
  166. }
  167. const StringUtils = shaka.util.StringUtils;
  168. const cert = drmInfo.serverCertificate;
  169. const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
  170. const contentId = initDataAsString.split(';').pop();
  171. return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert);
  172. }
  173. /**
  174. * Conax initDataTransform configuration.
  175. *
  176. * @param {!Uint8Array} initData
  177. * @param {string} initDataType
  178. * @param {?shaka.extern.DrmInfo} drmInfo
  179. * @return {!Uint8Array}
  180. * @export
  181. */
  182. static conaxInitDataTransform(initData, initDataType, drmInfo) {
  183. if (initDataType !== 'skd') {
  184. return initData;
  185. }
  186. const StringUtils = shaka.util.StringUtils;
  187. const cert = drmInfo.serverCertificate;
  188. const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
  189. const skdValue = initDataAsString.split('skd://').pop().split('?').shift();
  190. const stringToArray = (string) => {
  191. // 2 bytes for each char
  192. const buffer = new ArrayBuffer(string.length * 2);
  193. const array = shaka.util.BufferUtils.toUint16(buffer);
  194. for (let i = 0, strLen = string.length; i < strLen; i++) {
  195. array[i] = string.charCodeAt(i);
  196. }
  197. return array;
  198. };
  199. const contentId = stringToArray(window.atob(skdValue));
  200. return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert);
  201. }
  202. /**
  203. * ExpressPlay initDataTransform configuration.
  204. *
  205. * @param {!Uint8Array} initData
  206. * @param {string} initDataType
  207. * @param {?shaka.extern.DrmInfo} drmInfo
  208. * @return {!Uint8Array}
  209. * @export
  210. */
  211. static expressplayInitDataTransform(initData, initDataType, drmInfo) {
  212. return shaka.drm.FairPlay.basicInitDataTransform_(
  213. initData, initDataType, drmInfo);
  214. }
  215. /**
  216. * Mux initDataTransform configuration.
  217. *
  218. * @param {!Uint8Array} initData
  219. * @param {string} initDataType
  220. * @param {?shaka.extern.DrmInfo} drmInfo
  221. * @return {!Uint8Array}
  222. * @export
  223. */
  224. static muxInitDataTransform(initData, initDataType, drmInfo) {
  225. return shaka.drm.FairPlay.basicInitDataTransform_(
  226. initData, initDataType, drmInfo);
  227. }
  228. /**
  229. * Verimatrix FairPlay request.
  230. *
  231. * @param {shaka.net.NetworkingEngine.RequestType} type
  232. * @param {shaka.extern.Request} request
  233. * @param {shaka.extern.RequestContext=} context
  234. * @export
  235. */
  236. static verimatrixFairPlayRequest(type, request, context) {
  237. if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
  238. return;
  239. }
  240. const drmInfo = request.drmInfo;
  241. if (!drmInfo ||
  242. !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) {
  243. return;
  244. }
  245. const body = /** @type {!(ArrayBuffer|ArrayBufferView)} */(request.body);
  246. const originalPayload = shaka.util.BufferUtils.toUint8(body);
  247. const base64Payload = shaka.util.Uint8ArrayUtils.toBase64(originalPayload);
  248. request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
  249. request.body = shaka.util.StringUtils.toUTF8('spc=' + base64Payload);
  250. }
  251. /**
  252. * Set content-type to application/octet-stream in a FairPlay request.
  253. *
  254. * @param {shaka.net.NetworkingEngine.RequestType} type
  255. * @param {shaka.extern.Request} request
  256. * @param {shaka.extern.RequestContext=} context
  257. * @private
  258. */
  259. static octetStreamFairPlayRequest_(type, request, context) {
  260. if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
  261. return;
  262. }
  263. const drmInfo = request.drmInfo;
  264. if (!drmInfo ||
  265. !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) {
  266. return;
  267. }
  268. request.headers['Content-Type'] = 'application/octet-stream';
  269. }
  270. /**
  271. * EZDRM FairPlay request.
  272. *
  273. * @param {shaka.net.NetworkingEngine.RequestType} type
  274. * @param {shaka.extern.Request} request
  275. * @param {shaka.extern.RequestContext=} context
  276. * @export
  277. */
  278. static ezdrmFairPlayRequest(type, request, context) {
  279. shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request);
  280. }
  281. /**
  282. * Conax FairPlay request.
  283. *
  284. * @param {shaka.net.NetworkingEngine.RequestType} type
  285. * @param {shaka.extern.Request} request
  286. * @param {shaka.extern.RequestContext=} context
  287. * @export
  288. */
  289. static conaxFairPlayRequest(type, request, context) {
  290. shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request);
  291. }
  292. /**
  293. * ExpressPlay FairPlay request.
  294. *
  295. * @param {shaka.net.NetworkingEngine.RequestType} type
  296. * @param {shaka.extern.Request} request
  297. * @param {shaka.extern.RequestContext=} context
  298. * @export
  299. */
  300. static expressplayFairPlayRequest(type, request, context) {
  301. if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
  302. return;
  303. }
  304. const drmInfo = request.drmInfo;
  305. if (!drmInfo ||
  306. !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) {
  307. return;
  308. }
  309. shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request);
  310. }
  311. /**
  312. * Mux FairPlay request.
  313. *
  314. * @param {shaka.net.NetworkingEngine.RequestType} type
  315. * @param {shaka.extern.Request} request
  316. * @param {shaka.extern.RequestContext=} context
  317. * @export
  318. */
  319. static muxFairPlayRequest(type, request, context) {
  320. shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request);
  321. }
  322. /**
  323. * Common FairPlay response transform for some DRMs providers.
  324. *
  325. * @param {shaka.net.NetworkingEngine.RequestType} type
  326. * @param {shaka.extern.Response} response
  327. * @param {shaka.extern.RequestContext=} context
  328. * @export
  329. */
  330. static commonFairPlayResponse(type, response, context) {
  331. if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
  332. return;
  333. }
  334. const drmInfo = response.originalRequest.drmInfo;
  335. if (!drmInfo ||
  336. !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) {
  337. return;
  338. }
  339. // In Apple's docs, responses can be of the form:
  340. // '\n<ckc>base64encoded</ckc>\n' or 'base64encoded'
  341. // We have also seen responses in JSON format from some of our partners.
  342. // In all of these text-based formats, the CKC data is base64-encoded.
  343. let responseText;
  344. try {
  345. // Convert it to text for further processing.
  346. responseText = shaka.util.StringUtils.fromUTF8(response.data);
  347. } catch (error) {
  348. // Assume it's not a text format of any kind and leave it alone.
  349. return;
  350. }
  351. let licenseProcessing = false;
  352. // Trim whitespace.
  353. responseText = responseText.trim();
  354. // Look for <ckc> wrapper and remove it.
  355. if (responseText.substr(0, 5) === '<ckc>' &&
  356. responseText.substr(-6) === '</ckc>') {
  357. responseText = responseText.slice(5, -6);
  358. licenseProcessing = true;
  359. }
  360. if (!licenseProcessing) {
  361. // Look for a JSON wrapper and remove it.
  362. try {
  363. const responseObject = /** @type {!Object} */(JSON.parse(responseText));
  364. if (responseObject['ckc']) {
  365. responseText = responseObject['ckc'];
  366. licenseProcessing = true;
  367. }
  368. if (responseObject['CkcMessage']) {
  369. responseText = responseObject['CkcMessage'];
  370. licenseProcessing = true;
  371. }
  372. if (responseObject['License']) {
  373. responseText = responseObject['License'];
  374. licenseProcessing = true;
  375. }
  376. } catch (err) {
  377. // It wasn't JSON. Fall through with other transformations.
  378. }
  379. }
  380. if (licenseProcessing) {
  381. // Decode the base64-encoded data into the format the browser expects.
  382. // It's not clear why FairPlay license servers don't just serve this
  383. // directly.
  384. response.data = shaka.util.BufferUtils.toArrayBuffer(
  385. shaka.util.Uint8ArrayUtils.fromBase64(responseText));
  386. }
  387. }
  388. };