PeerConnectionIdp.jsm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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/. */
  5. this.EXPORTED_SYMBOLS = ['PeerConnectionIdp'];
  6. const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  7. Cu.import('resource://gre/modules/Services.jsm');
  8. Cu.import('resource://gre/modules/XPCOMUtils.jsm');
  9. XPCOMUtils.defineLazyModuleGetter(this, 'IdpSandbox',
  10. 'resource://gre/modules/media/IdpSandbox.jsm');
  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. function PeerConnectionIdp(win, timeout) {
  18. this._win = win;
  19. this._timeout = timeout || 5000;
  20. this.provider = null;
  21. this._resetAssertion();
  22. }
  23. (function() {
  24. PeerConnectionIdp._mLinePattern = new RegExp('^m=', 'm');
  25. // attributes are funny, the 'a' is case sensitive, the name isn't
  26. let pattern = '^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)';
  27. PeerConnectionIdp._identityPattern = new RegExp(pattern, 'm');
  28. pattern = '^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)';
  29. PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, 'm');
  30. })();
  31. PeerConnectionIdp.prototype = {
  32. get enabled() {
  33. return !!this._idp;
  34. },
  35. _resetAssertion: function() {
  36. this.assertion = null;
  37. this.idpLoginUrl = null;
  38. },
  39. setIdentityProvider: function(provider, protocol, username) {
  40. this._resetAssertion();
  41. this.provider = provider;
  42. this.protocol = protocol || 'default';
  43. this.username = username;
  44. if (this._idp) {
  45. if (this._idp.isSame(provider, protocol)) {
  46. return; // noop
  47. }
  48. this._idp.stop();
  49. }
  50. this._idp = new IdpSandbox(provider, protocol, this._win);
  51. },
  52. // start the IdP and do some error fixup
  53. start: function() {
  54. return this._idp.start()
  55. .catch(e => {
  56. throw new this._win.DOMException(e.message, 'IdpError');
  57. });
  58. },
  59. close: function() {
  60. this._resetAssertion();
  61. this.provider = null;
  62. this.protocol = null;
  63. if (this._idp) {
  64. this._idp.stop();
  65. this._idp = null;
  66. }
  67. },
  68. _getFingerprintsFromSdp: function(sdp) {
  69. let fingerprints = {};
  70. let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
  71. while (m) {
  72. fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
  73. sdp = sdp.substring(m.index + m[0].length);
  74. m = sdp.match(PeerConnectionIdp._fingerprintPattern);
  75. }
  76. return Object.keys(fingerprints).map(k => fingerprints[k]);
  77. },
  78. _isValidAssertion: function(assertion) {
  79. return assertion && assertion.idp &&
  80. typeof assertion.idp.domain === 'string' &&
  81. (!assertion.idp.protocol ||
  82. typeof assertion.idp.protocol === 'string') &&
  83. typeof assertion.assertion === 'string';
  84. },
  85. _getIdentityFromSdp: function(sdp) {
  86. // a=identity is session level
  87. let idMatch;
  88. let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
  89. if (mLineMatch) {
  90. let sessionLevel = sdp.substring(0, mLineMatch.index);
  91. idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
  92. }
  93. if (!idMatch) {
  94. return; // undefined === no identity
  95. }
  96. let assertion;
  97. try {
  98. assertion = JSON.parse(atob(idMatch[1]));
  99. } catch (e) {
  100. throw new this._win.DOMException('invalid identity assertion: ' + e,
  101. 'InvalidSessionDescriptionError');
  102. }
  103. if (!this._isValidAssertion(assertion)) {
  104. throw new this._win.DOMException('assertion missing idp/idp.domain/assertion',
  105. 'InvalidSessionDescriptionError');
  106. }
  107. return assertion;
  108. },
  109. /**
  110. * Verifies the a=identity line the given SDP contains, if any.
  111. * If the verification succeeds callback is called with the message from the
  112. * IdP proxy as parameter, else (verification failed OR no a=identity line in
  113. * SDP at all) null is passed to callback.
  114. *
  115. * Note that this only verifies that the SDP is coherent. We still rely on
  116. * the fact that the RTCPeerConnection won't connect to a peer if the
  117. * fingerprint of the certificate they offer doesn't appear in the SDP.
  118. */
  119. verifyIdentityFromSDP: function(sdp, origin) {
  120. let identity = this._getIdentityFromSdp(sdp);
  121. let fingerprints = this._getFingerprintsFromSdp(sdp);
  122. if (!identity || fingerprints.length <= 0) {
  123. return this._win.Promise.resolve(); // undefined result = no identity
  124. }
  125. this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
  126. return this._verifyIdentity(identity.assertion, fingerprints, origin);
  127. },
  128. /**
  129. * Checks that the name in the identity provided by the IdP is OK.
  130. *
  131. * @param name (string) the name to validate
  132. * @throws if the name isn't valid
  133. */
  134. _validateName: function(name) {
  135. let error = msg => {
  136. throw new this._win.DOMException('assertion name error: ' + msg,
  137. 'IdpError');
  138. };
  139. if (typeof name !== 'string') {
  140. error('name not a string');
  141. }
  142. let atIdx = name.indexOf('@');
  143. if (atIdx <= 0) {
  144. error('missing authority in name from IdP');
  145. }
  146. // no third party assertions... for now
  147. let tail = name.substring(atIdx + 1);
  148. // strip the port number, if present
  149. let provider = this.provider;
  150. let providerPortIdx = provider.indexOf(':');
  151. if (providerPortIdx > 0) {
  152. provider = provider.substring(0, providerPortIdx);
  153. }
  154. let idnService = Components.classes['@mozilla.org/network/idn-service;1']
  155. .getService(Components.interfaces.nsIIDNService);
  156. if (idnService.convertUTF8toACE(tail) !==
  157. idnService.convertUTF8toACE(provider)) {
  158. error('name "' + name +
  159. '" doesn\'t match IdP: "' + this.provider + '"');
  160. }
  161. },
  162. /**
  163. * Check the validation response. We are very defensive here when handling
  164. * the message from the IdP proxy. That way, broken IdPs aren't likely to
  165. * cause catastrophic damage.
  166. */
  167. _checkValidation: function(validation, sdpFingerprints) {
  168. let error = msg => {
  169. throw new this._win.DOMException('IdP validation error: ' + msg,
  170. 'IdpError');
  171. };
  172. if (!this.provider) {
  173. error('IdP closed');
  174. }
  175. if (typeof validation !== 'object' ||
  176. typeof validation.contents !== 'string' ||
  177. typeof validation.identity !== 'string') {
  178. error('no payload in validation response');
  179. }
  180. let fingerprints;
  181. try {
  182. fingerprints = JSON.parse(validation.contents).fingerprint;
  183. } catch (e) {
  184. error('invalid JSON');
  185. }
  186. let isFingerprint = f =>
  187. (typeof f.digest === 'string') &&
  188. (typeof f.algorithm === 'string');
  189. if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
  190. error('fingerprints must be an array of objects' +
  191. ' with digest and algorithm attributes');
  192. }
  193. // everything in `innerSet` is found in `outerSet`
  194. let isSubsetOf = (outerSet, innerSet, comparator) => {
  195. return innerSet.every(i => {
  196. return outerSet.some(o => comparator(i, o));
  197. });
  198. };
  199. let compareFingerprints = (a, b) => {
  200. return (a.digest === b.digest) && (a.algorithm === b.algorithm);
  201. };
  202. if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
  203. error('the fingerprints must be covered by the assertion');
  204. }
  205. this._validateName(validation.identity);
  206. return validation;
  207. },
  208. /**
  209. * Asks the IdP proxy to verify an identity assertion.
  210. */
  211. _verifyIdentity: function(assertion, fingerprints, origin) {
  212. let p = this.start()
  213. .then(idp => this._wrapCrossCompartmentPromise(
  214. idp.validateAssertion(assertion, origin)))
  215. .then(validation => this._checkValidation(validation, fingerprints));
  216. return this._applyTimeout(p);
  217. },
  218. /**
  219. * Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
  220. * must have already run successfully, otherwise this does nothing to the sdp.
  221. */
  222. addIdentityAttribute: function(sdp) {
  223. if (!this.assertion) {
  224. return sdp;
  225. }
  226. // yes, we assume that this matches; if it doesn't something is *wrong*
  227. let match = sdp.match(PeerConnectionIdp._mLinePattern);
  228. return sdp.substring(0, match.index) +
  229. 'a=identity:' + this.assertion + '\r\n' +
  230. sdp.substring(match.index);
  231. },
  232. /**
  233. * Asks the IdP proxy for an identity assertion. Don't call this unless you
  234. * have checked .enabled, or you really like exceptions. Also, don't call
  235. * this when another call is still running, because it's not certain which
  236. * call will finish first and the final state will be similarly uncertain.
  237. */
  238. getIdentityAssertion: function(fingerprint, origin) {
  239. if (!this.enabled) {
  240. throw new this._win.DOMException(
  241. 'no IdP set, call setIdentityProvider() to set one', 'InvalidStateError');
  242. }
  243. let [algorithm, digest] = fingerprint.split(' ', 2);
  244. let content = {
  245. fingerprint: [{
  246. algorithm: algorithm,
  247. digest: digest
  248. }]
  249. };
  250. this._resetAssertion();
  251. let p = this.start()
  252. .then(idp => this._wrapCrossCompartmentPromise(
  253. idp.generateAssertion(JSON.stringify(content),
  254. origin, this.username)))
  255. .then(assertion => {
  256. if (!this._isValidAssertion(assertion)) {
  257. throw new this._win.DOMException('IdP generated invalid assertion',
  258. 'IdpError');
  259. }
  260. // save the base64+JSON assertion, since that is all that is used
  261. this.assertion = btoa(JSON.stringify(assertion));
  262. return this.assertion;
  263. });
  264. return this._applyTimeout(p);
  265. },
  266. /**
  267. * Promises generated by the sandbox need to be very carefully treated so that
  268. * they can chain into promises in the `this._win` compartment. Results need
  269. * to be cloned across; errors need to be converted.
  270. */
  271. _wrapCrossCompartmentPromise: function(sandboxPromise) {
  272. return new this._win.Promise((resolve, reject) => {
  273. sandboxPromise.then(
  274. result => resolve(Cu.cloneInto(result, this._win)),
  275. e => {
  276. let message = '' + (e.message || JSON.stringify(e) || 'IdP error');
  277. if (e.name === 'IdpLoginError') {
  278. if (typeof e.loginUrl === 'string') {
  279. this.idpLoginUrl = e.loginUrl;
  280. }
  281. reject(new this._win.DOMException(message, 'IdpLoginError'));
  282. } else {
  283. reject(new this._win.DOMException(message, 'IdpError'));
  284. }
  285. });
  286. });
  287. },
  288. /**
  289. * Wraps a promise, adding a timeout guard on it so that it can't take longer
  290. * than the specified time. Returns a promise that rejects if the timeout
  291. * elapses before `p` resolves.
  292. */
  293. _applyTimeout: function(p) {
  294. let timeout = new this._win.Promise(
  295. r => this._win.setTimeout(r, this._timeout))
  296. .then(() => {
  297. throw new this._win.DOMException('IdP timed out', 'IdpError');
  298. });
  299. return this._win.Promise.race([ timeout, p ]);
  300. }
  301. };
  302. this.PeerConnectionIdp = PeerConnectionIdp;