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. * @private
  128. */
  129. static basicInitDataTransform_(initData, initDataType, drmInfo) {
  130. if (initDataType !== 'skd') {
  131. return initData;
  132. }
  133. const StringUtils = shaka.util.StringUtils;
  134. const FairPlayUtils = shaka.util.FairPlayUtils;
  135. const cert = drmInfo.serverCertificate;
  136. const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
  137. const contentId = initDataAsString.split('skd://').pop();
  138. return FairPlayUtils.initDataTransform(initData, contentId, cert);
  139. }
  140. /**
  141. * Verimatrix initDataTransform configuration.
  142. *
  143. * @param {!Uint8Array} initData
  144. * @param {string} initDataType
  145. * @param {?shaka.extern.DrmInfo} drmInfo
  146. * @export
  147. */
  148. static verimatrixInitDataTransform(initData, initDataType, drmInfo) {
  149. return shaka.util.FairPlayUtils.basicInitDataTransform_(
  150. initData, initDataType, drmInfo);
  151. }
  152. /**
  153. * EZDRM initDataTransform configuration.
  154. *
  155. * @param {!Uint8Array} initData
  156. * @param {string} initDataType
  157. * @param {?shaka.extern.DrmInfo} drmInfo
  158. * @export
  159. */
  160. static ezdrmInitDataTransform(initData, initDataType, drmInfo) {
  161. if (initDataType !== 'skd') {
  162. return initData;
  163. }
  164. const StringUtils = shaka.util.StringUtils;
  165. const FairPlayUtils = shaka.util.FairPlayUtils;
  166. const cert = drmInfo.serverCertificate;
  167. const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
  168. const contentId = initDataAsString.split(';').pop();
  169. return FairPlayUtils.initDataTransform(initData, contentId, cert);
  170. }
  171. /**
  172. * Conax initDataTransform configuration.
  173. *
  174. * @param {!Uint8Array} initData
  175. * @param {string} initDataType
  176. * @param {?shaka.extern.DrmInfo} drmInfo
  177. * @export
  178. */
  179. static conaxInitDataTransform(initData, initDataType, drmInfo) {
  180. if (initDataType !== 'skd') {
  181. return initData;
  182. }
  183. const StringUtils = shaka.util.StringUtils;
  184. const FairPlayUtils = shaka.util.FairPlayUtils;
  185. const cert = drmInfo.serverCertificate;
  186. const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
  187. const skdValue = initDataAsString.split('skd://').pop().split('?').shift();
  188. const stringToArray = (string) => {
  189. // 2 bytes for each char
  190. const buffer = new ArrayBuffer(string.length * 2);
  191. const array = shaka.util.BufferUtils.toUint16(buffer);
  192. for (let i = 0, strLen = string.length; i < strLen; i++) {
  193. array[i] = string.charCodeAt(i);
  194. }
  195. return array;
  196. };
  197. const contentId = stringToArray(window.atob(skdValue));
  198. return FairPlayUtils.initDataTransform(initData, contentId, cert);
  199. }
  200. /**
  201. * ExpressPlay initDataTransform configuration.
  202. *
  203. * @param {!Uint8Array} initData
  204. * @param {string} initDataType
  205. * @param {?shaka.extern.DrmInfo} drmInfo
  206. * @export
  207. */
  208. static expressplayInitDataTransform(initData, initDataType, drmInfo) {
  209. return shaka.util.FairPlayUtils.basicInitDataTransform_(
  210. initData, initDataType, drmInfo);
  211. }
  212. /**
  213. * Verimatrix FairPlay request.
  214. *
  215. * @param {shaka.net.NetworkingEngine.RequestType} type
  216. * @param {shaka.extern.Request} request
  217. * @param {shaka.extern.RequestContext=} context
  218. * @export
  219. */
  220. static verimatrixFairPlayRequest(type, request, context) {
  221. if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
  222. return;
  223. }
  224. const body = /** @type {!(ArrayBuffer|ArrayBufferView)} */(request.body);
  225. const originalPayload = shaka.util.BufferUtils.toUint8(body);
  226. const base64Payload = shaka.util.Uint8ArrayUtils.toBase64(originalPayload);
  227. request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
  228. request.body = shaka.util.StringUtils.toUTF8('spc=' + base64Payload);
  229. }
  230. /**
  231. * Set content-type to application/octet-stream in a FairPlay request.
  232. *
  233. * @param {shaka.net.NetworkingEngine.RequestType} type
  234. * @param {shaka.extern.Request} request
  235. * @param {shaka.extern.RequestContext=} context
  236. * @private
  237. */
  238. static octetStreamFairPlayRequest_(type, request, context) {
  239. if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
  240. return;
  241. }
  242. request.headers['Content-Type'] = 'application/octet-stream';
  243. }
  244. /**
  245. * EZDRM FairPlay request.
  246. *
  247. * @param {shaka.net.NetworkingEngine.RequestType} type
  248. * @param {shaka.extern.Request} request
  249. * @param {shaka.extern.RequestContext=} context
  250. * @export
  251. */
  252. static ezdrmFairPlayRequest(type, request, context) {
  253. shaka.util.FairPlayUtils.octetStreamFairPlayRequest_(type, request);
  254. }
  255. /**
  256. * Conax FairPlay request.
  257. *
  258. * @param {shaka.net.NetworkingEngine.RequestType} type
  259. * @param {shaka.extern.Request} request
  260. * @param {shaka.extern.RequestContext=} context
  261. * @export
  262. */
  263. static conaxFairPlayRequest(type, request, context) {
  264. shaka.util.FairPlayUtils.octetStreamFairPlayRequest_(type, request);
  265. }
  266. /**
  267. * ExpressPlay FairPlay request.
  268. *
  269. * @param {shaka.net.NetworkingEngine.RequestType} type
  270. * @param {shaka.extern.Request} request
  271. * @param {shaka.extern.RequestContext=} context
  272. * @export
  273. */
  274. static expressplayFairPlayRequest(type, request, context) {
  275. shaka.util.FairPlayUtils.octetStreamFairPlayRequest_(type, request);
  276. }
  277. /**
  278. * Common FairPlay response transform for some DRMs providers.
  279. *
  280. * @param {shaka.net.NetworkingEngine.RequestType} type
  281. * @param {shaka.extern.Response} response
  282. * @param {shaka.extern.RequestContext=} context
  283. * @export
  284. */
  285. static commonFairPlayResponse(type, response, context) {
  286. if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
  287. return;
  288. }
  289. // In Apple's docs, responses can be of the form:
  290. // '\n<ckc>base64encoded</ckc>\n' or 'base64encoded'
  291. // We have also seen responses in JSON format from some of our partners.
  292. // In all of these text-based formats, the CKC data is base64-encoded.
  293. let responseText;
  294. try {
  295. // Convert it to text for further processing.
  296. responseText = shaka.util.StringUtils.fromUTF8(response.data);
  297. } catch (error) {
  298. // Assume it's not a text format of any kind and leave it alone.
  299. return;
  300. }
  301. let licenseProcessing = false;
  302. // Trim whitespace.
  303. responseText = responseText.trim();
  304. // Look for <ckc> wrapper and remove it.
  305. if (responseText.substr(0, 5) === '<ckc>' &&
  306. responseText.substr(-6) === '</ckc>') {
  307. responseText = responseText.slice(5, -6);
  308. licenseProcessing = true;
  309. }
  310. // Look for a JSON wrapper and remove it.
  311. try {
  312. const responseObject = /** @type {!Object} */(JSON.parse(responseText));
  313. if (responseObject['ckc']) {
  314. responseText = responseObject['ckc'];
  315. licenseProcessing = true;
  316. }
  317. if (responseObject['CkcMessage']) {
  318. responseText = responseObject['CkcMessage'];
  319. licenseProcessing = true;
  320. }
  321. if (responseObject['License']) {
  322. responseText = responseObject['License'];
  323. licenseProcessing = true;
  324. }
  325. } catch (err) {
  326. // It wasn't JSON. Fall through with other transformations.
  327. }
  328. if (licenseProcessing) {
  329. // Decode the base64-encoded data into the format the browser expects.
  330. // It's not clear why FairPlay license servers don't just serve this
  331. // directly.
  332. response.data = shaka.util.BufferUtils.toArrayBuffer(
  333. shaka.util.Uint8ArrayUtils.fromBase64(responseText));
  334. }
  335. }
  336. };