summaryrefslogtreecommitdiffstats
path: root/dom/media/PeerConnectionIdp.sys.mjs
blob: 2be6643b0666b0cd9244ad4d2bf90df740add9b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
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]);
  },
};