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/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 IdpSandbox: "resource://gre/modules/media/IdpSandbox.sys.mjs",
12 * Creates an IdP helper.
14 * @param win (object) the window we are working for
15 * @param timeout (int) the timeout in milliseconds
17 export function PeerConnectionIdp(win, timeout) {
19 this._timeout = timeout || 5000;
22 this._resetAssertion();
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");
34 PeerConnectionIdp.prototype = {
40 this.assertion = null;
41 this.idpLoginUrl = null;
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;
51 if (this._idp.isSame(provider, protocol)) {
56 this._idp = new lazy.IdpSandbox(provider, protocol, this._win);
59 // start the IdP and do some error fixup
61 return this._idp.start().catch(e => {
62 throw new this._win.DOMException(e.message, "IdpError");
67 this._resetAssertion();
71 this.peeridentity = null;
78 _getFingerprintsFromSdp(sdp) {
79 let fingerprints = {};
80 let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
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);
87 return Object.keys(fingerprints).map(k => fingerprints[k]);
90 _isValidAssertion(assertion) {
94 typeof assertion.idp.domain === "string" &&
95 (!assertion.idp.protocol || typeof assertion.idp.protocol === "string") &&
96 typeof assertion.assertion === "string"
100 _getSessionLevelEnd(sdp) {
101 const match = sdp.match(PeerConnectionIdp._mLinePattern);
108 _getIdentityFromSdp(sdp) {
109 // a=identity is session level
111 const index = this._getSessionLevelEnd(sdp);
112 const sessionLevel = sdp.substring(0, index);
113 idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
115 return undefined; // undefined === no identity
120 assertion = JSON.parse(atob(idMatch[1]));
122 throw new this._win.DOMException(
123 "invalid identity assertion: " + e,
124 "InvalidSessionDescriptionError"
127 if (!this._isValidAssertion(assertion)) {
128 throw new this._win.DOMException(
129 "assertion missing idp/idp.domain/assertion",
130 "InvalidSessionDescriptionError"
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.
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.
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
153 this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
154 return this._verifyIdentity(identity.assertion, fingerprints, origin);
158 * Checks that the name in the identity provided by the IdP is OK.
160 * @param name (string) the name to validate
161 * @throws if the name isn't valid
163 _validateName(name) {
165 throw new this._win.DOMException(
166 "assertion name error: " + msg,
171 if (typeof name !== "string") {
172 error("name not a string");
174 let atIdx = name.indexOf("@");
176 error("missing authority in name from IdP");
179 // no third party assertions... for now
180 let tail = name.substring(atIdx + 1);
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);
188 let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
192 idnService.convertUTF8toACE(tail) !==
193 idnService.convertUTF8toACE(provider)
195 error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"');
200 * Check the validation response. We are very defensive here when handling
201 * the message from the IdP proxy. That way, broken IdPs aren't likely to
202 * cause catastrophic damage.
204 _checkValidation(validation, sdpFingerprints) {
206 throw new this._win.DOMException(
207 "IdP validation error: " + msg,
212 if (!this.provider) {
217 typeof validation !== "object" ||
218 typeof validation.contents !== "string" ||
219 typeof validation.identity !== "string"
221 error("no payload in validation response");
226 fingerprints = JSON.parse(validation.contents).fingerprint;
228 error("invalid JSON");
231 let isFingerprint = f =>
232 typeof f.digest === "string" && typeof f.algorithm === "string";
233 if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
235 "fingerprints must be an array of objects" +
236 " with digest and algorithm attributes"
240 // everything in `innerSet` is found in `outerSet`
241 let isSubsetOf = (outerSet, innerSet, comparator) => {
242 return innerSet.every(i => {
243 return outerSet.some(o => comparator(i, o));
246 let compareFingerprints = (a, b) => {
247 return a.digest === b.digest && a.algorithm === b.algorithm;
249 if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
250 error("the fingerprints must be covered by the assertion");
252 this._validateName(validation.identity);
257 * Asks the IdP proxy to verify an identity assertion.
259 _verifyIdentity(assertion, fingerprints, origin) {
262 this._wrapCrossCompartmentPromise(
263 idp.validateAssertion(assertion, origin)
266 .then(validation => this._checkValidation(validation, fingerprints));
268 return this._applyTimeout(p);
272 * Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
273 * must have already run successfully, otherwise this does nothing to the sdp.
275 addIdentityAttribute(sdp) {
276 if (!this.assertion) {
280 const index = this._getSessionLevelEnd(sdp);
282 sdp.substring(0, index) +
291 * Asks the IdP proxy for an identity assertion. Don't call this unless you
292 * have checked .enabled, or you really like exceptions. Also, don't call
293 * this when another call is still running, because it's not certain which
294 * call will finish first and the final state will be similarly uncertain.
296 getIdentityAssertion(fingerprint, origin) {
298 throw new this._win.DOMException(
299 "no IdP set, call setIdentityProvider() to set one",
304 let [algorithm, digest] = fingerprint.split(" ", 2);
314 this._resetAssertion();
318 protocol: this.protocol,
319 usernameHint: this.username,
320 peerIdentity: this.peeridentity,
322 return this._wrapCrossCompartmentPromise(
323 idp.generateAssertion(JSON.stringify(content), origin, options)
327 if (!this._isValidAssertion(assertion)) {
328 throw new this._win.DOMException(
329 "IdP generated invalid assertion",
333 // save the base64+JSON assertion, since that is all that is used
334 this.assertion = btoa(JSON.stringify(assertion));
335 return this.assertion;
338 return this._applyTimeout(p);
342 * Promises generated by the sandbox need to be very carefully treated so that
343 * they can chain into promises in the `this._win` compartment. Results need
344 * to be cloned across; errors need to be converted.
346 _wrapCrossCompartmentPromise(sandboxPromise) {
347 return new this._win.Promise((resolve, reject) => {
349 result => resolve(Cu.cloneInto(result, this._win)),
351 let message = "" + (e.message || JSON.stringify(e) || "IdP error");
352 if (e.name === "IdpLoginError") {
353 if (typeof e.loginUrl === "string") {
354 this.idpLoginUrl = e.loginUrl;
356 reject(new this._win.DOMException(message, "IdpLoginError"));
358 reject(new this._win.DOMException(message, "IdpError"));
366 * Wraps a promise, adding a timeout guard on it so that it can't take longer
367 * than the specified time. Returns a promise that rejects if the timeout
368 * elapses before `p` resolves.
371 let timeout = new this._win.Promise(r =>
372 this._win.setTimeout(r, this._timeout)
374 throw new this._win.DOMException("IdP timed out", "IdpError");
376 return this._win.Promise.race([timeout, p]);