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 (observer: HlsEventEmitter, config: HlsConfig, { removePKCS7Padding = true } = {}) {
  26. this.observer = observer;
  27. this.config = config;
  28. this.removePKCS7Padding = removePKCS7Padding;
  29. // built in decryptor expects PKCS7 padding
  30. if (removePKCS7Padding) {
  31. try {
  32. const browserCrypto = self.crypto;
  33. if (browserCrypto) {
  34. this.subtle = browserCrypto.subtle || (browserCrypto as any).webkitSubtle as SubtleCrypto;
  35. } else {
  36. this.config.enableSoftwareAES = true;
  37. }
  38. } catch (e) { /* no-op */ }
  39. }
  40. }
  41.  
  42. isSync () {
  43. return this.config.enableSoftwareAES;
  44. }
  45.  
  46. flush (): Uint8Array | void {
  47. const { currentResult } = this;
  48. if (!currentResult) {
  49. this.reset();
  50. return;
  51. }
  52. const data = new Uint8Array(currentResult);
  53. this.reset();
  54. if (this.removePKCS7Padding) {
  55. return removePadding(data);
  56. }
  57. return data;
  58. }
  59.  
  60. reset () {
  61. this.currentResult = null;
  62. this.currentIV = null;
  63. this.remainderData = null;
  64. if (this.softwareDecrypter) {
  65. this.softwareDecrypter = null;
  66. }
  67. }
  68.  
  69. public softwareDecrypt (data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer): ArrayBuffer | null {
  70. const { currentIV, currentResult, remainderData } = this;
  71. this.logOnce('JS AES decrypt');
  72. // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
  73. // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
  74. // the end on flush(), but by that time we have already received all bytes for the segment.
  75. // Progressive decryption does not work with WebCrypto
  76.  
  77. if (remainderData) {
  78. data = appendUint8Array(remainderData, data);
  79. this.remainderData = null;
  80. }
  81.  
  82. // Byte length must be a multiple of 16 (AES-128 = 128 bit blocks = 16 bytes)
  83. const currentChunk = this.getValidChunk(data);
  84. if (!currentChunk.length) {
  85. return null;
  86. }
  87.  
  88. if (currentIV) {
  89. iv = currentIV;
  90. }
  91.  
  92. let softwareDecrypter = this.softwareDecrypter;
  93. if (!softwareDecrypter) {
  94. softwareDecrypter = this.softwareDecrypter = new AESDecryptor();
  95. }
  96. softwareDecrypter.expandKey(key);
  97.  
  98. const result = currentResult;
  99.  
  100. this.currentResult = softwareDecrypter.decrypt(currentChunk.buffer, 0, iv);
  101. this.currentIV = sliceUint8(currentChunk, -16).buffer;
  102.  
  103. if (!result) {
  104. return null;
  105. }
  106. return result;
  107. }
  108.  
  109. public webCryptoDecrypt (data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
  110. const subtle = this.subtle;
  111. if (this.key !== key || !this.fastAesKey) {
  112. this.key = key;
  113. this.fastAesKey = new FastAESKey(subtle, key);
  114. }
  115. return this.fastAesKey.expandKey()
  116. .then((aesKey) => {
  117. // decrypt using web crypto
  118. if (!subtle) {
  119. return Promise.reject(new Error('web crypto not initialized'));
  120. }
  121.  
  122. const crypto = new AESCrypto(subtle, iv);
  123. return crypto.decrypt(data.buffer, aesKey);
  124. })
  125. .catch((err) => {
  126. return this.onWebCryptoError(err, data, key, iv) as ArrayBuffer;
  127. });
  128. }
  129.  
  130. private onWebCryptoError (err, data, key, iv): ArrayBuffer | null {
  131. logger.warn('[decrypter.ts]: WebCrypto Error, disable WebCrypto API:', err);
  132. this.config.enableSoftwareAES = true;
  133. this.logEnabled = true;
  134. return this.softwareDecrypt(data, key, iv);
  135. }
  136.  
  137. private getValidChunk (data: Uint8Array) : Uint8Array {
  138. let currentChunk = data;
  139. const splitPoint = data.length - (data.length % CHUNK_SIZE);
  140. if (splitPoint !== data.length) {
  141. currentChunk = sliceUint8(data, 0, splitPoint);
  142. this.remainderData = sliceUint8(data, splitPoint);
  143. }
  144. return currentChunk;
  145. }
  146.  
  147. private logOnce (msg: string) {
  148. if (!this.logEnabled) {
  149. return;
  150. }
  151. logger.log(`[decrypter.ts]: ${msg}`);
  152. this.logEnabled = false;
  153. }
  154. }