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. destroy() {
  52. // @ts-ignore
  53. this.observer = null;
  54. }
  55.  
  56. public isSync() {
  57. return this.config.enableSoftwareAES;
  58. }
  59.  
  60. public flush(): Uint8Array | void {
  61. const { currentResult } = this;
  62. if (!currentResult) {
  63. this.reset();
  64. return;
  65. }
  66. const data = new Uint8Array(currentResult);
  67. this.reset();
  68. if (this.removePKCS7Padding) {
  69. return removePadding(data);
  70. }
  71. return data;
  72. }
  73.  
  74. public reset() {
  75. this.currentResult = null;
  76. this.currentIV = null;
  77. this.remainderData = null;
  78. if (this.softwareDecrypter) {
  79. this.softwareDecrypter = null;
  80. }
  81. }
  82.  
  83. public decrypt(
  84. data: Uint8Array | ArrayBuffer,
  85. key: ArrayBuffer,
  86. iv: ArrayBuffer,
  87. callback: (decryptedData: ArrayBuffer) => void
  88. ) {
  89. if (this.config.enableSoftwareAES) {
  90. this.softwareDecrypt(new Uint8Array(data), key, iv);
  91. const decryptResult = this.flush();
  92. if (decryptResult) {
  93. callback(decryptResult.buffer);
  94. }
  95. } else {
  96. this.webCryptoDecrypt(new Uint8Array(data), key, iv).then(callback);
  97. }
  98. }
  99.  
  100. public softwareDecrypt(
  101. data: Uint8Array,
  102. key: ArrayBuffer,
  103. iv: ArrayBuffer
  104. ): ArrayBuffer | null {
  105. const { currentIV, currentResult, remainderData } = this;
  106. this.logOnce('JS AES decrypt');
  107. // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
  108. // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
  109. // the end on flush(), but by that time we have already received all bytes for the segment.
  110. // Progressive decryption does not work with WebCrypto
  111.  
  112. if (remainderData) {
  113. data = appendUint8Array(remainderData, data);
  114. this.remainderData = null;
  115. }
  116.  
  117. // Byte length must be a multiple of 16 (AES-128 = 128 bit blocks = 16 bytes)
  118. const currentChunk = this.getValidChunk(data);
  119. if (!currentChunk.length) {
  120. return null;
  121. }
  122.  
  123. if (currentIV) {
  124. iv = currentIV;
  125. }
  126.  
  127. let softwareDecrypter = this.softwareDecrypter;
  128. if (!softwareDecrypter) {
  129. softwareDecrypter = this.softwareDecrypter = new AESDecryptor();
  130. }
  131. softwareDecrypter.expandKey(key);
  132.  
  133. const result = currentResult;
  134.  
  135. this.currentResult = softwareDecrypter.decrypt(currentChunk.buffer, 0, iv);
  136. this.currentIV = sliceUint8(currentChunk, -16).buffer;
  137.  
  138. if (!result) {
  139. return null;
  140. }
  141. return result;
  142. }
  143.  
  144. public webCryptoDecrypt(
  145. data: Uint8Array,
  146. key: ArrayBuffer,
  147. iv: ArrayBuffer
  148. ): Promise<ArrayBuffer> {
  149. const subtle = this.subtle;
  150. if (this.key !== key || !this.fastAesKey) {
  151. this.key = key;
  152. this.fastAesKey = new FastAESKey(subtle, key);
  153. }
  154. return this.fastAesKey
  155. .expandKey()
  156. .then((aesKey) => {
  157. // decrypt using web crypto
  158. if (!subtle) {
  159. return Promise.reject(new Error('web crypto not initialized'));
  160. }
  161.  
  162. const crypto = new AESCrypto(subtle, iv);
  163. return crypto.decrypt(data.buffer, aesKey);
  164. })
  165. .catch((err) => {
  166. return this.onWebCryptoError(err, data, key, iv) as ArrayBuffer;
  167. });
  168. }
  169.  
  170. private onWebCryptoError(err, data, key, iv): ArrayBuffer | null {
  171. logger.warn('[decrypter.ts]: WebCrypto Error, disable WebCrypto API:', err);
  172. this.config.enableSoftwareAES = true;
  173. this.logEnabled = true;
  174. return this.softwareDecrypt(data, key, iv);
  175. }
  176.  
  177. private getValidChunk(data: Uint8Array): Uint8Array {
  178. let currentChunk = data;
  179. const splitPoint = data.length - (data.length % CHUNK_SIZE);
  180. if (splitPoint !== data.length) {
  181. currentChunk = sliceUint8(data, 0, splitPoint);
  182. this.remainderData = sliceUint8(data, splitPoint);
  183. }
  184. return currentChunk;
  185. }
  186.  
  187. private logOnce(msg: string) {
  188. if (!this.logEnabled) {
  189. return;
  190. }
  191. logger.log(`[decrypter.ts]: ${msg}`);
  192. this.logEnabled = false;
  193. }
  194. }