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