mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			378 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			378 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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/. */
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  IdpSandbox: "resource://gre/modules/media/IdpSandbox.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
/**
 | 
						|
 * Creates an IdP helper.
 | 
						|
 *
 | 
						|
 * @param win (object) the window we are working for
 | 
						|
 * @param timeout (int) the timeout in milliseconds
 | 
						|
 */
 | 
						|
export function PeerConnectionIdp(win, timeout) {
 | 
						|
  this._win = win;
 | 
						|
  this._timeout = timeout || 5000;
 | 
						|
 | 
						|
  this.provider = null;
 | 
						|
  this._resetAssertion();
 | 
						|
}
 | 
						|
 | 
						|
(function () {
 | 
						|
  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");
 | 
						|
})();
 | 
						|
 | 
						|
PeerConnectionIdp.prototype = {
 | 
						|
  get enabled() {
 | 
						|
    return !!this._idp;
 | 
						|
  },
 | 
						|
 | 
						|
  _resetAssertion() {
 | 
						|
    this.assertion = null;
 | 
						|
    this.idpLoginUrl = null;
 | 
						|
  },
 | 
						|
 | 
						|
  setIdentityProvider(provider, protocol, usernameHint, peerIdentity) {
 | 
						|
    this._resetAssertion();
 | 
						|
    this.provider = provider;
 | 
						|
    this.protocol = protocol;
 | 
						|
    this.username = usernameHint;
 | 
						|
    this.peeridentity = peerIdentity;
 | 
						|
    if (this._idp) {
 | 
						|
      if (this._idp.isSame(provider, protocol)) {
 | 
						|
        return; // noop
 | 
						|
      }
 | 
						|
      this._idp.stop();
 | 
						|
    }
 | 
						|
    this._idp = new lazy.IdpSandbox(provider, protocol, this._win);
 | 
						|
  },
 | 
						|
 | 
						|
  // start the IdP and do some error fixup
 | 
						|
  start() {
 | 
						|
    return this._idp.start().catch(e => {
 | 
						|
      throw new this._win.DOMException(e.message, "IdpError");
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  close() {
 | 
						|
    this._resetAssertion();
 | 
						|
    this.provider = null;
 | 
						|
    this.protocol = null;
 | 
						|
    this.username = null;
 | 
						|
    this.peeridentity = null;
 | 
						|
    if (this._idp) {
 | 
						|
      this._idp.stop();
 | 
						|
      this._idp = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _getFingerprintsFromSdp(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(assertion) {
 | 
						|
    return (
 | 
						|
      assertion &&
 | 
						|
      assertion.idp &&
 | 
						|
      typeof assertion.idp.domain === "string" &&
 | 
						|
      (!assertion.idp.protocol || typeof assertion.idp.protocol === "string") &&
 | 
						|
      typeof assertion.assertion === "string"
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  _getSessionLevelEnd(sdp) {
 | 
						|
    const match = sdp.match(PeerConnectionIdp._mLinePattern);
 | 
						|
    if (!match) {
 | 
						|
      return sdp.length;
 | 
						|
    }
 | 
						|
    return match.index;
 | 
						|
  },
 | 
						|
 | 
						|
  _getIdentityFromSdp(sdp) {
 | 
						|
    // a=identity is session level
 | 
						|
    let idMatch;
 | 
						|
    const index = this._getSessionLevelEnd(sdp);
 | 
						|
    const sessionLevel = sdp.substring(0, index);
 | 
						|
    idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
 | 
						|
    if (!idMatch) {
 | 
						|
      return undefined; // undefined === no identity
 | 
						|
    }
 | 
						|
 | 
						|
    let assertion;
 | 
						|
    try {
 | 
						|
      assertion = JSON.parse(atob(idMatch[1]));
 | 
						|
    } catch (e) {
 | 
						|
      throw new this._win.DOMException(
 | 
						|
        "invalid identity assertion: " + e,
 | 
						|
        "InvalidSessionDescriptionError"
 | 
						|
      );
 | 
						|
    }
 | 
						|
    if (!this._isValidAssertion(assertion)) {
 | 
						|
      throw new this._win.DOMException(
 | 
						|
        "assertion missing idp/idp.domain/assertion",
 | 
						|
        "InvalidSessionDescriptionError"
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return assertion;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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.  We still rely on
 | 
						|
   * the fact that the RTCPeerConnection won't connect to a peer if the
 | 
						|
   * fingerprint of the certificate they offer doesn't appear in the SDP.
 | 
						|
   */
 | 
						|
  verifyIdentityFromSDP(sdp, origin) {
 | 
						|
    let identity = this._getIdentityFromSdp(sdp);
 | 
						|
    let fingerprints = this._getFingerprintsFromSdp(sdp);
 | 
						|
    if (!identity || fingerprints.length <= 0) {
 | 
						|
      return this._win.Promise.resolve(); // undefined result = no identity
 | 
						|
    }
 | 
						|
 | 
						|
    this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
 | 
						|
    return this._verifyIdentity(identity.assertion, fingerprints, origin);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Checks that the name in the identity provided by the IdP is OK.
 | 
						|
   *
 | 
						|
   * @param name (string) the name to validate
 | 
						|
   * @throws if the name isn't valid
 | 
						|
   */
 | 
						|
  _validateName(name) {
 | 
						|
    let error = msg => {
 | 
						|
      throw new this._win.DOMException(
 | 
						|
        "assertion name error: " + msg,
 | 
						|
        "IdpError"
 | 
						|
      );
 | 
						|
    };
 | 
						|
 | 
						|
    if (typeof name !== "string") {
 | 
						|
      error("name not a string");
 | 
						|
    }
 | 
						|
    let atIdx = name.indexOf("@");
 | 
						|
    if (atIdx <= 0) {
 | 
						|
      error("missing authority in name from IdP");
 | 
						|
    }
 | 
						|
 | 
						|
    // 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);
 | 
						|
    }
 | 
						|
    let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
 | 
						|
      Ci.nsIIDNService
 | 
						|
    );
 | 
						|
    if (
 | 
						|
      idnService.convertUTF8toACE(tail) !==
 | 
						|
      idnService.convertUTF8toACE(provider)
 | 
						|
    ) {
 | 
						|
      error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"');
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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.
 | 
						|
   */
 | 
						|
  _checkValidation(validation, sdpFingerprints) {
 | 
						|
    let error = msg => {
 | 
						|
      throw new this._win.DOMException(
 | 
						|
        "IdP validation error: " + msg,
 | 
						|
        "IdpError"
 | 
						|
      );
 | 
						|
    };
 | 
						|
 | 
						|
    if (!this.provider) {
 | 
						|
      error("IdP closed");
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      typeof validation !== "object" ||
 | 
						|
      typeof validation.contents !== "string" ||
 | 
						|
      typeof validation.identity !== "string"
 | 
						|
    ) {
 | 
						|
      error("no payload in validation response");
 | 
						|
    }
 | 
						|
 | 
						|
    let fingerprints;
 | 
						|
    try {
 | 
						|
      fingerprints = JSON.parse(validation.contents).fingerprint;
 | 
						|
    } catch (e) {
 | 
						|
      error("invalid JSON");
 | 
						|
    }
 | 
						|
 | 
						|
    let isFingerprint = f =>
 | 
						|
      typeof f.digest === "string" && typeof f.algorithm === "string";
 | 
						|
    if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
 | 
						|
      error(
 | 
						|
        "fingerprints must be an array of objects" +
 | 
						|
          " with digest and algorithm attributes"
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    // everything in `innerSet` is found in `outerSet`
 | 
						|
    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;
 | 
						|
    };
 | 
						|
    if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
 | 
						|
      error("the fingerprints must be covered by the assertion");
 | 
						|
    }
 | 
						|
    this._validateName(validation.identity);
 | 
						|
    return validation;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Asks the IdP proxy to verify an identity assertion.
 | 
						|
   */
 | 
						|
  _verifyIdentity(assertion, fingerprints, origin) {
 | 
						|
    let p = this.start()
 | 
						|
      .then(idp =>
 | 
						|
        this._wrapCrossCompartmentPromise(
 | 
						|
          idp.validateAssertion(assertion, origin)
 | 
						|
        )
 | 
						|
      )
 | 
						|
      .then(validation => this._checkValidation(validation, fingerprints));
 | 
						|
 | 
						|
    return this._applyTimeout(p);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Enriches the given SDP with an `a=identity` line.  getIdentityAssertion()
 | 
						|
   * must have already run successfully, otherwise this does nothing to the sdp.
 | 
						|
   */
 | 
						|
  addIdentityAttribute(sdp) {
 | 
						|
    if (!this.assertion) {
 | 
						|
      return sdp;
 | 
						|
    }
 | 
						|
 | 
						|
    const index = this._getSessionLevelEnd(sdp);
 | 
						|
    return (
 | 
						|
      sdp.substring(0, index) +
 | 
						|
      "a=identity:" +
 | 
						|
      this.assertion +
 | 
						|
      "\r\n" +
 | 
						|
      sdp.substring(index)
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Asks the IdP proxy for an identity assertion.  Don't call this unless you
 | 
						|
   * have checked .enabled, or you really like exceptions.  Also, don't call
 | 
						|
   * this when another call is still running, because it's not certain which
 | 
						|
   * call will finish first and the final state will be similarly uncertain.
 | 
						|
   */
 | 
						|
  getIdentityAssertion(fingerprint, origin) {
 | 
						|
    if (!this.enabled) {
 | 
						|
      throw new this._win.DOMException(
 | 
						|
        "no IdP set, call setIdentityProvider() to set one",
 | 
						|
        "InvalidStateError"
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    let [algorithm, digest] = fingerprint.split(" ", 2);
 | 
						|
    let content = {
 | 
						|
      fingerprint: [
 | 
						|
        {
 | 
						|
          algorithm,
 | 
						|
          digest,
 | 
						|
        },
 | 
						|
      ],
 | 
						|
    };
 | 
						|
 | 
						|
    this._resetAssertion();
 | 
						|
    let p = this.start()
 | 
						|
      .then(idp => {
 | 
						|
        let options = {
 | 
						|
          protocol: this.protocol,
 | 
						|
          usernameHint: this.username,
 | 
						|
          peerIdentity: this.peeridentity,
 | 
						|
        };
 | 
						|
        return this._wrapCrossCompartmentPromise(
 | 
						|
          idp.generateAssertion(JSON.stringify(content), origin, options)
 | 
						|
        );
 | 
						|
      })
 | 
						|
      .then(assertion => {
 | 
						|
        if (!this._isValidAssertion(assertion)) {
 | 
						|
          throw new this._win.DOMException(
 | 
						|
            "IdP generated invalid assertion",
 | 
						|
            "IdpError"
 | 
						|
          );
 | 
						|
        }
 | 
						|
        // save the base64+JSON assertion, since that is all that is used
 | 
						|
        this.assertion = btoa(JSON.stringify(assertion));
 | 
						|
        return this.assertion;
 | 
						|
      });
 | 
						|
 | 
						|
    return this._applyTimeout(p);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Promises generated by the sandbox need to be very carefully treated so that
 | 
						|
   * they can chain into promises in the `this._win` compartment.  Results need
 | 
						|
   * to be cloned across; errors need to be converted.
 | 
						|
   */
 | 
						|
  _wrapCrossCompartmentPromise(sandboxPromise) {
 | 
						|
    return new this._win.Promise((resolve, reject) => {
 | 
						|
      sandboxPromise.then(
 | 
						|
        result => resolve(Cu.cloneInto(result, this._win)),
 | 
						|
        e => {
 | 
						|
          let message = "" + (e.message || JSON.stringify(e) || "IdP error");
 | 
						|
          if (e.name === "IdpLoginError") {
 | 
						|
            if (typeof e.loginUrl === "string") {
 | 
						|
              this.idpLoginUrl = e.loginUrl;
 | 
						|
            }
 | 
						|
            reject(new this._win.DOMException(message, "IdpLoginError"));
 | 
						|
          } else {
 | 
						|
            reject(new this._win.DOMException(message, "IdpError"));
 | 
						|
          }
 | 
						|
        }
 | 
						|
      );
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Wraps a promise, adding a timeout guard on it so that it can't take longer
 | 
						|
   * than the specified time.  Returns a promise that rejects if the timeout
 | 
						|
   * elapses before `p` resolves.
 | 
						|
   */
 | 
						|
  _applyTimeout(p) {
 | 
						|
    let timeout = new this._win.Promise(r =>
 | 
						|
      this._win.setTimeout(r, this._timeout)
 | 
						|
    ).then(() => {
 | 
						|
      throw new this._win.DOMException("IdP timed out", "IdpError");
 | 
						|
    });
 | 
						|
    return this._win.Promise.race([timeout, p]);
 | 
						|
  },
 | 
						|
};
 |