123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- /* 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'];
- 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, 'IdpSandbox',
- 'resource://gre/modules/media/IdpSandbox.jsm');
- /**
- * Creates an IdP helper.
- *
- * @param win (object) the window we are working for
- * @param timeout (int) the timeout in milliseconds
- */
- 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: function() {
- this.assertion = null;
- this.idpLoginUrl = null;
- },
- setIdentityProvider: function(provider, protocol, username) {
- this._resetAssertion();
- this.provider = provider;
- this.protocol = protocol || 'default';
- this.username = username;
- if (this._idp) {
- if (this._idp.isSame(provider, protocol)) {
- return; // noop
- }
- this._idp.stop();
- }
- this._idp = new IdpSandbox(provider, protocol, this._win);
- },
- // start the IdP and do some error fixup
- start: function() {
- return this._idp.start()
- .catch(e => {
- throw new this._win.DOMException(e.message, 'IdpError');
- });
- },
- close: function() {
- this._resetAssertion();
- this.provider = null;
- this.protocol = null;
- if (this._idp) {
- this._idp.stop();
- this._idp = null;
- }
- },
- _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 idMatch;
- let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
- if (mLineMatch) {
- let sessionLevel = sdp.substring(0, mLineMatch.index);
- idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
- }
- if (!idMatch) {
- return; // 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: function(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: function(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 = Components.classes['@mozilla.org/network/idn-service;1']
- .getService(Components.interfaces.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: function(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: function(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: 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' +
- sdp.substring(match.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: function(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: algorithm,
- digest: digest
- }]
- };
- this._resetAssertion();
- let p = this.start()
- .then(idp => this._wrapCrossCompartmentPromise(
- idp.generateAssertion(JSON.stringify(content),
- origin, this.username)))
- .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: function(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: function(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 ]);
- }
- };
- this.PeerConnectionIdp = PeerConnectionIdp;
|