Bug 975144 - Rework RTC identity to use JS sandbox draft
authorMartin Thomson <martin.thomson@gmail.com>
Fri, 09 Jan 2015 15:42:02 -0800
changeset 236140 cab675a7a4c0c5a8ca6238504766fde08f2201f9
parent 236139 c036ac8dd8a0191fa5262e13fcc7dbfc65b2e79d
child 236141 8e8fae8a8c9789735eaf136d526a3ceea970cf46
push id389
push usermartin.thomson@gmail.com
push dateFri, 09 Jan 2015 23:59:51 +0000
bugs975144
milestone37.0a1
Bug 975144 - Rework RTC identity to use JS sandbox
dom/media/IdpProxy.jsm
dom/media/IdpSandbox.jsm
dom/media/PeerConnection.js
dom/media/PeerConnectionIdp.jsm
dom/media/moz.build
deleted file mode 100644
--- a/dom/media/IdpProxy.jsm
+++ /dev/null
@@ -1,271 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["IdpProxy"];
-
-const {
-  classes: Cc,
-  interfaces: Ci,
-  utils: Cu,
-  results: Cr
-} = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Sandbox",
-                                  "resource://gre/modules/identity/Sandbox.jsm");
-
-/**
- * An invisible iframe for hosting the idp shim.
- *
- * There is no visible UX here, as we assume the user has already
- * logged in elsewhere (on a different screen in the web site hosting
- * the RTC functions).
- */
-function IdpChannel(uri, messageCallback) {
-  this.sandbox = null;
-  this.messagechannel = null;
-  this.source = uri;
-  this.messageCallback = messageCallback;
-}
-
-IdpChannel.prototype = {
-  /**
-   * Create a hidden, sandboxed iframe for hosting the IdP's js shim.
-   *
-   * @param callback
-   *                (function) invoked when this completes, with an error
-   *                argument if there is a problem, no argument if everything is
-   *                ok
-   */
-  open: function(callback) {
-    if (this.sandbox) {
-      return callback(new Error("IdP channel already open"));
-    }
-
-    let ready = this._sandboxReady.bind(this, callback);
-    this.sandbox = new Sandbox(this.source, ready);
-  },
-
-  _sandboxReady: function(aCallback, aSandbox) {
-    // Inject a message channel into the subframe.
-    try {
-      this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel();
-      Object.defineProperty(
-        aSandbox._frame.contentWindow.wrappedJSObject,
-        "rtcwebIdentityPort",
-        {
-          value: this.messagechannel.port2
-        }
-      );
-    } catch (e) {
-      this.close();
-      aCallback(e); // oops, the IdP proxy overwrote this.. bad
-      return;
-    }
-    this.messagechannel.port1.onmessage = function(msg) {
-      this.messageCallback(msg.data);
-    }.bind(this);
-    this.messagechannel.port1.start();
-    aCallback();
-  },
-
-  send: function(msg) {
-    this.messagechannel.port1.postMessage(msg);
-  },
-
-  close: function IdpChannel_close() {
-    if (this.sandbox) {
-      if (this.messagechannel) {
-        this.messagechannel.port1.close();
-      }
-      this.sandbox.free();
-    }
-    this.messagechannel = null;
-    this.sandbox = null;
-  }
-};
-
-/**
- * A message channel between the RTC PeerConnection and a designated IdP Proxy.
- *
- * @param domain (string) the domain to load up
- * @param protocol (string) Optional string for the IdP protocol
- */
-function IdpProxy(domain, protocol) {
-  IdpProxy.validateDomain(domain);
-  IdpProxy.validateProtocol(protocol);
-
-  this.domain = domain;
-  this.protocol = protocol || "default";
-
-  this._reset();
-}
-
-/**
- * Checks that the domain is only a domain, and doesn't contain anything else.
- * Adds it to a URI, then checks that it matches perfectly.
- */
-IdpProxy.validateDomain = function(domain) {
-  let message = "Invalid domain for identity provider; ";
-  if (!domain || typeof domain !== "string") {
-    throw new Error(message + "must be a non-zero length string");
-  }
-
-  message += "must only have a domain name and optionally a port";
-  try {
-    let ioService = Components.classes["@mozilla.org/network/io-service;1"]
-                    .getService(Components.interfaces.nsIIOService);
-    let uri = ioService.newURI('https://' + domain + '/', null, null);
-
-    // this should trap errors
-    // we could check uri.userPass, uri.path and uri.ref, but there is no need
-    if (uri.hostPort !== domain) {
-      throw new Error(message);
-    }
-  } catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) {
-    throw new Error(message);
-  }
-};
-
-/**
- * Checks that the IdP protocol is sane.  In particular, we don't want someone
- * adding relative paths (e.g., "../../myuri"), which could be used to move
- * outside of /.well-known/ and into space that they control.
- */
-IdpProxy.validateProtocol = function(protocol) {
-  if (!protocol) {
-    return;  // falsy values turn into "default", so they are OK
-  }
-  let message = "Invalid protocol for identity provider; ";
-  if (typeof protocol !== "string") {
-    throw new Error(message + "must be a string");
-  }
-  if (decodeURIComponent(protocol).match(/[\/\\]/)) {
-    throw new Error(message + "must not include '/' or '\\'");
-  }
-};
-
-IdpProxy.prototype = {
-  _reset: function() {
-    this.channel = null;
-    this.ready = false;
-
-    this.counter = 0;
-    this.tracking = {};
-    this.pending = [];
-  },
-
-  isSame: function(domain, protocol) {
-    return this.domain === domain && ((protocol || "default") === this.protocol);
-  },
-
-  /**
-   * Get a sandboxed iframe for hosting the idp-proxy's js. Create a message
-   * channel down to the frame.
-   *
-   * @param errorCallback (function) a callback that will be invoked if there
-   *                is a fatal error starting the proxy
-   */
-  start: function(errorCallback) {
-    if (this.channel) {
-      return;
-    }
-    let well_known = "https://" + this.domain;
-    well_known += "/.well-known/idp-proxy/" + this.protocol;
-    this.channel = new IdpChannel(well_known, this._messageReceived.bind(this));
-    this.channel.open(function(error) {
-      if (error) {
-        this.close();
-        if (typeof errorCallback === "function") {
-          errorCallback(error);
-        }
-      }
-    }.bind(this));
-  },
-
-  /**
-   * Send a message up to the idp proxy. This should be an RTC "SIGN" or
-   * "VERIFY" message. This method adds the tracking 'id' parameter
-   * automatically to the message so that the callback is only invoked for the
-   * response to the message.
-   *
-   * This enqueues the message to send if the IdP hasn't signaled that it is
-   * "READY", and sends the message when it is.
-   *
-   * The caller is responsible for ensuring that a response is received. If the
-   * IdP doesn't respond, the callback simply isn't invoked.
-   */
-  send: function(message, callback) {
-    this.start();
-    if (this.ready) {
-      message.id = "" + (++this.counter);
-      this.tracking[message.id] = callback;
-      this.channel.send(message);
-    } else {
-      this.pending.push({ message: message, callback: callback });
-    }
-  },
-
-  /**
-   * Handle a message from the IdP. This automatically sends if the message is
-   * 'READY' so there is no need to track readiness state outside of this obj.
-   */
-  _messageReceived: function(message) {
-    if (!message) {
-      return;
-    }
-    if (!this.ready && message.type === "READY") {
-      this.ready = true;
-      this.pending.forEach(function(p) {
-        this.send(p.message, p.callback);
-      }, this);
-      this.pending = [];
-    } else if (this.tracking[message.id]) {
-      var callback = this.tracking[message.id];
-      delete this.tracking[message.id];
-      callback(message);
-    } else {
-      let console = Cc["@mozilla.org/consoleservice;1"].
-        getService(Ci.nsIConsoleService);
-      console.logStringMessage("Received bad message from IdP: " +
-                               message.id + ":" + message.type);
-    }
-  },
-
-  /**
-   * Performs cleanup.  The object should be OK to use again.
-   */
-  close: function() {
-    if (!this.channel) {
-      return;
-    }
-
-    // clear out before letting others know in case they do something bad
-    let trackingCopy = this.tracking;
-    let pendingCopy = this.pending;
-
-    this.channel.close();
-    this._reset();
-
-    // dump a message of type "ERROR" in response to all outstanding
-    // messages to the IdP
-    let error = { type: "ERROR", error: "IdP closed" };
-    Object.keys(trackingCopy).forEach(function(k) {
-      trackingCopy[k](error);
-    });
-    pendingCopy.forEach(function(p) {
-      p.callback(error);
-    });
-  },
-
-  toString: function() {
-    return this.domain + '/.../' + this.protocol;
-  }
-};
-
-this.IdpProxy = IdpProxy;
new file mode 100644
--- /dev/null
+++ b/dom/media/IdpSandbox.jsm
@@ -0,0 +1,236 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const {
+  classes: Cc,
+  interfaces: Ci,
+  utils: Cu,
+  results: Cr
+} = Components;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+/** This little class ensures that redirects maintain an https:// origin */
+function RedirectHttpsOnly() {}
+
+RedirectHttpsOnly.prototype = {
+  asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
+    if (newChannel.URI.scheme !== 'https') {
+      callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+    } else {
+      callback.onRedirectVerifyCallback(Cr.NS_OK);
+    }
+  },
+
+  getInterface: function(iid) {
+    return this.QueryInterface(iid);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink])
+};
+
+/** This class loads a resource into a single string. ResourceLoader.load() is
+ * the entry point. */
+function ResourceLoader(res, rej) {
+  this.resolve = res;
+  this.reject = rej;
+  this.data = '';
+}
+
+/** Loads the identified https:// URL.  */
+ResourceLoader.load = function(uri) {
+  return new Promise((resolve, reject) => {
+    let listener = new ResourceLoader(resolve, reject);
+    let ioService = Cc['@mozilla.org/network/io-service;1']
+      .getService(Ci.nsIIOService);
+    let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+    // the '2' identifies this as a script load
+    let ioChannel = ioService.newChannelFromURI2(uri, null, systemPrincipal,
+                                                 systemPrincipal, 0, 2);
+    ioChannel.notificationCallbacks = new RedirectHttpsOnly();
+    ioChannel.asyncOpen(listener, null);
+  });
+};
+
+ResourceLoader.prototype = {
+  onDataAvailable: function(request, context, input, offset, count) {
+    let stream = Cc['@mozilla.org/scriptableinputstream;1']
+      .createInstance(Ci.nsIScriptableInputStream);
+    stream.init(input);
+    this.data += stream.read(count);
+  },
+
+  onStartRequest: function (request, context) {},
+
+  onStopRequest: function(request, context, status) {
+    if (Components.isSuccessCode(status)) {
+      var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+      if (statusCode === 200) {
+        this.resolve({ request: request, data: this.data });
+      } else {
+        this.reject(new Error('Non-200 response from server: ' + statusCode));
+      }
+    } else {
+      this.reject(new Error('Load failed: ' + status));
+    }
+  },
+
+  getInterface: function(iid) {
+    return this.QueryInterface(iid);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])
+};
+
+/**
+ * Creates an object that is superficially equivalent to window.location
+ * for the sandboxed code to look at.
+ */
+function createLocationFromURI(uri) {
+  return {
+    href: uri.spec,
+    protocol: uri.scheme + ':',
+    host: uri.host + ((uri.port >= 0) ?
+                      (':' + uri.port) : ''),
+    port: uri.port,
+    hostname: uri.host,
+    pathname: uri.path.replace(/[#\?].*/, ''),
+    search: uri.path.replace(/^[^\?]*/, '').replace(/#.*/, ''),
+    hash: uri.hasRef ? ('#' + uri.ref) : '',
+    origin: uri.prePath,
+    toString: function() {
+      return uri.spec;
+    }
+  };
+}
+
+/**
+ * A javascript sandbox for running an IdP.
+ *
+ * @param domain (string) the domain of the IdP
+ * @param protocol (string?) the protocol of the IdP [default: 'default']
+ * @throws if the domain or protocol aren't valid
+ */
+function IdpSandbox(domain, protocol) {
+  this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
+  this.active = null;
+  this.sandbox = null;
+}
+
+IdpSandbox.checkDomain = function(domain) {
+  if (!domain || typeof domain !== 'string') {
+    throw new Error('Invalid domain for identity provider: ' +
+                    'must be a non-zero length string');
+  }
+};
+
+/**
+ * Checks that the IdP protocol is superficially sane.  In particular, we don't
+ * want someone adding relative paths (e.g., '../../myuri'), which could be used
+ * to move outside of /.well-known/ and into space that they control.
+ */
+IdpSandbox.checkProtocol = function(protocol) {
+  let message = 'Invalid protocol for identity provider: ';
+  if (!protocol || typeof protocol !== 'string') {
+    throw new Error(message + 'must be a non-zero length string');
+  }
+  if (decodeURIComponent(protocol).match(/[\/\\]/)) {
+    throw new Error(message + "must not include '/' or '\\'");
+  }
+};
+
+/**
+ * Turns a domain and protocol into a URI.  This does some aggressive checking
+ * to make sure that we aren't being fooled somehow.  Throws on fooling.
+ */
+IdpSandbox.createIdpUri = function(domain, protocol) {
+  IdpSandbox.checkDomain(domain);
+  IdpSandbox.checkProtocol(protocol);
+
+  let message = 'Invalid IdP parameters: ';
+  try {
+    let wkIdp = 'https://' + domain + '/.well-known/idp-proxy/' + protocol;
+    let ioService = Components.classes['@mozilla.org/network/io-service;1']
+                    .getService(Ci.nsIIOService);
+    let uri = ioService.newURI(wkIdp, null, null);
+
+    if (uri.hostPort !== domain) {
+      throw new Error(message + 'domain is invalid');
+    }
+    if (uri.path.indexOf('/.well-known/idp-proxy/') !== 0) {
+      throw new Error(message + 'must produce a /.well-known/idp-proxy/ URI');
+    }
+
+    return uri;
+  } catch (e if (typeof e.result !== 'undefined' &&
+                 e.result === Cr.NS_ERROR_MALFORMED_URI)) {
+    throw new Error(message + 'must produce a valid URI');
+  }
+};
+
+IdpSandbox.prototype = {
+  isSame: function(domain, protocol) {
+    return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec;
+  },
+
+  start: function() {
+    if (!this.active) {
+      this.active = ResourceLoader.load(this.source)
+        .then(result => this._createSandbox(result));
+    }
+    return this.active;
+  },
+
+  // Provides the sandbox with some useful facilities.  Initially, this is only
+  // a minimal set; it is far easier to add more as the need arises, than to
+  // take them back if we discover a mistake.
+  _populateSandbox: function() {
+    this.sandbox.location = Cu.cloneInto(createLocationFromURI(this.source),
+                                         this.sandbox,
+                                         { cloneFunctions: true });
+  },
+
+  _createSandbox: function(result) {
+    let principal = Services.scriptSecurityManager
+      .getChannelResultPrincipal(result.request);
+
+    this.sandbox = Cu.Sandbox(principal, {
+      sandboxName: 'IdP-' + this.source.host,
+      wantComponents: false,
+      wantExportHelpers: false,
+      wantGlobalProperties: [
+        'indexedDB', 'XMLHttpRequest', 'TextEncoder', 'TextDecoder',
+        'URL', 'URLSearchParams', 'atob', 'btoa', 'Blob', 'crypto',
+        'rtcIdentityProvider'
+      ]
+    });
+    this._populateSandbox();
+
+    let registrar = this.sandbox.rtcIdentityProvider;
+    // putting a javascript version of 1.8 here seems fragile
+    Cu.evalInSandbox(result.data, this.sandbox,
+                     '1.8', result.request.URI.spec, 1);
+
+    if (!registrar.idp) {
+      throw new Error('IdP failed to call rtcIdentityProvider.register()');
+    }
+    return registrar;
+  },
+
+  stop: function() {
+    if (this.sandbox) {
+      Cu.nukeSandbox(this.sandbox);
+    }
+    this.sandbox = null;
+    this.active = null;
+  },
+
+  toString: function() {
+    return this.source.spec;
+  }
+};
+
+this.EXPORTED_SYMBOLS = ['IdpSandbox'];
+this.IdpSandbox = IdpSandbox;
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -371,21 +371,23 @@ RTCPeerConnection.prototype = {
           "RTCPeerConnection is gone (did you enter Offline mode?)");
     }
     return this._pc;
   },
 
   _initIdp: function() {
     let prefName = "media.peerconnection.identity.timeout";
     let idpTimeout = Services.prefs.getIntPref(prefName);
-    let warningFunc = this.logWarning.bind(this);
-    this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
-                                           this.dispatchEvent.bind(this));
-    this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
-                                            this.dispatchEvent.bind(this));
+    let warn = this.logWarning.bind(this);
+    let idpErrorReport = (type, args) => {
+      this.dispatchEvent(
+        new this._win.RTCPeerConnectionIdentityErrorEvent(type, args));
+    };
+    this._localIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
+    this._remoteIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
   },
 
   /**
    * Add a function to the task chain.
    *   onSuccess - legacy callback (optional)
    *   onError   - legacy callback (optional)
    */
   _queue: function(func, onSuccess, onError) {
@@ -734,51 +736,43 @@ RTCPeerConnection.prototype = {
           this.close();
         } else {
           idpComplete = true;
           allDone();
         }
       };
     }
 
-    try {
-      this._remoteIdp.verifyIdentityFromSDP(sdp, origin, idpDone);
-    } catch (e) {
-      // if processing the SDP for identity doesn't work
-      this.logWarning(e.message, e.fileName, e.lineNumber);
-      idpDone(null);
-    }
+    this._remoteIdp.verifyIdentityFromSDP(sdp, origin)
+      .then(idpDone);
 
     this._onSetRemoteDescriptionSuccess = setRemoteDone;
     this._onSetRemoteDescriptionFailure = onError;
     this._impl.setRemoteDescription(type, sdp);
   },
 
   setIdentityProvider: function(provider, protocol, username) {
     this._checkClosed();
     this._localIdp.setIdentityProvider(provider, protocol, username);
   },
 
-  _gotIdentityAssertion: function(assertion){
+  _gotIdentityAssertion: function(assertion) {
+    if (!assertion) {
+      return;
+    }
     let args = { assertion: assertion };
     let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult", args);
     this.dispatchEvent(ev);
   },
 
   getIdentityAssertion: function() {
     this._checkClosed();
 
-    var gotAssertion = assertion => {
-      if (assertion) {
-        this._gotIdentityAssertion(assertion);
-      }
-    };
-
-    this._localIdp.getIdentityAssertion(this._impl.fingerprint,
-                                        gotAssertion);
+    this._localIdp.getIdentityAssertion(this._impl.fingerprint)
+      .then(assertion => this._gotIdentityAssertion(assertion));
   },
 
   updateIce: function(config) {
     throw new this._win.DOMError("NotSupportedError", "updateIce not yet implemented");
   },
 
   addIceCandidate: function(cand, onSuccess, onError) {
     if (!cand.candidate && !cand.sdpMLineIndex) {
@@ -906,17 +900,17 @@ RTCPeerConnection.prototype = {
 
   get localDescription() {
     this._checkClosed();
     let sdp = this._impl.localDescription;
     if (sdp.length == 0) {
       return null;
     }
 
-    sdp = this._localIdp.wrapSdp(sdp);
+    sdp = this._localIdp.addIdentityAttribute(sdp);
     return new this._win.mozRTCSessionDescription({ type: this._localType,
                                                     sdp: sdp });
   },
 
   get remoteDescription() {
     this._checkClosed();
     let sdp = this._impl.remoteDescription;
     if (sdp.length == 0) {
@@ -1062,42 +1056,52 @@ PeerConnectionObserver.prototype = {
   },
 
   dispatchEvent: function(event) {
     this._dompc.dispatchEvent(event);
   },
 
   onCreateOfferSuccess: function(sdp) {
     let pc = this._dompc;
-    let fp = pc._impl.fingerprint;
-    let origin = Cu.getWebIDLCallerPrincipal().origin;
-    pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
-      if (assertion) {
-        pc._gotIdentityAssertion(assertion);
-      }
+    let idp = pc._localIdp;
+
+    if (idp.enabled) {
+      idp.getIdentityAssertion(pc._impl.fingerprint)
+        .then(assertion => {
+          pc._gotIdentityAssertion(assertion);
+          sdp = idp.addIdentityAttribute(sdp);
+          pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
+                                                                          sdp: sdp }));
+        }, e => {}); // errors are handled in the IdP
+    } else {
       pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
                                                                       sdp: sdp }));
-    }.bind(this));
+    }
   },
 
   onCreateOfferError: function(code, message) {
     this._dompc._onCreateOfferFailure(this.newError(code, message));
   },
 
   onCreateAnswerSuccess: function(sdp) {
     let pc = this._dompc;
-    let fp = pc._impl.fingerprint;
-    let origin = Cu.getWebIDLCallerPrincipal().origin;
-    pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
-      if (assertion) {
-        pc._gotIdentityAssertion(assertion);
-      }
+    let idp = pc._localIdp;
+
+    if (idp.enabled) {
+      idp.getIdentityAssertion(pc._impl.fingerprint)
+        .then(assertion => {
+          pc._gotIdentityAssertion(assertion);
+          sdp = idp.addIdentityAttribute(sdp);
+          pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
+                                                                           sdp: sdp }));
+        }, e => {});
+    } else {
       pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
                                                                        sdp: sdp }));
-    }.bind(this));
+    }
   },
 
   onCreateAnswerError: function(code, message) {
     this._dompc._onCreateAnswerFailure(this.newError(code, message));
   },
 
   onSetLocalDescriptionSuccess: function() {
     this._dompc._onSetLocalDescriptionSuccess();
--- a/dom/media/PeerConnectionIdp.jsm
+++ b/dom/media/PeerConnectionIdp.jsm
@@ -1,363 +1,351 @@
 /* jshint moz:true, browser:true */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
+this.EXPORTED_SYMBOLS = ['PeerConnectionIdp'];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy",
-  "resource://gre/modules/media/IdpProxy.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'IdpSandbox',
+  'resource://gre/modules/media/IdpSandbox.jsm');
+
+function TimerResolver(resolve) {
+  this.notify = resolve;
+}
+TimerResolver.prototype = {
+  getInterface: function(iid) {
+    return this.QueryInterface(iid);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+}
+function delay(t) {
+  return new Promise(resolve => {
+    let timer = Cc['@mozilla.org/timer;1'].getService(Ci.nsITimer);
+    timer.initWithCallback(new TimerResolver(resolve), t, 0); // One shot
+  });
+}
 
 /**
  * Creates an IdP helper.
  *
- * @param window (object) the window object to use for miscellaneous goodies
  * @param timeout (int) the timeout in milliseconds
  * @param warningFunc (function) somewhere to dump warning messages
- * @param dispatchEventFunc (function) somewhere to dump error events
+ * @param dispatchErrorFunc (function(string, dict)) somewhere to dump errors
  */
-function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
-  this._win = window;
+function PeerConnectionIdp(timeout, warningFunc, dispatchErrorFunc) {
   this._timeout = timeout || 5000;
   this._warning = warningFunc;
-  this._dispatchEvent = dispatchEventFunc;
+  this._dispatchError = dispatchErrorFunc;
 
   this.assertion = null;
   this.provider = null;
 }
 
 (function() {
-  PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
+  PeerConnectionIdp._mLinePattern = new RegExp('^m=', 'm');
   // attributes are funny, the 'a' is case sensitive, the name isn't
-  let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
-  PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
-  pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
-  PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
+  let pattern = '^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)';
+  PeerConnectionIdp._identityPattern = new RegExp(pattern, 'm');
+  pattern = '^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)';
+  PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, 'm');
 })();
 
 PeerConnectionIdp.prototype = {
+  get enabled() {
+    return !!this._idp;
+  },
+
   setIdentityProvider: function(provider, protocol, username) {
     this.provider = provider;
-    this.protocol = protocol;
+    this.protocol = protocol || 'default';
     this.username = username;
-    if (this._idpchannel) {
-      if (this._idpchannel.isSame(provider, protocol)) {
-        return;
+    if (this._idp) {
+      if (this._idp.isSame(provider, protocol)) {
+        return; // noop
       }
-      this._idpchannel.close();
+      this._idp.stop();
     }
-    this._idpchannel = new IdpProxy(provider, protocol);
+    this._idp = new IdpSandbox(provider, protocol);
   },
 
   close: function() {
     this.assertion = null;
     this.provider = null;
-    if (this._idpchannel) {
-      this._idpchannel.close();
-      this._idpchannel = null;
+    this.protocol = null;
+    if (this._idp) {
+      this._idp.stop();
+      this._idp = null;
     }
   },
 
   /**
    * Generate an error event of the identified type;
    * and put a little more precise information in the console.
+   *
+   * A little note on error handling in this class: this class reports errors
+   * exclusively through the event handlers that are passed to it
+   * (this._dispatchError, specifically).  That means that all the functions
+   * return resolved promises; promises are never rejected.  This probably isn't
+   * the best design, but the refactor can wait.
    */
   reportError: function(type, message, extra) {
     let args = {
       idp: this.provider,
       protocol: this.protocol
     };
     if (extra) {
       Object.keys(extra).forEach(function(k) {
         args[k] = extra[k];
       });
     }
-    this._warning("RTC identity: " + message, null, 0);
-    let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
-    this._dispatchEvent(ev);
+    this._warning('RTC identity: ' + message, null, 0);
+    this._dispatchError('idp' + type + 'error', args);
   },
 
   _getFingerprintsFromSdp: function(sdp) {
     let fingerprints = {};
     let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
     while (m) {
       fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
       sdp = sdp.substring(m.index + m[0].length);
       m = sdp.match(PeerConnectionIdp._fingerprintPattern);
     }
 
     return Object.keys(fingerprints).map(k => fingerprints[k]);
   },
 
+  _isValidAssertion: function(assertion) {
+    return assertion && assertion.idp &&
+      typeof assertion.idp.domain === 'string' &&
+      (!assertion.idp.protocol ||
+       typeof assertion.idp.protocol === 'string') &&
+      typeof assertion.assertion === 'string';
+  },
+
   _getIdentityFromSdp: function(sdp) {
     // a=identity is session level
     let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
     let sessionLevel = sdp.substring(0, mLineMatch.index);
     let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
-    if (idMatch) {
-      let assertion = {};
-      try {
-        assertion = JSON.parse(atob(idMatch[1]));
-      } catch (e) {
-        this.reportError("validation",
-                         "invalid identity assertion: " + e);
-      } // for JSON.parse
-      if (typeof assertion.idp === "object" &&
-          typeof assertion.idp.domain === "string" &&
-          typeof assertion.assertion === "string") {
-        return assertion;
-      }
+    if (!idMatch) {
+      return; // undefined === error
+    }
 
-      this.reportError("validation", "assertion missing" +
-                       " idp/idp.domain/assertion");
+    let assertion;
+    try {
+      assertion = JSON.parse(atob(idMatch[1]));
+    } catch (e) {
+      this.reportError('validation',
+                       'invalid identity assertion: ' + e);
     }
-    // undefined!
+    if (!this._isValidAssertion(assertion)) {
+      this.reportError('validation', 'assertion missing' +
+                       ' idp/idp.domain/assertion');
+    }
+    return assertion;
   },
 
   /**
-   * Queues a task to verify the a=identity line the given SDP contains, if any.
+   * Verifies the a=identity line the given SDP contains, if any.
    * If the verification succeeds callback is called with the message from the
    * IdP proxy as parameter, else (verification failed OR no a=identity line in
    * SDP at all) null is passed to callback.
+   *
+   * Note that this only verifies that the SDP is coherent.  This relies on the
+   * invariant that the RTCPeerConnection won't connect to a peer if the
+   * fingerprint of the certificate they offer doesn't appear in the SDP.
    */
-  verifyIdentityFromSDP: function(sdp, origin, callback) {
+  verifyIdentityFromSDP: function(sdp, origin) {
     let identity = this._getIdentityFromSdp(sdp);
     let fingerprints = this._getFingerprintsFromSdp(sdp);
-    // it's safe to use the fingerprint we got from the SDP here,
-    // only because we ensure that there is only one
     if (!identity || fingerprints.length <= 0) {
-      callback(null);
-      return;
+      return Promise.resolve();
     }
 
     this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
-    this._verifyIdentity(identity.assertion, fingerprints, origin, callback);
+    return this._verifyIdentity(identity.assertion, fingerprints, origin);
   },
 
   /**
    * Checks that the name in the identity provided by the IdP is OK.
    *
+   * @param error (function) an error function to call
    * @param name (string) the name to validate
-   * @returns (string) an error message, iff the name isn't good
+   * @throws if the name isn't valid
    */
-  _validateName: function(name) {
-    if (typeof name !== "string") {
-      return "name not a string";
+  _validateName: function(error, name) {
+    if (typeof name !== 'string') {
+      return error('name not a string');
     }
-    let atIdx = name.indexOf("@");
-    if (atIdx > 0) {
-      // no third party assertions... for now
-      let tail = name.substring(atIdx + 1);
+    let atIdx = name.indexOf('@');
+    if (atIdx <= 0) {
+      return error('missing authority in name from IdP');
+    }
 
-      // strip the port number, if present
-      let provider = this.provider;
-      let providerPortIdx = provider.indexOf(":");
-      if (providerPortIdx > 0) {
-        provider = provider.substring(0, providerPortIdx);
-      }
-      let idnService = Components.classes["@mozilla.org/network/idn-service;1"].
-        getService(Components.interfaces.nsIIDNService);
-      if (idnService.convertUTF8toACE(tail) !==
-          idnService.convertUTF8toACE(provider)) {
-        return "name '" + identity.name +
-            "' doesn't match IdP: '" + this.provider + "'";
-      }
-      return null;
+    // no third party assertions... for now
+    let tail = name.substring(atIdx + 1);
+
+    // strip the port number, if present
+    let provider = this.provider;
+    let providerPortIdx = provider.indexOf(':');
+    if (providerPortIdx > 0) {
+      provider = provider.substring(0, providerPortIdx);
     }
-    return "missing authority in name from IdP";
+    let idnService = Components.classes['@mozilla.org/network/idn-service;1'].
+      getService(Components.interfaces.nsIIDNService);
+    if (idnService.convertUTF8toACE(tail) !==
+        idnService.convertUTF8toACE(provider)) {
+      return error('name "' + identity.name +
+            '" doesn\'t match IdP: "' + this.provider + '"');
+    }
+    return true;
   },
 
-  // we are very defensive here when handling the message from the IdP
-  // proxy so that broken IdPs can only do as little harm as possible.
-  _checkVerifyResponse: function(message, fingerprints) {
-    let warn = msg => {
-      this.reportError("validation",
-                       "assertion validation failure: " + msg);
+  /**
+   * Check the validation response.  We are very defensive here when handling
+   * the message from the IdP proxy.  That way, broken IdPs aren't likely to
+   * cause catastrophic damage.
+   */
+  _isValidVerificationResponse: function(validation, sdpFingerprints) {
+    let error = msg => {
+      this.reportError('validation', 'assertion validation failure: ' + msg);
+      return false;
     };
 
-    let isSubsetOf = (outer, inner, cmp) => {
-      return inner.some(i => {
-        return !outer.some(o => cmp(i, o));
+    if (typeof validation !== 'object' ||
+        typeof validation.contents !== 'string' ||
+        typeof validation.identity !== 'string') {
+      return error('no payload in validation response');
+    }
+
+    let fingerprints;
+    try {
+      fingerprints = JSON.parse(validation.contents).fingerprint;
+    } catch (e) {
+      return error('idp returned invalid JSON');
+    }
+
+    let isFingerprint = f =>
+        (typeof f.digest === 'string') &&
+        (typeof f.algorithm === 'string');
+    if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
+      return error('fingerprints must be an array of objects' +
+                   ' with digest and algorithm attributes');
+    }
+
+    let isSubsetOf = (outerSet, innerSet, comparator) => {
+      return innerSet.every(i => {
+        return outerSet.some(o => comparator(i, o));
       });
     };
     let compareFingerprints = (a, b) => {
       return (a.digest === b.digest) && (a.algorithm === b.algorithm);
     };
-
-    try {
-      let contents = JSON.parse(message.contents);
-      if (!Array.isArray(contents.fingerprint)) {
-        warn("fingerprint is not an array");
-      } else if (isSubsetOf(contents.fingerprint, fingerprints,
-                            compareFingerprints)) {
-        warn("fingerprints in SDP aren't a subset of those in the assertion");
-      } else {
-        let error = this._validateName(message.identity);
-        if (error) {
-          warn(error);
-        } else {
-          return true;
-        }
-      }
-    } catch(e) {
-      warn("invalid JSON in content");
+    if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
+      return error('the fingerprints in SDP aren\'t covered by the assertion');
     }
-    return false;
+    return this._validateName(error, validation.identity);
   },
 
   /**
-   * Asks the IdP proxy to verify an identity.
+   * Asks the IdP proxy to verify an identity assertion.
    */
-  _verifyIdentity: function(assertion, fingerprints, origin, callback) {
-    function onVerification(message) {
-      if (message && this._checkVerifyResponse(message, fingerprints)) {
-        callback(message);
-      } else {
-        this._warning("RTC identity: assertion validation failure", null, 0);
-        callback(null);
-      }
-    }
+  _verifyIdentity: function(assertion, fingerprints, origin) {
+    let validationPromise = this._idp.start()
+        .then(idp => idp.validateAssertion(assertion, origin));
 
-    let request = {
-      type: "VERIFY",
-      message: assertion,
-      origin: origin
-    };
-    this._sendToIdp(request, "validation", onVerification.bind(this));
+    return this._safetyNet('validation', validationPromise)
+      .then(validation => {
+        if (validation &&
+            this._isValidVerificationResponse(validation, fingerprints)) {
+          return validation;
+        }
+      });
   },
 
   /**
-   * Asks the IdP proxy for an identity assertion and, on success, enriches the
-   * given SDP with an a=identity line and calls callback with the new SDP as
-   * parameter. If no IdP is configured the original SDP (without a=identity
-   * line) is passed to the callback.
+   * Enriches the given SDP with an `a=identity` line.  getIdentityAssertion()
+   * must have already run successfully, otherwise this does nothing to the sdp.
    */
-  appendIdentityToSDP: function(sdp, fingerprint, origin, callback) {
-    let onAssertion = function() {
-      callback(this.wrapSdp(sdp), this.assertion);
-    }.bind(this);
-
-    if (!this._idpchannel || this.assertion) {
-      onAssertion();
-      return;
-    }
-
-    this._getIdentityAssertion(fingerprint, origin, onAssertion);
-  },
-
-  /**
-   * Inserts an identity assertion into the given SDP.
-   */
-  wrapSdp: function(sdp) {
+  addIdentityAttribute: function(sdp) {
     if (!this.assertion) {
       return sdp;
     }
 
     // yes, we assume that this matches; if it doesn't something is *wrong*
     let match = sdp.match(PeerConnectionIdp._mLinePattern);
     return sdp.substring(0, match.index) +
-      "a=identity:" + this.assertion + "\r\n" +
+      'a=identity:' + this.assertion + '\r\n' +
       sdp.substring(match.index);
   },
 
-  getIdentityAssertion: function(fingerprint, callback) {
-    if (!this._idpchannel) {
-      this.reportError("assertion", "IdP not set");
-      callback(null);
-      return;
+  /**
+   * Asks the IdP proxy for an identity assertion.  Don't call this unless you
+   * have checked .enabled, or you really like exceptions.
+   */
+  getIdentityAssertion: function(fingerprint) {
+    if (!this.enabled) {
+      this.reportError('assertion', 'no IdP set,' +
+                       ' call setIdentityProvider() to set one');
+      return Promise.resolve();
     }
 
-    let origin = Cu.getWebIDLCallerPrincipal().origin;
-    this._getIdentityAssertion(fingerprint, origin, callback);
-  },
-
-  _getIdentityAssertion: function(fingerprint, origin, callback) {
-    let [algorithm, digest] = fingerprint.split(" ", 2);
-    let message = {
+    let [algorithm, digest] = fingerprint.split(' ', 2);
+    let content = {
       fingerprint: [{
         algorithm: algorithm,
         digest: digest
       }]
     };
-    let request = {
-      type: "SIGN",
-      message: JSON.stringify(message),
-      username: this.username,
-      origin: origin
-    };
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+
+    let assertionPromise = this._idp.start()
+        .then(idp => idp.generateAssertion(JSON.stringify(content),
+                                           origin, this.username));
 
-    // catch the assertion, clean it up, warn if absent
-    function trapAssertion(assertion) {
-      if (!assertion) {
-        this._warning("RTC identity: assertion generation failure", null, 0);
-        this.assertion = null;
-      } else {
-        this.assertion = btoa(JSON.stringify(assertion));
-      }
-      callback(this.assertion);
-    }
-
-    this._sendToIdp(request, "assertion", trapAssertion.bind(this));
+    return this._safetyNet('assertion', assertionPromise)
+      .then(assertion => {
+        if (this._isValidAssertion(assertion)) {
+          // save the base64+JSON assertion, since that is all that is used
+          this.assertion = btoa(JSON.stringify(assertion));
+        } else {
+          if (assertion) {
+            // only report an error for an invalid assertion
+            // other paths generate more specific error reports
+            this.reportError('assertion', 'invalid assertion generated');
+          }
+          this.assertion = null;
+        }
+        return this.assertion;
+      });
   },
 
   /**
-   * Packages a message and sends it to the IdP.
-   * @param request (dictionary) the message to send
-   * @param type (DOMString) the type of message (assertion/validation)
-   * @param callback (function) the function to call with the results
+   * Wraps a promise, adding a timeout guard on it so that it can't take longer
+   * than the specified time.  Returns a promise that always resolves; if there
+   * is a problem the resolved value is undefined.
    */
-  _sendToIdp: function(request, type, callback) {
-    this._idpchannel.send(request, this._wrapCallback(type, callback));
-  },
-
-  _reportIdpError: function(type, message) {
-    let args = {};
-    let msg = "";
-    if (message.type === "ERROR") {
-      msg = message.error;
-    } else {
-      msg = JSON.stringify(message.message);
-      if (message.type === "LOGINNEEDED") {
-        args.loginUrl = message.loginUrl;
-      }
-    }
-    this.reportError(type, "received response of type '" +
-                     message.type + "' from IdP: " + msg, args);
-  },
-
-  /**
-   * Wraps a callback, adding a timeout and ensuring that the callback doesn't
-   * receive any message other than one where the IdP generated a "SUCCESS"
-   * response.
-   */
-  _wrapCallback: function(type, callback) {
-    let timeout = this._win.setTimeout(function() {
-      this.reportError(type, "IdP timeout for " + this._idpchannel + " " +
-                       (this._idpchannel.ready ? "[ready]" : "[not ready]"));
-      timeout = null;
-      callback(null);
-    }.bind(this), this._timeout);
-
-    return function(message) {
-      if (!timeout) {
-        return;
-      }
-      this._win.clearTimeout(timeout);
-      timeout = null;
-
-      let content = null;
-      if (message.type === "SUCCESS") {
-        content = message.message;
-      } else {
-        this._reportIdpError(type, message);
-      }
-      callback(content);
-    }.bind(this);
+  _safetyNet: function(type, p) {
+    let done = false; // ... all because Promises don't expose state
+    let timeoutPromise = delay(this._timeout)
+        .then(() => {
+          if (!done) {
+            this.reportError(type, 'IdP timed out');
+          }
+        });
+    let realPromise = p
+        .catch(e => this.reportError(type, 'error reported by IdP: ' + e.message))
+        .then(result => {
+          done = true;
+          return result;
+        });
+    // If timeoutPromise completes first, the returned value will be undefined,
+    // just like when there is an error.
+    return Promise.race([realPromise, timeoutPromise]);
   }
 };
 
 this.PeerConnectionIdp = PeerConnectionIdp;
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -233,17 +233,17 @@ if CONFIG['GNU_CC'] or CONFIG['CLANG_CL'
   SOURCES['DecoderTraits.cpp'].flags += ['-Wno-error=multichar']
 
 EXTRA_COMPONENTS += [
     'PeerConnection.js',
     'PeerConnection.manifest',
 ]
 
 EXTRA_JS_MODULES.media += [
-    'IdpProxy.jsm',
+    'IdpSandbox.jsm',
     'PeerConnectionIdp.jsm',
     'RTCStatsReport.jsm',
 ]
 
 FAIL_ON_WARNINGS = True
 
 MSVC_ENABLE_PGO = True