tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PeerConnectionIdp.sys.mjs (11113B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  IdpSandbox: "resource://gre/modules/media/IdpSandbox.sys.mjs",
      9 });
     10 
     11 /**
     12 * Creates an IdP helper.
     13 *
     14 * @param win (object) the window we are working for
     15 * @param timeout (int) the timeout in milliseconds
     16 */
     17 export function PeerConnectionIdp(win, timeout) {
     18  this._win = win;
     19  this._timeout = timeout || 5000;
     20 
     21  this.provider = null;
     22  this._resetAssertion();
     23 }
     24 
     25 (function () {
     26  PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
     27  // attributes are funny, the 'a' is case sensitive, the name isn't
     28  let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
     29  PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
     30  pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
     31  PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
     32 })();
     33 
     34 PeerConnectionIdp.prototype = {
     35  get enabled() {
     36    return !!this._idp;
     37  },
     38 
     39  _resetAssertion() {
     40    this.assertion = null;
     41    this.idpLoginUrl = null;
     42  },
     43 
     44  setIdentityProvider(provider, protocol, usernameHint, peerIdentity) {
     45    this._resetAssertion();
     46    this.provider = provider;
     47    this.protocol = protocol;
     48    this.username = usernameHint;
     49    this.peeridentity = peerIdentity;
     50    if (this._idp) {
     51      if (this._idp.isSame(provider, protocol)) {
     52        return; // noop
     53      }
     54      this._idp.stop();
     55    }
     56    this._idp = new lazy.IdpSandbox(provider, protocol, this._win);
     57  },
     58 
     59  // start the IdP and do some error fixup
     60  start() {
     61    return this._idp.start().catch(e => {
     62      throw new this._win.DOMException(e.message, "IdpError");
     63    });
     64  },
     65 
     66  close() {
     67    this._resetAssertion();
     68    this.provider = null;
     69    this.protocol = null;
     70    this.username = null;
     71    this.peeridentity = null;
     72    if (this._idp) {
     73      this._idp.stop();
     74      this._idp = null;
     75    }
     76  },
     77 
     78  _getFingerprintsFromSdp(sdp) {
     79    let fingerprints = {};
     80    let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
     81    while (m) {
     82      fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
     83      sdp = sdp.substring(m.index + m[0].length);
     84      m = sdp.match(PeerConnectionIdp._fingerprintPattern);
     85    }
     86 
     87    return Object.keys(fingerprints).map(k => fingerprints[k]);
     88  },
     89 
     90  _isValidAssertion(assertion) {
     91    return (
     92      assertion &&
     93      assertion.idp &&
     94      typeof assertion.idp.domain === "string" &&
     95      (!assertion.idp.protocol || typeof assertion.idp.protocol === "string") &&
     96      typeof assertion.assertion === "string"
     97    );
     98  },
     99 
    100  _getSessionLevelEnd(sdp) {
    101    const match = sdp.match(PeerConnectionIdp._mLinePattern);
    102    if (!match) {
    103      return sdp.length;
    104    }
    105    return match.index;
    106  },
    107 
    108  _getIdentityFromSdp(sdp) {
    109    // a=identity is session level
    110    let idMatch;
    111    const index = this._getSessionLevelEnd(sdp);
    112    const sessionLevel = sdp.substring(0, index);
    113    idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
    114    if (!idMatch) {
    115      return undefined; // undefined === no identity
    116    }
    117 
    118    let assertion;
    119    try {
    120      assertion = JSON.parse(atob(idMatch[1]));
    121    } catch (e) {
    122      throw new this._win.DOMException(
    123        "invalid identity assertion: " + e,
    124        "InvalidSessionDescriptionError"
    125      );
    126    }
    127    if (!this._isValidAssertion(assertion)) {
    128      throw new this._win.DOMException(
    129        "assertion missing idp/idp.domain/assertion",
    130        "InvalidSessionDescriptionError"
    131      );
    132    }
    133    return assertion;
    134  },
    135 
    136  /**
    137   * Verifies the a=identity line the given SDP contains, if any.
    138   * If the verification succeeds callback is called with the message from the
    139   * IdP proxy as parameter, else (verification failed OR no a=identity line in
    140   * SDP at all) null is passed to callback.
    141   *
    142   * Note that this only verifies that the SDP is coherent.  We still rely on
    143   * the fact that the RTCPeerConnection won't connect to a peer if the
    144   * fingerprint of the certificate they offer doesn't appear in the SDP.
    145   */
    146  verifyIdentityFromSDP(sdp, origin) {
    147    let identity = this._getIdentityFromSdp(sdp);
    148    let fingerprints = this._getFingerprintsFromSdp(sdp);
    149    if (!identity || fingerprints.length <= 0) {
    150      return this._win.Promise.resolve(); // undefined result = no identity
    151    }
    152 
    153    this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
    154    return this._verifyIdentity(identity.assertion, fingerprints, origin);
    155  },
    156 
    157  /**
    158   * Checks that the name in the identity provided by the IdP is OK.
    159   *
    160   * @param name (string) the name to validate
    161   * @throws if the name isn't valid
    162   */
    163  _validateName(name) {
    164    let error = msg => {
    165      throw new this._win.DOMException(
    166        "assertion name error: " + msg,
    167        "IdpError"
    168      );
    169    };
    170 
    171    if (typeof name !== "string") {
    172      error("name not a string");
    173    }
    174    let atIdx = name.indexOf("@");
    175    if (atIdx <= 0) {
    176      error("missing authority in name from IdP");
    177    }
    178 
    179    // no third party assertions... for now
    180    let tail = name.substring(atIdx + 1);
    181 
    182    // strip the port number, if present
    183    let provider = this.provider;
    184    let providerPortIdx = provider.indexOf(":");
    185    if (providerPortIdx > 0) {
    186      provider = provider.substring(0, providerPortIdx);
    187    }
    188    let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
    189      Ci.nsIIDNService
    190    );
    191    if (idnService.domainToASCII(tail) !== idnService.domainToASCII(provider)) {
    192      error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"');
    193    }
    194  },
    195 
    196  /**
    197   * Check the validation response.  We are very defensive here when handling
    198   * the message from the IdP proxy.  That way, broken IdPs aren't likely to
    199   * cause catastrophic damage.
    200   */
    201  _checkValidation(validation, sdpFingerprints) {
    202    let error = msg => {
    203      throw new this._win.DOMException(
    204        "IdP validation error: " + msg,
    205        "IdpError"
    206      );
    207    };
    208 
    209    if (!this.provider) {
    210      error("IdP closed");
    211    }
    212 
    213    if (
    214      typeof validation !== "object" ||
    215      typeof validation.contents !== "string" ||
    216      typeof validation.identity !== "string"
    217    ) {
    218      error("no payload in validation response");
    219    }
    220 
    221    let fingerprints;
    222    try {
    223      fingerprints = JSON.parse(validation.contents).fingerprint;
    224    } catch (e) {
    225      error("invalid JSON");
    226    }
    227 
    228    let isFingerprint = f =>
    229      typeof f.digest === "string" && typeof f.algorithm === "string";
    230    if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
    231      error(
    232        "fingerprints must be an array of objects" +
    233          " with digest and algorithm attributes"
    234      );
    235    }
    236 
    237    // everything in `innerSet` is found in `outerSet`
    238    let isSubsetOf = (outerSet, innerSet, comparator) => {
    239      return innerSet.every(i => {
    240        return outerSet.some(o => comparator(i, o));
    241      });
    242    };
    243    let compareFingerprints = (a, b) => {
    244      return a.digest === b.digest && a.algorithm === b.algorithm;
    245    };
    246    if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
    247      error("the fingerprints must be covered by the assertion");
    248    }
    249    this._validateName(validation.identity);
    250    return validation;
    251  },
    252 
    253  /**
    254   * Asks the IdP proxy to verify an identity assertion.
    255   */
    256  _verifyIdentity(assertion, fingerprints, origin) {
    257    let p = this.start()
    258      .then(idp =>
    259        this._wrapCrossCompartmentPromise(
    260          idp.validateAssertion(assertion, origin)
    261        )
    262      )
    263      .then(validation => this._checkValidation(validation, fingerprints));
    264 
    265    return this._applyTimeout(p);
    266  },
    267 
    268  /**
    269   * Enriches the given SDP with an `a=identity` line.  getIdentityAssertion()
    270   * must have already run successfully, otherwise this does nothing to the sdp.
    271   */
    272  addIdentityAttribute(sdp) {
    273    if (!this.assertion) {
    274      return sdp;
    275    }
    276 
    277    const index = this._getSessionLevelEnd(sdp);
    278    return (
    279      sdp.substring(0, index) +
    280      "a=identity:" +
    281      this.assertion +
    282      "\r\n" +
    283      sdp.substring(index)
    284    );
    285  },
    286 
    287  /**
    288   * Asks the IdP proxy for an identity assertion.  Don't call this unless you
    289   * have checked .enabled, or you really like exceptions.  Also, don't call
    290   * this when another call is still running, because it's not certain which
    291   * call will finish first and the final state will be similarly uncertain.
    292   */
    293  getIdentityAssertion(fingerprint, origin) {
    294    if (!this.enabled) {
    295      throw new this._win.DOMException(
    296        "no IdP set, call setIdentityProvider() to set one",
    297        "InvalidStateError"
    298      );
    299    }
    300 
    301    let [algorithm, digest] = fingerprint.split(" ", 2);
    302    let content = {
    303      fingerprint: [
    304        {
    305          algorithm,
    306          digest,
    307        },
    308      ],
    309    };
    310 
    311    this._resetAssertion();
    312    let p = this.start()
    313      .then(idp => {
    314        let options = {
    315          protocol: this.protocol,
    316          usernameHint: this.username,
    317          peerIdentity: this.peeridentity,
    318        };
    319        return this._wrapCrossCompartmentPromise(
    320          idp.generateAssertion(JSON.stringify(content), origin, options)
    321        );
    322      })
    323      .then(assertion => {
    324        if (!this._isValidAssertion(assertion)) {
    325          throw new this._win.DOMException(
    326            "IdP generated invalid assertion",
    327            "IdpError"
    328          );
    329        }
    330        // save the base64+JSON assertion, since that is all that is used
    331        this.assertion = btoa(JSON.stringify(assertion));
    332        return this.assertion;
    333      });
    334 
    335    return this._applyTimeout(p);
    336  },
    337 
    338  /**
    339   * Promises generated by the sandbox need to be very carefully treated so that
    340   * they can chain into promises in the `this._win` compartment.  Results need
    341   * to be cloned across; errors need to be converted.
    342   */
    343  _wrapCrossCompartmentPromise(sandboxPromise) {
    344    return new this._win.Promise((resolve, reject) => {
    345      sandboxPromise.then(
    346        result => resolve(Cu.cloneInto(result, this._win)),
    347        e => {
    348          let message = "" + (e.message || JSON.stringify(e) || "IdP error");
    349          if (e.name === "IdpLoginError") {
    350            if (typeof e.loginUrl === "string") {
    351              this.idpLoginUrl = e.loginUrl;
    352            }
    353            reject(new this._win.DOMException(message, "IdpLoginError"));
    354          } else {
    355            reject(new this._win.DOMException(message, "IdpError"));
    356          }
    357        }
    358      );
    359    });
    360  },
    361 
    362  /**
    363   * Wraps a promise, adding a timeout guard on it so that it can't take longer
    364   * than the specified time.  Returns a promise that rejects if the timeout
    365   * elapses before `p` resolves.
    366   */
    367  _applyTimeout(p) {
    368    let timeout = new this._win.Promise(r =>
    369      this._win.setTimeout(r, this._timeout)
    370    ).then(() => {
    371      throw new this._win.DOMException("IdP timed out", "IdpError");
    372    });
    373    return this._win.Promise.race([timeout, p]);
    374  },
    375 };