summaryrefslogtreecommitdiffstats
path: root/eme_interception.js
diff options
context:
space:
mode:
Diffstat (limited to 'eme_interception.js')
-rw-r--r--eme_interception.js418
1 files changed, 418 insertions, 0 deletions
diff --git a/eme_interception.js b/eme_interception.js
new file mode 100644
index 0000000..d676881
--- /dev/null
+++ b/eme_interception.js
@@ -0,0 +1,418 @@
+/**
+ * Hooks EME calls and forwards them for analysis and decryption.
+ *
+ * Most of the code here was borrowed from https://github.com/google/eme_logger/blob/master/eme_listeners.js
+ */
+
+ var lastReceivedLicenseRequest = null;
+ var lastReceivedLicenseResponse = null;
+
+ /** Set up the EME listeners. */
+function startEMEInterception()
+{
+ var listener = new EmeInterception();
+ listener.setUpListeners();
+}
+
+ /**
+ * Gets called whenever an EME method is getting called or an EME event fires
+ */
+EmeInterception.onOperation = function(operationType, args)
+{
+ if (operationType == "GenerateRequestCall")
+ {
+ // got initData
+ // console.log(args);
+ }
+ else if (operationType == "MessageEvent")
+ {
+ var licenseRequest = args.message;
+ lastReceivedLicenseRequest = licenseRequest;
+ }
+ else if (operationType == "UpdateCall")
+ {
+ var licenseResponse = args[0];
+ lastReceivedLicenseResponse = licenseResponse;
+
+ // OK, let's try to decrypt it, assuming the response correlates to the request
+ WidevineCrypto.decryptContentKey(lastReceivedLicenseRequest, lastReceivedLicenseResponse);
+ }
+};
+
+
+/**
+ * Manager for EME event and method listeners.
+ * @constructor
+ */
+function EmeInterception()
+{
+ this.unprefixedEmeEnabled = Navigator.prototype.requestMediaKeySystemAccess ? true : false;
+ this.prefixedEmeEnabled = HTMLMediaElement.prototype.webkitGenerateKeyRequest ? true : false;
+}
+
+
+/**
+ * The number of types of HTML Media Elements to track.
+ * @const {number}
+ */
+EmeInterception.NUM_MEDIA_ELEMENT_TYPES = 3;
+
+
+/**
+ * Sets up EME listeners for whichever type of EME is enabled.
+ */
+EmeInterception.prototype.setUpListeners = function()
+{
+ if (!this.unprefixedEmeEnabled && !this.prefixedEmeEnabled) {
+ // EME is not enabled, just ignore
+ return;
+ }
+ if (this.unprefixedEmeEnabled) {
+ this.addListenersToNavigator_();
+ }
+ if (this.prefixedEmeEnabled) {
+ // Prefixed EME is enabled
+ }
+ this.addListenersToAllEmeElements_();
+};
+
+
+/**
+ * Adds listeners to the EME methods on the Navigator object.
+ * @private
+ */
+EmeInterception.prototype.addListenersToNavigator_ = function()
+{
+ if (navigator.listenersAdded_)
+ return;
+
+ var originalRequestMediaKeySystemAccessFn = EmeInterception.extendEmeMethod(
+ navigator,
+ navigator.requestMediaKeySystemAccess,
+ "RequestMediaKeySystemAccessCall");
+
+ navigator.requestMediaKeySystemAccess = function()
+ {
+ var options = arguments[1];
+
+ // slice "It is recommended that a robustness level be specified" warning
+ var modifiedArguments = arguments;
+ var modifiedOptions = EmeInterception.addRobustnessLevelIfNeeded(options);
+ modifiedArguments[1] = modifiedOptions;
+
+ var result = originalRequestMediaKeySystemAccessFn.apply(null, modifiedArguments);
+ // Attach listeners to returned MediaKeySystemAccess object
+ return result.then(function(mediaKeySystemAccess)
+ {
+ this.addListenersToMediaKeySystemAccess_(mediaKeySystemAccess);
+ return Promise.resolve(mediaKeySystemAccess);
+ }.bind(this));
+
+ }.bind(this);
+
+ navigator.listenersAdded_ = true;
+};
+
+
+/**
+ * Adds listeners to the EME methods on a MediaKeySystemAccess object.
+ * @param {MediaKeySystemAccess} mediaKeySystemAccess A MediaKeySystemAccess
+ * object to add listeners to.
+ * @private
+ */
+EmeInterception.prototype.addListenersToMediaKeySystemAccess_ = function(mediaKeySystemAccess)
+{
+ if (mediaKeySystemAccess.listenersAdded_) {
+ return;
+ }
+ mediaKeySystemAccess.originalGetConfiguration = mediaKeySystemAccess.getConfiguration;
+ mediaKeySystemAccess.getConfiguration = EmeInterception.extendEmeMethod(
+ mediaKeySystemAccess,
+ mediaKeySystemAccess.getConfiguration,
+ "GetConfigurationCall");
+
+ var originalCreateMediaKeysFn = EmeInterception.extendEmeMethod(
+ mediaKeySystemAccess,
+ mediaKeySystemAccess.createMediaKeys,
+ "CreateMediaKeysCall");
+
+ mediaKeySystemAccess.createMediaKeys = function()
+ {
+ var result = originalCreateMediaKeysFn.apply(null, arguments);
+ // Attach listeners to returned MediaKeys object
+ return result.then(function(mediaKeys) {
+ mediaKeys.keySystem_ = mediaKeySystemAccess.keySystem;
+ this.addListenersToMediaKeys_(mediaKeys);
+ return Promise.resolve(mediaKeys);
+ }.bind(this));
+
+ }.bind(this);
+
+ mediaKeySystemAccess.listenersAdded_ = true;
+};
+
+
+/**
+ * Adds listeners to the EME methods on a MediaKeys object.
+ * @param {MediaKeys} mediaKeys A MediaKeys object to add listeners to.
+ * @private
+ */
+EmeInterception.prototype.addListenersToMediaKeys_ = function(mediaKeys)
+{
+ if (mediaKeys.listenersAdded_) {
+ return;
+ }
+ var originalCreateSessionFn = EmeInterception.extendEmeMethod(mediaKeys, mediaKeys.createSession, "CreateSessionCall");
+ mediaKeys.createSession = function()
+ {
+ var result = originalCreateSessionFn.apply(null, arguments);
+ result.keySystem_ = mediaKeys.keySystem_;
+ // Attach listeners to returned MediaKeySession object
+ this.addListenersToMediaKeySession_(result);
+ return result;
+ }.bind(this);
+
+ mediaKeys.setServerCertificate = EmeInterception.extendEmeMethod(mediaKeys, mediaKeys.setServerCertificate, "SetServerCertificateCall");
+ mediaKeys.listenersAdded_ = true;
+};
+
+
+/** Adds listeners to the EME methods and events on a MediaKeySession object.
+ * @param {MediaKeySession} session A MediaKeySession object to add
+ * listeners to.
+ * @private
+ */
+EmeInterception.prototype.addListenersToMediaKeySession_ = function(session)
+{
+ if (session.listenersAdded_) {
+ return;
+ }
+
+ session.generateRequest = EmeInterception.extendEmeMethod(session,session.generateRequest, "GenerateRequestCall");
+ session.load = EmeInterception.extendEmeMethod(session, session.load, "LoadCall");
+ session.update = EmeInterception.extendEmeMethod(session,session.update, "UpdateCall");
+ session.close = EmeInterception.extendEmeMethod(session, session.close, "CloseCall");
+ session.remove = EmeInterception.extendEmeMethod(session, session.remove, "RemoveCall");
+
+ session.addEventListener('message', function(e)
+ {
+ e.keySystem = session.keySystem_;
+ EmeInterception.interceptEvent("MessageEvent", e);
+ });
+
+ session.addEventListener('keystatuseschange', EmeInterception.interceptEvent.bind(null, "KeyStatusesChangeEvent"));
+
+ session.listenersAdded_ = true;
+};
+
+
+/**
+ * Adds listeners to all currently created media elements (audio, video) and sets up a
+ * mutation-summary observer to add listeners to any newly created media
+ * elements.
+ * @private
+ */
+EmeInterception.prototype.addListenersToAllEmeElements_ = function()
+{
+ this.addEmeInterceptionToInitialMediaElements_();
+
+ // TODO: Use MutationObserver directry
+ // var observer = new MutationSummary({
+ // callback: function(summaries) {
+ // applyListeners(summaries);
+ // },
+ // queries: [{element: 'video'}, {element: 'audio'}, {element: 'media'}]
+ // });
+
+ // var applyListeners = function(summaries) {
+ // for (var i = 0; i < EmeInterception.NUM_MEDIA_ELEMENT_TYPES; i++) {
+ // var elements = summaries[i];
+ // elements.added.forEach(function(element) {
+ // this.addListenersToEmeElement_(element, true);
+ // }.bind(this));
+ // }
+ // }.bind(this);
+};
+
+
+/**
+ * Adds listeners to the EME elements currently in the document.
+ * @private
+ */
+EmeInterception.prototype.addEmeInterceptionToInitialMediaElements_ = function()
+{
+ var audioElements = document.getElementsByTagName('audio');
+ for (var i = 0; i < audioElements.length; ++i) {
+ this.addListenersToEmeElement_(audioElements[i], false);
+ }
+ var videoElements = document.getElementsByTagName('video');
+ for (var i = 0; i < videoElements.length; ++i) {
+ this.addListenersToEmeElement_(videoElements[i], false);
+ }
+ var mediaElements = document.getElementsByTagName('media');
+ for (var i = 0; i < mediaElements.length; ++i) {
+ this.addListenersToEmeElement_(mediaElements[i], false);
+ }
+};
+
+
+/**
+ * Adds method and event listeners to media element.
+ * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to.
+ * @private
+ */
+EmeInterception.prototype.addListenersToEmeElement_ = function(element)
+{
+ this.addEmeEventListeners_(element);
+ this.addEmeMethodListeners_(element);
+ console.info('EME listeners successfully added to:', element);
+};
+
+
+/**
+ * Adds event listeners to a media element.
+ * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to.
+ * @private
+ */
+EmeInterception.prototype.addEmeEventListeners_ = function(element)
+{
+ if (element.eventListenersAdded_) {
+ return;
+ }
+
+ if (this.prefixedEmeEnabled)
+ {
+ element.addEventListener('webkitneedkey', EmeInterception.interceptEvent.bind(null, "NeedKeyEvent"));
+ element.addEventListener('webkitkeymessage', EmeInterception.interceptEvent.bind(null, "KeyMessageEvent"));
+ element.addEventListener('webkitkeyadded', EmeInterception.interceptEvent.bind(null, "KeyAddedEvent"));
+ element.addEventListener('webkitkeyerror', EmeInterception.interceptEvent.bind(null, "KeyErrorEvent"));
+ }
+
+ element.addEventListener('encrypted', EmeInterception.interceptEvent.bind(null, "EncryptedEvent"));
+ element.addEventListener('play', EmeInterception.interceptEvent.bind(null, "PlayEvent"));
+
+ element.addEventListener('error', function(e) {
+ console.error('Error Event');
+ EmeInterception.interceptEvent("ErrorEvent", e);
+ });
+
+ element.eventListenersAdded_ = true;
+};
+
+
+/**
+ * Adds method listeners to a media element.
+ * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to.
+ * @private
+ */
+EmeInterception.prototype.addEmeMethodListeners_ = function(element)
+{
+ if (element.methodListenersAdded_) {
+ return;
+ }
+
+ element.play = EmeInterception.extendEmeMethod(element, element.play, "PlayCall");
+
+ if (this.prefixedEmeEnabled) {
+ element.canPlayType = EmeInterception.extendEmeMethod(element, element.canPlayType, "CanPlayTypeCall");
+
+ element.webkitGenerateKeyRequest = EmeInterception.extendEmeMethod(element, element.webkitGenerateKeyRequest, "GenerateKeyRequestCall");
+ element.webkitAddKey = EmeInterception.extendEmeMethod(element, element.webkitAddKey, "AddKeyCall");
+ element.webkitCancelKeyRequest = EmeInterception.extendEmeMethod(element, element.webkitCancelKeyRequest, "CancelKeyRequestCall");
+
+ }
+
+ if (this.unprefixedEmeEnabled) {
+ element.setMediaKeys = EmeInterception.extendEmeMethod(element, element.setMediaKeys, "SetMediaKeysCall");
+ }
+
+ element.methodListenersAdded_ = true;
+};
+
+
+/**
+ * Creates a wrapper function that logs calls to the given method.
+ * @param {!Object} element An element or object whose function
+ * call will be logged.
+ * @param {!Function} originalFn The function to log.
+ * @param {!Function} type The constructor for a logger class that will
+ * be instantiated to log the originalFn call.
+ * @return {!Function} The new version, with logging, of orginalFn.
+ */
+EmeInterception.extendEmeMethod = function(element, originalFn, type)
+{
+ return function()
+ {
+ try
+ {
+ var result = originalFn.apply(element, arguments);
+ var args = [].slice.call(arguments);
+ EmeInterception.interceptCall(type, args, result, element);
+ }
+ catch (e)
+ {
+ console.error(e);
+ }
+
+
+ return result;
+ };
+};
+
+
+/**
+ * Intercepts a method call to the console and a separate frame.
+ * @param {!Function} constructor The constructor for a logger class that will
+ * be instantiated to log this call.
+ * @param {Array} args The arguments this call was made with.
+ * @param {Object} result The result of this method call.
+ * @param {!Object} target The element this method was called on.
+ * @return {!eme.EmeMethodCall} The data that has been logged.
+ */
+EmeInterception.interceptCall = function(type, args, result, target)
+{
+ EmeInterception.onOperation(type, args);
+ return args;
+};
+
+/**
+ * Intercepts an event to the console and a separate frame.
+ * @param {!Function} constructor The constructor for a logger class that will
+ * be instantiated to log this event.
+ * @param {!Event} event An EME event.
+ * @return {!eme.EmeEvent} The data that has been logged.
+ */
+EmeInterception.interceptEvent = function(type, event)
+{
+ EmeInterception.onOperation(type, event);
+ return event;
+};
+
+EmeInterception.addRobustnessLevelIfNeeded = function(options)
+{
+ for (var i = 0; i < options.length; i++)
+ {
+ var option = options[i];
+ var videoCapabilities = option["videoCapabilities"];
+ var audioCapabilties = option["audioCapabilities"];
+ if (videoCapabilities != null)
+ {
+ for (var j = 0; j < videoCapabilities.length; j++)
+ if (videoCapabilities[j]["robustness"] == undefined) videoCapabilities[j]["robustness"] = "SW_SECURE_CRYPTO";
+ }
+
+ if (audioCapabilties != null)
+ {
+ for (var j = 0; j < audioCapabilties.length; j++)
+ if (audioCapabilties[j]["robustness"] == undefined) audioCapabilties[j]["robustness"] = "SW_SECURE_CRYPTO";
+ }
+
+ option["videoCapabilities"] = videoCapabilities;
+ option["audioCapabilities"] = audioCapabilties;
+ options[i] = option;
+ }
+
+ return options;
+}
+
+startEMEInterception();