diff options
Diffstat (limited to 'dom/media/PeerConnectionIdp.jsm')
-rw-r--r-- | dom/media/PeerConnectionIdp.jsm | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/dom/media/PeerConnectionIdp.jsm b/dom/media/PeerConnectionIdp.jsm new file mode 100644 index 000000000..d1578d1b7 --- /dev/null +++ b/dom/media/PeerConnectionIdp.jsm @@ -0,0 +1,341 @@ +/* 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; |