Source: lib/drm/playready.js

/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.drm.PlayReady');

goog.require('shaka.log');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TXml');
goog.require('shaka.util.Uint8ArrayUtils');


/**
 * @summary A set of functions for parsing Microsoft Playready Objects.
 */
shaka.drm.PlayReady = class {
  /**
   * Parses an Array buffer starting at byteOffset for PlayReady Object Records.
   * Each PRO Record is preceded by its PlayReady Record type and length in
   * bytes.
   *
   * PlayReady Object Record format: https://goo.gl/FTcu46
   *
   * @param {!DataView} view
   * @param {number} byteOffset
   * @return {!Array.<shaka.drm.PlayReady.PlayReadyRecord>}
   * @private
   */
  static parseMsProRecords_(view, byteOffset) {
    const records = [];

    while (byteOffset < view.byteLength - 1) {
      const type = view.getUint16(byteOffset, true);
      byteOffset += 2;

      const byteLength = view.getUint16(byteOffset, true);
      byteOffset += 2;

      if ((byteLength & 1) != 0 || byteLength + byteOffset > view.byteLength) {
        shaka.log.warning('Malformed MS PRO object');
        return [];
      }

      const recordValue = shaka.util.BufferUtils.toUint8(
          view, byteOffset, byteLength);
      records.push({
        type: type,
        value: recordValue,
      });

      byteOffset += byteLength;
    }

    return records;
  }

  /**
   * Parses a buffer for PlayReady Objects.  The data
   * should contain a 32-bit integer indicating the length of
   * the PRO in bytes.  Following that, a 16-bit integer for
   * the number of PlayReady Object Records in the PRO.  Lastly,
   * a byte array of the PRO Records themselves.
   *
   * PlayReady Object format: https://goo.gl/W8yAN4
   *
   * @param {BufferSource} data
   * @return {!Array.<shaka.drm.PlayReady.PlayReadyRecord>}
   * @private
   */
  static parseMsPro_(data) {
    let byteOffset = 0;
    const view = shaka.util.BufferUtils.toDataView(data);

    // First 4 bytes is the PRO length (DWORD)
    const byteLength = view.getUint32(byteOffset, /* littleEndian= */ true);
    byteOffset += 4;

    if (byteLength != data.byteLength) {
      // Malformed PRO
      shaka.log.warning('PlayReady Object with invalid length encountered.');
      return [];
    }

    // Skip PRO Record count (WORD)
    byteOffset += 2;

    // Rest of the data contains the PRO Records
    return shaka.drm.PlayReady.parseMsProRecords_(view, byteOffset);
  }

  /**
   * PlayReady Header format: https://goo.gl/dBzxNA
   *
   * @param {!shaka.extern.xml.Node} xml
   * @return {string}
   * @private
   */
  static getLaurl_(xml) {
    const TXml = shaka.util.TXml;
    // LA_URL element is optional and no more than one is
    // allowed inside the DATA element. Only absolute URLs are allowed.
    // If the LA_URL element exists, it must not be empty.
    for (const elem of TXml.getElementsByTagName(xml, 'DATA')) {
      if (elem.children) {
        for (const child of elem.children) {
          if (child.tagName == 'LA_URL') {
            return /** @type{string} */(shaka.util.TXml.getTextContents(child));
          }
        }
      }
    }

    // Not found
    return '';
  }

  /**
   * Gets a PlayReady Header Object
   *
   * @param {!shaka.extern.xml.Node} element
   * @return {?shaka.extern.xml.Node}
   * @private
   */
  static getPlayReadyHeaderObject_(element) {
    const PLAYREADY_RECORD_TYPES = shaka.drm.PlayReady.PLAYREADY_RECORD_TYPES;

    const bytes = shaka.util.Uint8ArrayUtils.fromBase64(
        /** @type{string} */ (shaka.util.TXml.getTextContents(element)));
    const records = shaka.drm.PlayReady.parseMsPro_(bytes);
    const record = records.filter((record) => {
      return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT;
    })[0];

    if (!record) {
      return null;
    }

    const xml = shaka.util.StringUtils.fromUTF16(record.value, true);
    const rootElement = shaka.util.TXml.parseXmlString(xml, 'WRMHEADER');
    if (!rootElement) {
      return null;
    }
    return rootElement;
  }

  /**
   * Gets a PlayReady license URL from a PlayReady Header Object
   *
   * @param {!shaka.extern.xml.Node} element
   * @return {string}
   */
  static getLicenseUrl(element) {
    const headerObject = shaka.drm.PlayReady.getPlayReadyHeaderObject_(element);
    if (!headerObject) {
      return '';
    }

    return shaka.drm.PlayReady.getLaurl_(headerObject);
  }

  /**
   * Gets a PlayReady KID from a protection element containing a
   * PlayReady Header Object
   *
   * @param {!shaka.extern.xml.Node} element
   * @return {?string}
   */
  static getPlayReadyKID(element) {
    const rootElement = shaka.drm.PlayReady.getPlayReadyHeaderObject_(element);
    if (!rootElement) {
      return null;
    }

    const TXml = shaka.util.TXml;
    // KID element is optional and no more than one is
    // allowed inside the DATA element.
    for (const elem of TXml.getElementsByTagName(rootElement, 'DATA')) {
      const kid = TXml.findChild(elem, 'KID');
      if (kid) {
        // GUID: [DWORD, WORD, WORD, 8-BYTE]
        const guidBytes =
            shaka.util.Uint8ArrayUtils.fromBase64(
                /** @type{string} */ (shaka.util.TXml.getTextContents(kid)));
        // Reverse byte order from little-endian to big-endian
        const kidBytes = new Uint8Array([
          guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0],
          guidBytes[5], guidBytes[4],
          guidBytes[7], guidBytes[6],
          ...guidBytes.slice(8),
        ]);
        return shaka.util.Uint8ArrayUtils.toHex(kidBytes);
      }
    }

    // Not found
    return null;
  }
};

/**
 * @typedef {{
 *   type: number,
 *   value: !Uint8Array
 * }}
 *
 * @description
 * The parsed result of a PlayReady object record.
 *
 * @property {number} type
 *   Type of data stored in the record.
 * @property {!Uint8Array} value
 *   Record content.
 */
shaka.drm.PlayReady.PlayReadyRecord;

/**
 * Enum for PlayReady record types.
 * @enum {number}
 */
shaka.drm.PlayReady.PLAYREADY_RECORD_TYPES = {
  RIGHTS_MANAGEMENT: 0x001,
  RESERVED: 0x002,
  EMBEDDED_LICENSE: 0x003,
};