Home Reference Source

src/loader/level-key.ts

  1. import {
  2. changeEndianness,
  3. convertDataUriToArrayBytes,
  4. } from '../utils/keysystem-util';
  5. import { KeySystemFormats } from '../utils/mediakeys-helper';
  6. import { mp4pssh } from '../utils/mp4-tools';
  7. import { logger } from '../utils/logger';
  8. import { base64Decode } from '../utils/numeric-encoding-utils';
  9.  
  10. let keyUriToKeyIdMap: { [uri: string]: Uint8Array } = {};
  11.  
  12. export interface DecryptData {
  13. uri: string;
  14. method: string;
  15. keyFormat: string;
  16. keyFormatVersions: number[];
  17. iv: Uint8Array | null;
  18. key: Uint8Array | null;
  19. keyId: Uint8Array | null;
  20. pssh: Uint8Array | null;
  21. encrypted: boolean;
  22. isCommonEncryption: boolean;
  23. }
  24.  
  25. export class LevelKey implements DecryptData {
  26. public readonly uri: string;
  27. public readonly method: string;
  28. public readonly keyFormat: string;
  29. public readonly keyFormatVersions: number[];
  30. public readonly encrypted: boolean;
  31. public readonly isCommonEncryption: boolean;
  32. public iv: Uint8Array | null = null;
  33. public key: Uint8Array | null = null;
  34. public keyId: Uint8Array | null = null;
  35. public pssh: Uint8Array | null = null;
  36.  
  37. static clearKeyUriToKeyIdMap() {
  38. keyUriToKeyIdMap = {};
  39. }
  40.  
  41. constructor(
  42. method: string,
  43. uri: string,
  44. format: string,
  45. formatversions: number[] = [1],
  46. iv: Uint8Array | null = null
  47. ) {
  48. this.method = method;
  49. this.uri = uri;
  50. this.keyFormat = format;
  51. this.keyFormatVersions = formatversions;
  52. this.iv = iv;
  53. this.encrypted = method ? method !== 'NONE' : false;
  54. this.isCommonEncryption = this.encrypted && method !== 'AES-128';
  55. }
  56.  
  57. public isSupported(): boolean {
  58. // If it's Segment encryption or No encryption, just select that key system
  59. if (this.method) {
  60. if (this.method === 'AES-128' || this.method === 'NONE') {
  61. return true;
  62. }
  63. switch (this.keyFormat) {
  64. case 'identity':
  65. // Maintain support for clear SAMPLE-AES with MPEG-3 TS
  66. return this.method === 'SAMPLE-AES';
  67. case KeySystemFormats.FAIRPLAY:
  68. case KeySystemFormats.WIDEVINE:
  69. case KeySystemFormats.PLAYREADY:
  70. case KeySystemFormats.CLEARKEY:
  71. return (
  72. [
  73. 'ISO-23001-7',
  74. 'SAMPLE-AES',
  75. 'SAMPLE-AES-CENC',
  76. 'SAMPLE-AES-CTR',
  77. ].indexOf(this.method) !== -1
  78. );
  79. }
  80. }
  81. return false;
  82. }
  83.  
  84. public getDecryptData(sn: number | 'initSegment'): LevelKey | null {
  85. if (!this.encrypted || !this.uri) {
  86. return null;
  87. }
  88.  
  89. if (this.method === 'AES-128' && this.uri && !this.iv) {
  90. if (typeof sn !== 'number') {
  91. // We are fetching decryption data for a initialization segment
  92. // If the segment was encrypted with AES-128
  93. // It must have an IV defined. We cannot substitute the Segment Number in.
  94. if (this.method === 'AES-128' && !this.iv) {
  95. logger.warn(
  96. `missing IV for initialization segment with method="${this.method}" - compliance issue`
  97. );
  98. }
  99. // Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
  100. sn = 0;
  101. }
  102. const iv = createInitializationVector(sn);
  103. const decryptdata = new LevelKey(
  104. this.method,
  105. this.uri,
  106. 'identity',
  107. this.keyFormatVersions,
  108. iv
  109. );
  110. return decryptdata;
  111. }
  112.  
  113. // Initialize keyId if possible
  114. const keyBytes = convertDataUriToArrayBytes(this.uri);
  115. if (keyBytes) {
  116. switch (this.keyFormat) {
  117. case KeySystemFormats.WIDEVINE:
  118. this.pssh = keyBytes;
  119. // In case of widevine keyID is embedded in PSSH box. Read Key ID.
  120. if (keyBytes.length >= 22) {
  121. this.keyId = keyBytes.subarray(
  122. keyBytes.length - 22,
  123. keyBytes.length - 6
  124. );
  125. }
  126. break;
  127. case KeySystemFormats.PLAYREADY: {
  128. const PlayReadyKeySystemUUID = new Uint8Array([
  129. 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6,
  130. 0x5b, 0xe0, 0x88, 0x5f, 0x95,
  131. ]);
  132.  
  133. this.pssh = mp4pssh(PlayReadyKeySystemUUID, null, keyBytes);
  134.  
  135. const keyBytesUtf16 = new Uint16Array(
  136. keyBytes.buffer,
  137. keyBytes.byteOffset,
  138. keyBytes.byteLength / 2
  139. );
  140. const keyByteStr = String.fromCharCode.apply(
  141. null,
  142. Array.from(keyBytesUtf16)
  143. );
  144.  
  145. // Parse Playready WRMHeader XML
  146. const xmlKeyBytes = keyByteStr.substring(
  147. keyByteStr.indexOf('<'),
  148. keyByteStr.length
  149. );
  150. const parser = new DOMParser();
  151. const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
  152. const keyData = xmlDoc.getElementsByTagName('KID')[0];
  153. if (keyData) {
  154. const keyId = keyData.childNodes[0]
  155. ? keyData.childNodes[0].nodeValue
  156. : keyData.getAttribute('VALUE');
  157. if (keyId) {
  158. const keyIdArray = base64Decode(keyId).subarray(0, 16);
  159. // KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
  160. // KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
  161. changeEndianness(keyIdArray);
  162. this.keyId = keyIdArray;
  163. }
  164. }
  165. break;
  166. }
  167. default: {
  168. let keydata = keyBytes.subarray(0, 16);
  169. if (keydata.length !== 16) {
  170. const padded = new Uint8Array(16);
  171. padded.set(keydata, 16 - keydata.length);
  172. keydata = padded;
  173. }
  174. this.keyId = keydata;
  175. break;
  176. }
  177. }
  178. }
  179.  
  180. // Default behavior: assign a new keyId for each uri
  181. if (!this.keyId || this.keyId.byteLength !== 16) {
  182. let keyId = keyUriToKeyIdMap[this.uri];
  183. if (!keyId) {
  184. const val =
  185. Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER;
  186. keyId = new Uint8Array(16);
  187. const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes
  188. dv.setUint32(0, val);
  189. keyUriToKeyIdMap[this.uri] = keyId;
  190. }
  191. this.keyId = keyId;
  192. }
  193.  
  194. return this;
  195. }
  196. }
  197.  
  198. function createInitializationVector(segmentNumber: number): Uint8Array {
  199. const uint8View = new Uint8Array(16);
  200. for (let i = 12; i < 16; i++) {
  201. uint8View[i] = (segmentNumber >> (8 * (15 - i))) & 0xff;
  202. }
  203. return uint8View;
  204. }