1 /* jshint moz:true, browser:true */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 var EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
8 ChromeUtils.defineModuleGetter(
11 "resource://gre/modules/media/IdpSandbox.jsm"
15 * Creates an IdP helper.
17 * @param win (object) the window we are working for
18 * @param timeout (int) the timeout in milliseconds
20 function PeerConnectionIdp(win, timeout) {
22 this._timeout = timeout || 5000;
25 this._resetAssertion();
29 PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
30 // attributes are funny, the 'a' is case sensitive, the name isn't
31 let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
32 PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
33 pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
34 PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
37 PeerConnectionIdp.prototype = {
43 this.assertion = null;
44 this.idpLoginUrl = null;
47 setIdentityProvider(provider, protocol, usernameHint, peerIdentity) {
48 this._resetAssertion();
49 this.provider = provider;
50 this.protocol = protocol;
51 this.username = usernameHint;
52 this.peeridentity = peerIdentity;
54 if (this._idp.isSame(provider, protocol)) {
59 this._idp = new IdpSandbox(provider, protocol, this._win);
62 // start the IdP and do some error fixup
64 return this._idp.start().catch(e => {
65 throw new this._win.DOMException(e.message, "IdpError");
70 this._resetAssertion();
74 this.peeridentity = null;
81 _getFingerprintsFromSdp(sdp) {
82 let fingerprints = {};
83 let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
85 fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
86 sdp = sdp.substring(m.index + m[0].length);
87 m = sdp.match(PeerConnectionIdp._fingerprintPattern);
90 return Object.keys(fingerprints).map(k => fingerprints[k]);
93 _isValidAssertion(assertion) {
97 typeof assertion.idp.domain === "string" &&
98 (!assertion.idp.protocol || typeof assertion.idp.protocol === "string") &&
99 typeof assertion.assertion === "string"
103 _getSessionLevelEnd(sdp) {
104 const match = sdp.match(PeerConnectionIdp._mLinePattern);
111 _getIdentityFromSdp(sdp) {
112 // a=identity is session level
114 const index = this._getSessionLevelEnd(sdp);
115 const sessionLevel = sdp.substring(0, index);
116 idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
118 return undefined; // undefined === no identity
123 assertion = JSON.parse(atob(idMatch[1]));
125 throw new this._win.DOMException(
126 "invalid identity assertion: " + e,
127 "InvalidSessionDescriptionError"
130 if (!this._isValidAssertion(assertion)) {
131 throw new this._win.DOMException(
132 "assertion missing idp/idp.domain/assertion",
133 "InvalidSessionDescriptionError"
140 * Verifies the a=identity line the given SDP contains, if any.
141 * If the verification succeeds callback is called with the message from the
142 * IdP proxy as parameter, else (verification failed OR no a=identity line in
143 * SDP at all) null is passed to callback.
145 * Note that this only verifies that the SDP is coherent. We still rely on
146 * the fact that the RTCPeerConnection won't connect to a peer if the
147 * fingerprint of the certificate they offer doesn't appear in the SDP.
149 verifyIdentityFromSDP(sdp, origin) {
150 let identity = this._getIdentityFromSdp(sdp);
151 let fingerprints = this._getFingerprintsFromSdp(sdp);
152 if (!identity || fingerprints.length <= 0) {
153 return this._win.Promise.resolve(); // undefined result = no identity
156 this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
157 return this._verifyIdentity(identity.assertion, fingerprints, origin);
161 * Checks that the name in the identity provided by the IdP is OK.
163 * @param name (string) the name to validate
164 * @throws if the name isn't valid
166 _validateName(name) {
168 throw new this._win.DOMException(
169 "assertion name error: " + msg,
174 if (typeof name !== "string") {
175 error("name not a string");
177 let atIdx = name.indexOf("@");
179 error("missing authority in name from IdP");
182 // no third party assertions... for now
183 let tail = name.substring(atIdx + 1);
185 // strip the port number, if present
186 let provider = this.provider;
187 let providerPortIdx = provider.indexOf(":");
188 if (providerPortIdx > 0) {
189 provider = provider.substring(0, providerPortIdx);
191 let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
195 idnService.convertUTF8toACE(tail) !==
196 idnService.convertUTF8toACE(provider)
198 error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"');
203 * Check the validation response. We are very defensive here when handling
204 * the message from the IdP proxy. That way, broken IdPs aren't likely to
205 * cause catastrophic damage.
207 _checkValidation(validation, sdpFingerprints) {
209 throw new this._win.DOMException(
210 "IdP validation error: " + msg,
215 if (!this.provider) {
220 typeof validation !== "object" ||
221 typeof validation.contents !== "string" ||
222 typeof validation.identity !== "string"
224 error("no payload in validation response");
229 fingerprints = JSON.parse(validation.contents).fingerprint;
231 error("invalid JSON");
234 let isFingerprint = f =>
235 typeof f.digest === "string" && typeof f.algorithm === "string";
236 if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
238 "fingerprints must be an array of objects" +
239 " with digest and algorithm attributes"
243 // everything in `innerSet` is found in `outerSet`
244 let isSubsetOf = (outerSet, innerSet, comparator) => {
245 return innerSet.every(i => {
246 return outerSet.some(o => comparator(i, o));
249 let compareFingerprints = (a, b) => {
250 return a.digest === b.digest && a.algorithm === b.algorithm;
252 if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
253 error("the fingerprints must be covered by the assertion");
255 this._validateName(validation.identity);
260 * Asks the IdP proxy to verify an identity assertion.
262 _verifyIdentity(assertion, fingerprints, origin) {
265 this._wrapCrossCompartmentPromise(
266 idp.validateAssertion(assertion, origin)
269 .then(validation => this._checkValidation(validation, fingerprints));
271 return this._applyTimeout(p);
275 * Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
276 * must have already run successfully, otherwise this does nothing to the sdp.
278 addIdentityAttribute(sdp) {
279 if (!this.assertion) {
283 const index = this._getSessionLevelEnd(sdp);
285 sdp.substring(0, index) +
294 * Asks the IdP proxy for an identity assertion. Don't call this unless you
295 * have checked .enabled, or you really like exceptions. Also, don't call
296 * this when another call is still running, because it's not certain which
297 * call will finish first and the final state will be similarly uncertain.
299 getIdentityAssertion(fingerprint, origin) {
301 throw new this._win.DOMException(
302 "no IdP set, call setIdentityProvider() to set one",
307 let [algorithm, digest] = fingerprint.split(" ", 2);
317 this._resetAssertion();
321 protocol: this.protocol,
322 usernameHint: this.username,
323 peerIdentity: this.peeridentity,
325 return this._wrapCrossCompartmentPromise(
326 idp.generateAssertion(JSON.stringify(content), origin, options)
330 if (!this._isValidAssertion(assertion)) {
331 throw new this._win.DOMException(
332 "IdP generated invalid assertion",
336 // save the base64+JSON assertion, since that is all that is used
337 this.assertion = btoa(JSON.stringify(assertion));
338 return this.assertion;
341 return this._applyTimeout(p);
345 * Promises generated by the sandbox need to be very carefully treated so that
346 * they can chain into promises in the `this._win` compartment. Results need
347 * to be cloned across; errors need to be converted.
349 _wrapCrossCompartmentPromise(sandboxPromise) {
350 return new this._win.Promise((resolve, reject) => {
352 result => resolve(Cu.cloneInto(result, this._win)),
354 let message = "" + (e.message || JSON.stringify(e) || "IdP error");
355 if (e.name === "IdpLoginError") {
356 if (typeof e.loginUrl === "string") {
357 this.idpLoginUrl = e.loginUrl;
359 reject(new this._win.DOMException(message, "IdpLoginError"));
361 reject(new this._win.DOMException(message, "IdpError"));
369 * Wraps a promise, adding a timeout guard on it so that it can't take longer
370 * than the specified time. Returns a promise that rejects if the timeout
371 * elapses before `p` resolves.
374 let timeout = new this._win.Promise(r =>
375 this._win.setTimeout(r, this._timeout)
377 throw new this._win.DOMException("IdP timed out", "IdpError");
379 return this._win.Promise.race([timeout, p]);