diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/PeerConnectionIdp.sys.mjs | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/dom/media/PeerConnectionIdp.sys.mjs b/dom/media/PeerConnectionIdp.sys.mjs new file mode 100644 index 0000000000..2be6643b06 --- /dev/null +++ b/dom/media/PeerConnectionIdp.sys.mjs @@ -0,0 +1,378 @@ +/* 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]); + }, +}; |