PeerConnectionIdp.sys.mjs (11113B)
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/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 IdpSandbox: "resource://gre/modules/media/IdpSandbox.sys.mjs", 9 }); 10 11 /** 12 * Creates an IdP helper. 13 * 14 * @param win (object) the window we are working for 15 * @param timeout (int) the timeout in milliseconds 16 */ 17 export function PeerConnectionIdp(win, timeout) { 18 this._win = win; 19 this._timeout = timeout || 5000; 20 21 this.provider = null; 22 this._resetAssertion(); 23 } 24 25 (function () { 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"); 32 })(); 33 34 PeerConnectionIdp.prototype = { 35 get enabled() { 36 return !!this._idp; 37 }, 38 39 _resetAssertion() { 40 this.assertion = null; 41 this.idpLoginUrl = null; 42 }, 43 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; 50 if (this._idp) { 51 if (this._idp.isSame(provider, protocol)) { 52 return; // noop 53 } 54 this._idp.stop(); 55 } 56 this._idp = new lazy.IdpSandbox(provider, protocol, this._win); 57 }, 58 59 // start the IdP and do some error fixup 60 start() { 61 return this._idp.start().catch(e => { 62 throw new this._win.DOMException(e.message, "IdpError"); 63 }); 64 }, 65 66 close() { 67 this._resetAssertion(); 68 this.provider = null; 69 this.protocol = null; 70 this.username = null; 71 this.peeridentity = null; 72 if (this._idp) { 73 this._idp.stop(); 74 this._idp = null; 75 } 76 }, 77 78 _getFingerprintsFromSdp(sdp) { 79 let fingerprints = {}; 80 let m = sdp.match(PeerConnectionIdp._fingerprintPattern); 81 while (m) { 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); 85 } 86 87 return Object.keys(fingerprints).map(k => fingerprints[k]); 88 }, 89 90 _isValidAssertion(assertion) { 91 return ( 92 assertion && 93 assertion.idp && 94 typeof assertion.idp.domain === "string" && 95 (!assertion.idp.protocol || typeof assertion.idp.protocol === "string") && 96 typeof assertion.assertion === "string" 97 ); 98 }, 99 100 _getSessionLevelEnd(sdp) { 101 const match = sdp.match(PeerConnectionIdp._mLinePattern); 102 if (!match) { 103 return sdp.length; 104 } 105 return match.index; 106 }, 107 108 _getIdentityFromSdp(sdp) { 109 // a=identity is session level 110 let idMatch; 111 const index = this._getSessionLevelEnd(sdp); 112 const sessionLevel = sdp.substring(0, index); 113 idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern); 114 if (!idMatch) { 115 return undefined; // undefined === no identity 116 } 117 118 let assertion; 119 try { 120 assertion = JSON.parse(atob(idMatch[1])); 121 } catch (e) { 122 throw new this._win.DOMException( 123 "invalid identity assertion: " + e, 124 "InvalidSessionDescriptionError" 125 ); 126 } 127 if (!this._isValidAssertion(assertion)) { 128 throw new this._win.DOMException( 129 "assertion missing idp/idp.domain/assertion", 130 "InvalidSessionDescriptionError" 131 ); 132 } 133 return assertion; 134 }, 135 136 /** 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. 141 * 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. 145 */ 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 151 } 152 153 this.setIdentityProvider(identity.idp.domain, identity.idp.protocol); 154 return this._verifyIdentity(identity.assertion, fingerprints, origin); 155 }, 156 157 /** 158 * Checks that the name in the identity provided by the IdP is OK. 159 * 160 * @param name (string) the name to validate 161 * @throws if the name isn't valid 162 */ 163 _validateName(name) { 164 let error = msg => { 165 throw new this._win.DOMException( 166 "assertion name error: " + msg, 167 "IdpError" 168 ); 169 }; 170 171 if (typeof name !== "string") { 172 error("name not a string"); 173 } 174 let atIdx = name.indexOf("@"); 175 if (atIdx <= 0) { 176 error("missing authority in name from IdP"); 177 } 178 179 // no third party assertions... for now 180 let tail = name.substring(atIdx + 1); 181 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); 187 } 188 let idnService = Cc["@mozilla.org/network/idn-service;1"].getService( 189 Ci.nsIIDNService 190 ); 191 if (idnService.domainToASCII(tail) !== idnService.domainToASCII(provider)) { 192 error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"'); 193 } 194 }, 195 196 /** 197 * Check the validation response. We are very defensive here when handling 198 * the message from the IdP proxy. That way, broken IdPs aren't likely to 199 * cause catastrophic damage. 200 */ 201 _checkValidation(validation, sdpFingerprints) { 202 let error = msg => { 203 throw new this._win.DOMException( 204 "IdP validation error: " + msg, 205 "IdpError" 206 ); 207 }; 208 209 if (!this.provider) { 210 error("IdP closed"); 211 } 212 213 if ( 214 typeof validation !== "object" || 215 typeof validation.contents !== "string" || 216 typeof validation.identity !== "string" 217 ) { 218 error("no payload in validation response"); 219 } 220 221 let fingerprints; 222 try { 223 fingerprints = JSON.parse(validation.contents).fingerprint; 224 } catch (e) { 225 error("invalid JSON"); 226 } 227 228 let isFingerprint = f => 229 typeof f.digest === "string" && typeof f.algorithm === "string"; 230 if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) { 231 error( 232 "fingerprints must be an array of objects" + 233 " with digest and algorithm attributes" 234 ); 235 } 236 237 // everything in `innerSet` is found in `outerSet` 238 let isSubsetOf = (outerSet, innerSet, comparator) => { 239 return innerSet.every(i => { 240 return outerSet.some(o => comparator(i, o)); 241 }); 242 }; 243 let compareFingerprints = (a, b) => { 244 return a.digest === b.digest && a.algorithm === b.algorithm; 245 }; 246 if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) { 247 error("the fingerprints must be covered by the assertion"); 248 } 249 this._validateName(validation.identity); 250 return validation; 251 }, 252 253 /** 254 * Asks the IdP proxy to verify an identity assertion. 255 */ 256 _verifyIdentity(assertion, fingerprints, origin) { 257 let p = this.start() 258 .then(idp => 259 this._wrapCrossCompartmentPromise( 260 idp.validateAssertion(assertion, origin) 261 ) 262 ) 263 .then(validation => this._checkValidation(validation, fingerprints)); 264 265 return this._applyTimeout(p); 266 }, 267 268 /** 269 * Enriches the given SDP with an `a=identity` line. getIdentityAssertion() 270 * must have already run successfully, otherwise this does nothing to the sdp. 271 */ 272 addIdentityAttribute(sdp) { 273 if (!this.assertion) { 274 return sdp; 275 } 276 277 const index = this._getSessionLevelEnd(sdp); 278 return ( 279 sdp.substring(0, index) + 280 "a=identity:" + 281 this.assertion + 282 "\r\n" + 283 sdp.substring(index) 284 ); 285 }, 286 287 /** 288 * Asks the IdP proxy for an identity assertion. Don't call this unless you 289 * have checked .enabled, or you really like exceptions. Also, don't call 290 * this when another call is still running, because it's not certain which 291 * call will finish first and the final state will be similarly uncertain. 292 */ 293 getIdentityAssertion(fingerprint, origin) { 294 if (!this.enabled) { 295 throw new this._win.DOMException( 296 "no IdP set, call setIdentityProvider() to set one", 297 "InvalidStateError" 298 ); 299 } 300 301 let [algorithm, digest] = fingerprint.split(" ", 2); 302 let content = { 303 fingerprint: [ 304 { 305 algorithm, 306 digest, 307 }, 308 ], 309 }; 310 311 this._resetAssertion(); 312 let p = this.start() 313 .then(idp => { 314 let options = { 315 protocol: this.protocol, 316 usernameHint: this.username, 317 peerIdentity: this.peeridentity, 318 }; 319 return this._wrapCrossCompartmentPromise( 320 idp.generateAssertion(JSON.stringify(content), origin, options) 321 ); 322 }) 323 .then(assertion => { 324 if (!this._isValidAssertion(assertion)) { 325 throw new this._win.DOMException( 326 "IdP generated invalid assertion", 327 "IdpError" 328 ); 329 } 330 // save the base64+JSON assertion, since that is all that is used 331 this.assertion = btoa(JSON.stringify(assertion)); 332 return this.assertion; 333 }); 334 335 return this._applyTimeout(p); 336 }, 337 338 /** 339 * Promises generated by the sandbox need to be very carefully treated so that 340 * they can chain into promises in the `this._win` compartment. Results need 341 * to be cloned across; errors need to be converted. 342 */ 343 _wrapCrossCompartmentPromise(sandboxPromise) { 344 return new this._win.Promise((resolve, reject) => { 345 sandboxPromise.then( 346 result => resolve(Cu.cloneInto(result, this._win)), 347 e => { 348 let message = "" + (e.message || JSON.stringify(e) || "IdP error"); 349 if (e.name === "IdpLoginError") { 350 if (typeof e.loginUrl === "string") { 351 this.idpLoginUrl = e.loginUrl; 352 } 353 reject(new this._win.DOMException(message, "IdpLoginError")); 354 } else { 355 reject(new this._win.DOMException(message, "IdpError")); 356 } 357 } 358 ); 359 }); 360 }, 361 362 /** 363 * Wraps a promise, adding a timeout guard on it so that it can't take longer 364 * than the specified time. Returns a promise that rejects if the timeout 365 * elapses before `p` resolves. 366 */ 367 _applyTimeout(p) { 368 let timeout = new this._win.Promise(r => 369 this._win.setTimeout(r, this._timeout) 370 ).then(() => { 371 throw new this._win.DOMException("IdP timed out", "IdpError"); 372 }); 373 return this._win.Promise.race([timeout, p]); 374 }, 375 };