Home Reference Source

src/crypt/decrypter.ts

  1. import AESCrypto from './aes-crypto';
  2. import FastAESKey from './fast-aes-key';
  3. import AESDecryptor, { removePadding } from './aes-decryptor';
  4. import { logger } from '../utils/logger';
  5. import { appendUint8Array } from '../utils/mp4-tools';
  6. import { sliceUint8 } from '../utils/typed-array';
  7. import type { HlsConfig } from '../config';
  8. import type { HlsEventEmitter } from '../events';
  9.  
  10. const CHUNK_SIZE = 16; // 16 bytes, 128 bits
  11.  
  12. export default class Decrypter {
  13. private logEnabled: boolean = true;
  14. private observer: HlsEventEmitter;
  15. private config: HlsConfig;
  16. private removePKCS7Padding: boolean;
  17. private subtle: SubtleCrypto | null = null;
  18. private softwareDecrypter: AESDecryptor | null = null;
  19. private key: ArrayBuffer | null = null;
  20. private fastAesKey: FastAESKey | null = null;
  21. private remainderData: Uint8Array | null = null;
  22. private currentIV: ArrayBuffer | null = null;
  23. private currentResult: ArrayBuffer | null = null;
  24.  
  25. constructor(
  26. observer: HlsEventEmitter,
  27. config: HlsConfig,
  28. { removePKCS7Padding = true } = {}
  29. ) {
  30. this.observer = observer;
  31. this.config = config;
  32. this.removePKCS7Padding = removePKCS7Padding;
  33. // built in decryptor expects PKCS7 padding
  34. if (removePKCS7Padding) {
  35. try {
  36. const browserCrypto = self.crypto;
  37. if (browserCrypto) {
  38. this.subtle =
  39. browserCrypto.subtle ||
  40. ((browserCrypto as any).webkitSubtle as SubtleCrypto);
  41. }
  42. } catch (e) {
  43. /* no-op */
  44. }
  45. }
  46. if (this.subtle === null) {
  47. this.config.enableSoftwareAES = true;
  48. }
  49. }
  50.  
  51. public isSync() {
  52. return this.config.enableSoftwareAES;
  53. }
  54.  
  55. public flush(): Uint8Array | void {
  56. const { currentResult } = this;
  57. if (!currentResult) {
  58. this.reset();
  59. return;
  60. }
  61. const data = new Uint8Array(currentResult);
  62. this.reset();
  63. if (this.removePKCS7Padding) {
  64. return removePadding(data);
  65. }
  66. return data;
  67. }
  68.  
  69. public reset() {
  70. this.currentResult = null;
  71. this.currentIV = null;
  72. this.remainderData = null;
  73. if (this.softwareDecrypter) {
  74. this.softwareDecrypter = null;
  75. }
  76. }
  77.  
  78. public decrypt(
  79. data: Uint8Array | ArrayBuffer,
  80. key: ArrayBuffer,
  81. iv: ArrayBuffer,
  82. callback: (decryptedData: ArrayBuffer) => void
  83. ) {
  84. if (this.config.enableSoftwareAES) {
  85. this.softwareDecrypt(new Uint8Array(data), key, iv);
  86. const decryptResult = this.flush();
  87. if (decryptResult) {
  88. callback(decryptResult.buffer);
  89. }
  90. } else {
  91. this.webCryptoDecrypt(new Uint8Array(data), key, iv).then(callback);
  92. }
  93. }
  94.  
  95. public softwareDecrypt(
  96. data: Uint8Array,
  97. key: ArrayBuffer,
  98. iv: ArrayBuffer
  99. ): ArrayBuffer | null {
  100. const { currentIV, currentResult, remainderData } = this;
  101. this.logOnce('JS AES decrypt');
  102. // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
  103. // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
  104. // the end on flush(), but by that time we have already received all bytes for the segment.
  105. // Progressive decryption does not work with WebCrypto
  106.  
  107. if (remainderData) {
  108. data = appendUint8Array(remainderData, data);
  109. this.remainderData = null;
  110. }
  111.  
  112. // Byte length must be a multiple of 16 (AES-128 = 128 bit blocks = 16 bytes)
  113. const currentChunk = this.getValidChunk(data);
  114. if (!currentChunk.length) {
  115. return null;
  116. }
  117.  
  118. if (currentIV) {
  119. iv = currentIV;
  120. }
  121.  
  122. let softwareDecrypter = this.softwareDecrypter;
  123. if (!softwareDecrypter) {
  124. softwareDecrypter = this.softwareDecrypter = new AESDecryptor();
  125. }
  126. softwareDecrypter.expandKey(key);
  127.  
  128. const result = currentResult;
  129.  
  130. this.currentResult = softwareDecrypter.decrypt(currentChunk.buffer, 0, iv);
  131. this.currentIV = sliceUint8(currentChunk, -16).buffer;
  132.  
  133. if (!result) {
  134. return null;
  135. }
  136. return result;
  137. }
  138.  
  139. public webCryptoDecrypt(
  140. data: Uint8Array,
  141. key: ArrayBuffer,
  142. iv: ArrayBuffer
  143. ): Promise<ArrayBuffer> {
  144. const subtle = this.subtle;
  145. if (this.key !== key || !this.fastAesKey) {
  146. this.key = key;
  147. this.fastAesKey = new FastAESKey(subtle, key);
  148. }
  149. return this.fastAesKey
  150. .expandKey()
  151. .then((aesKey) => {
  152. // decrypt using web crypto
  153. if (!subtle) {
  154. return Promise.reject(new Error('web crypto not initialized'));
  155. }
  156.  
  157. const crypto = new AESCrypto(subtle, iv);
  158. return crypto.decrypt(data.buffer, aesKey);
  159. })
  160. .catch((err) => {
  161. return this.onWebCryptoError(err, data, key, iv) as ArrayBuffer;
  162. });
  163. }
  164.  
  165. private onWebCryptoError(err, data, key, iv): ArrayBuffer | null {
  166. logger.warn('[decrypter.ts]: WebCrypto Error, disable WebCrypto API:', err);
  167. this.config.enableSoftwareAES = true;
  168. this.logEnabled = true;
  169. return this.softwareDecrypt(data, key, iv);
  170. }
  171.  
  172. private getValidChunk(data: Uint8Array): Uint8Array {
  173. let currentChunk = data;
  174. const splitPoint = data.length - (data.length % CHUNK_SIZE);
  175. if (splitPoint !== data.length) {
  176. currentChunk = sliceUint8(data, 0, splitPoint);
  177. this.remainderData = sliceUint8(data, splitPoint);
  178. }
  179. return currentChunk;
  180. }
  181.  
  182. private logOnce(msg: string) {
  183. if (!this.logEnabled) {
  184. return;
  185. }
  186. logger.log(`[decrypter.ts]: ${msg}`);
  187. this.logEnabled = false;
  188. }
  189. }