Source: lib/util/fairplay_utils.js

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