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