Bug 1622408 [wpt PR 22244] - Restore the event delegate for a CSSTransition after...
[gecko.git] / devtools / shared / security / auth.js
blob8849bbf509628813c0528582edb9566ba7010ab4
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/. */
5 "use strict";
7 var { Ci, Cc } = require("chrome");
8 var Services = require("Services");
9 var defer = require("devtools/shared/defer");
10 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
11 var { dumpn, dumpv } = DevToolsUtils;
12 loader.lazyRequireGetter(this, "prompt", "devtools/shared/security/prompt");
13 loader.lazyRequireGetter(this, "cert", "devtools/shared/security/cert");
14 loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
16 /**
17  * A simple enum-like object with keys mirrored to values.
18  * This makes comparison to a specfic value simpler without having to repeat and
19  * mis-type the value.
20  */
21 function createEnum(obj) {
22   for (const key in obj) {
23     obj[key] = key;
24   }
25   return obj;
28 /**
29  * |allowConnection| implementations can return various values as their |result|
30  * field to indicate what action to take.  By specifying these, we can
31  * centralize the common actions available, while still allowing embedders to
32  * present their UI in whatever way they choose.
33  */
34 var AuthenticationResult = (exports.AuthenticationResult = createEnum({
35   /**
36    * Close all listening sockets, and disable them from opening again.
37    */
38   DISABLE_ALL: null,
40   /**
41    * Deny the current connection.
42    */
43   DENY: null,
45   /**
46    * Additional data needs to be exchanged before a result can be determined.
47    */
48   PENDING: null,
50   /**
51    * Allow the current connection.
52    */
53   ALLOW: null,
55   /**
56    * Allow the current connection, and persist this choice for future
57    * connections from the same client.  This requires a trustable mechanism to
58    * identify the client in the future, such as the cert used during OOB_CERT.
59    */
60   ALLOW_PERSIST: null,
61 }));
63 /**
64  * An |Authenticator| implements an authentication mechanism via various hooks
65  * in the client and server debugger socket connection path (see socket.js).
66  *
67  * |Authenticator|s are stateless objects.  Each hook method is passed the state
68  * it needs by the client / server code in socket.js.
69  *
70  * Separate instances of the |Authenticator| are created for each use (client
71  * connection, server listener) in case some methods are customized by the
72  * embedder for a given use case.
73  */
74 var Authenticators = {};
76 /**
77  * The Prompt authenticator displays a server-side user prompt that includes
78  * connection details, and asks the user to verify the connection.  There are
79  * no cryptographic properties at work here, so it is up to the user to be sure
80  * that the client can be trusted.
81  */
82 var Prompt = (Authenticators.Prompt = {});
84 Prompt.mode = "PROMPT";
86 Prompt.Client = function() {};
87 Prompt.Client.prototype = {
88   mode: Prompt.mode,
90   /**
91    * When client is about to make a new connection, verify that the connection settings
92    * are compatible with this authenticator.
93    * @throws if validation requirements are not met
94    */
95   validateSettings() {},
97   /**
98    * When client has just made a new socket connection, validate the connection
99    * to ensure it meets the authenticator's policies.
100    *
101    * @param host string
102    *        The host name or IP address of the devtools server.
103    * @param port number
104    *        The port number of the devtools server.
105    * @param encryption boolean (optional)
106    *        Whether the server requires encryption.  Defaults to false.
107    * @param cert object (optional)
108    *        The server's cert details.
109    * @param s nsISocketTransport
110    *        Underlying socket transport, in case more details are needed.
111    * @return boolean
112    *         Whether the connection is valid.
113    */
114   validateConnection() {
115     return true;
116   },
118   /**
119    * Work with the server to complete any additional steps required by this
120    * authenticator's policies.
121    *
122    * Debugging commences after this hook completes successfully.
123    *
124    * @param host string
125    *        The host name or IP address of the devtools server.
126    * @param port number
127    *        The port number of the devtools server.
128    * @param encryption boolean (optional)
129    *        Whether the server requires encryption.  Defaults to false.
130    * @param transport DebuggerTransport
131    *        A transport that can be used to communicate with the server.
132    * @return A promise can be used if there is async behavior.
133    */
134   authenticate() {},
137 Prompt.Server = function() {};
138 Prompt.Server.prototype = {
139   mode: Prompt.mode,
141   /**
142    * Verify that listener settings are appropriate for this authentication mode.
143    *
144    * @param listener SocketListener
145    *        The socket listener about to be opened.
146    * @throws if validation requirements are not met
147    */
148   validateOptions() {},
150   /**
151    * Augment options on the listening socket about to be opened.
152    *
153    * @param listener SocketListener
154    *        The socket listener about to be opened.
155    * @param socket nsIServerSocket
156    *        The socket that is about to start listening.
157    */
158   augmentSocketOptions() {},
160   /**
161    * Augment the service discovery advertisement with any additional data needed
162    * to support this authentication mode.
163    *
164    * @param listener SocketListener
165    *        The socket listener that was just opened.
166    * @param advertisement object
167    *        The advertisement being built.
168    */
169   augmentAdvertisement(listener, advertisement) {
170     advertisement.authentication = Prompt.mode;
171   },
173   /**
174    * Determine whether a connection the server should be allowed or not based on
175    * this authenticator's policies.
176    *
177    * @param session object
178    *        In PROMPT mode, the |session| includes:
179    *        {
180    *          client: {
181    *            host,
182    *            port
183    *          },
184    *          server: {
185    *            host,
186    *            port
187    *          },
188    *          transport
189    *        }
190    * @return An AuthenticationResult value.
191    *         A promise that will be resolved to the above is also allowed.
192    */
193   authenticate({ client, server }) {
194     if (!Services.prefs.getBoolPref("devtools.debugger.prompt-connection")) {
195       return AuthenticationResult.ALLOW;
196     }
197     return this.allowConnection({
198       authentication: this.mode,
199       client,
200       server,
201     });
202   },
204   /**
205    * Prompt the user to accept or decline the incoming connection.  The default
206    * implementation is used unless this is overridden on a particular
207    * authenticator instance.
208    *
209    * It is expected that the implementation of |allowConnection| will show a
210    * prompt to the user so that they can allow or deny the connection.
211    *
212    * @param session object
213    *        In PROMPT mode, the |session| includes:
214    *        {
215    *          authentication: "PROMPT",
216    *          client: {
217    *            host,
218    *            port
219    *          },
220    *          server: {
221    *            host,
222    *            port
223    *          }
224    *        }
225    * @return An AuthenticationResult value.
226    *         A promise that will be resolved to the above is also allowed.
227    */
228   allowConnection: prompt.Server.defaultAllowConnection,
232  * The out-of-band (OOB) cert authenticator is based on self-signed X.509 certs
233  * at both the client and server end.
235  * The user is first prompted to verify the connection, similar to the prompt
236  * method above.  This prompt may display cert fingerprints if desired.
238  * Assuming the user approves the connection, further UI is used to assist the
239  * user in tranferring out-of-band (OOB) verification of the client's
240  * certificate.  For example, this could take the form of a QR code that the
241  * client displays which is then scanned by a camera on the server.
243  * Since it is assumed that an attacker can't forge the client's X.509 cert, the
244  * user may also choose to always allow a client, which would permit immediate
245  * connections in the future with no user interaction needed.
247  * See docs/wifi.md for details of the authentication design.
248  */
249 var OOBCert = (Authenticators.OOBCert = {});
251 OOBCert.mode = "OOB_CERT";
253 OOBCert.Client = function() {};
254 OOBCert.Client.prototype = {
255   mode: OOBCert.mode,
257   /**
258    * When client is about to make a new connection, verify that the connection settings
259    * are compatible with this authenticator.
260    * @throws if validation requirements are not met
261    */
262   validateSettings({ encryption }) {
263     if (!encryption) {
264       throw new Error(`${OOBCert.mode} authentication requires encryption.`);
265     }
266   },
268   /**
269    * When client has just made a new socket connection, validate the connection
270    * to ensure it meets the authenticator's policies.
271    *
272    * @param host string
273    *        The host name or IP address of the devtools server.
274    * @param port number
275    *        The port number of the devtools server.
276    * @param encryption boolean (optional)
277    *        Whether the server requires encryption.  Defaults to false.
278    * @param cert object (optional)
279    *        The server's cert details.
280    * @param socket nsISocketTransport
281    *        Underlying socket transport, in case more details are needed.
282    * @return boolean
283    *         Whether the connection is valid.
284    */
285   // eslint-disable-next-line no-shadow
286   validateConnection({ cert, socket }) {
287     // Step B.7
288     // Client verifies that Server's cert matches hash(ServerCert) from the
289     // advertisement
290     dumpv("Validate server cert hash");
291     const serverCert = socket.securityInfo.QueryInterface(
292       Ci.nsITransportSecurityInfo
293     ).serverCert;
294     const advertisedCert = cert;
295     if (serverCert.sha256Fingerprint != advertisedCert.sha256) {
296       dumpn("Server cert hash doesn't match advertisement");
297       return false;
298     }
299     return true;
300   },
302   /**
303    * Work with the server to complete any additional steps required by this
304    * authenticator's policies.
305    *
306    * Debugging commences after this hook completes successfully.
307    *
308    * @param host string
309    *        The host name or IP address of the devtools server.
310    * @param port number
311    *        The port number of the devtools server.
312    * @param encryption boolean (optional)
313    *        Whether the server requires encryption.  Defaults to false.
314    * @param cert object (optional)
315    *        The server's cert details.
316    * @param transport DebuggerTransport
317    *        A transport that can be used to communicate with the server.
318    * @return A promise can be used if there is async behavior.
319    */
320   // eslint-disable-next-line no-shadow
321   authenticate({ host, port, cert, transport }) {
322     const deferred = defer();
323     let oobData;
325     let activeSendDialog;
326     const closeDialog = () => {
327       // Close any prompts the client may have been showing from previous
328       // authentication steps
329       if (activeSendDialog && activeSendDialog.close) {
330         activeSendDialog.close();
331         activeSendDialog = null;
332       }
333     };
335     transport.hooks = {
336       onPacket: async packet => {
337         closeDialog();
338         const { authResult } = packet;
339         switch (authResult) {
340           case AuthenticationResult.PENDING:
341             // Step B.8
342             // Client creates hash(ClientCert) + K(random 128-bit number)
343             oobData = await this._createOOB();
344             activeSendDialog = this.sendOOB({
345               host,
346               port,
347               cert,
348               authResult,
349               oob: oobData,
350             });
351             break;
352           case AuthenticationResult.ALLOW:
353             // Step B.12
354             // Client verifies received value matches K
355             if (packet.k != oobData.k) {
356               transport.close(new Error("Auth secret mismatch"));
357               return;
358             }
359             // Step B.13
360             // Debugging begins
361             transport.hooks = null;
362             deferred.resolve(transport);
363             break;
364           case AuthenticationResult.ALLOW_PERSIST:
365             // Server previously persisted Client as allowed
366             // Step C.5
367             // Debugging begins
368             transport.hooks = null;
369             deferred.resolve(transport);
370             break;
371           default:
372             transport.close(new Error("Invalid auth result: " + authResult));
373             break;
374         }
375       },
376       onClosed(reason) {
377         closeDialog();
378         // Transport died before auth completed
379         transport.hooks = null;
380         deferred.reject(reason);
381       },
382     };
383     transport.ready();
384     return deferred.promise;
385   },
387   /**
388    * Create the package of data that needs to be transferred across the OOB
389    * channel.
390    */
391   async _createOOB() {
392     const clientCert = await cert.local.getOrCreate();
393     return {
394       sha256: clientCert.sha256Fingerprint,
395       k: this._createRandom(),
396     };
397   },
399   _createRandom() {
400     // 16 bytes / 128 bits
401     const length = 16;
402     const rng = Cc["@mozilla.org/security/random-generator;1"].createInstance(
403       Ci.nsIRandomGenerator
404     );
405     const bytes = rng.generateRandomBytes(length);
406     return bytes.map(byte => byte.toString(16)).join("");
407   },
409   /**
410    * Send data across the OOB channel to the server to authenticate the devices.
411    *
412    * @param host string
413    *        The host name or IP address of the devtools server.
414    * @param port number
415    *        The port number of the devtools server.
416    * @param cert object (optional)
417    *        The server's cert details.
418    * @param authResult AuthenticationResult
419    *        Authentication result sent from the server.
420    * @param oob object (optional)
421    *        The token data to be transferred during OOB_CERT step 8:
422    *        * sha256: hash(ClientCert)
423    *        * k     : K(random 128-bit number)
424    * @return object containing:
425    *         * close: Function to hide the notification
426    */
427   sendOOB: prompt.Client.defaultSendOOB,
430 OOBCert.Server = function() {};
431 OOBCert.Server.prototype = {
432   mode: OOBCert.mode,
434   /**
435    * Verify that listener settings are appropriate for this authentication mode.
436    *
437    * @param listener SocketListener
438    *        The socket listener about to be opened.
439    * @throws if validation requirements are not met
440    */
441   validateOptions(listener) {
442     if (!listener.encryption) {
443       throw new Error(OOBCert.mode + " authentication requires encryption.");
444     }
445   },
447   /**
448    * Augment options on the listening socket about to be opened.
449    *
450    * @param listener SocketListener
451    *        The socket listener about to be opened.
452    * @param socket nsIServerSocket
453    *        The socket that is about to start listening.
454    */
455   augmentSocketOptions(listener, socket) {
456     const requestCert = Ci.nsITLSServerSocket.REQUIRE_ALWAYS;
457     socket.setRequestClientCertificate(requestCert);
458   },
460   /**
461    * Augment the service discovery advertisement with any additional data needed
462    * to support this authentication mode.
463    *
464    * @param listener SocketListener
465    *        The socket listener that was just opened.
466    * @param advertisement object
467    *        The advertisement being built.
468    */
469   augmentAdvertisement(listener, advertisement) {
470     advertisement.authentication = OOBCert.mode;
471     // Step A.4
472     // Server announces itself via service discovery
473     // Announcement contains hash(ServerCert) as additional data
474     advertisement.cert = listener.cert;
475   },
477   /**
478    * Determine whether a connection the server should be allowed or not based on
479    * this authenticator's policies.
480    *
481    * @param session object
482    *        In OOB_CERT mode, the |session| includes:
483    *        {
484    *          client: {
485    *            host,
486    *            port,
487    *            cert: {
488    *              sha256
489    *            },
490    *          },
491    *          server: {
492    *            host,
493    *            port,
494    *            cert: {
495    *              sha256
496    *            }
497    *          },
498    *          transport
499    *        }
500    * @return An AuthenticationResult value.
501    *         A promise that will be resolved to the above is also allowed.
502    */
503   async authenticate({ client, server, transport }) {
504     // Step B.3 / C.3
505     // TLS connection established, authentication begins
506     const storageKey = `devtools.auth.${this.mode}.approved-clients`;
507     const approvedClients = (await asyncStorage.getItem(storageKey)) || {};
508     // Step C.4
509     // Server sees that ClientCert is from a known client via hash(ClientCert)
510     if (approvedClients[client.cert.sha256]) {
511       const authResult = AuthenticationResult.ALLOW_PERSIST;
512       transport.send({ authResult });
513       // Step C.5
514       // Debugging begins
515       return authResult;
516     }
518     // Step B.4
519     // Server sees that ClientCert is from a unknown client
520     // Tell client they are unknown and should display OOB client UX
521     transport.send({
522       authResult: AuthenticationResult.PENDING,
523     });
525     // Step B.5
526     // User is shown a Allow / Deny / Always Allow prompt on the Server
527     // with Client name and hash(ClientCert)
528     const authResult = await this.allowConnection({
529       authentication: this.mode,
530       client,
531       server,
532     });
534     switch (authResult) {
535       case AuthenticationResult.ALLOW_PERSIST:
536       case AuthenticationResult.ALLOW:
537         // Further processing
538         break;
539       default:
540         // Abort for any negative results
541         return authResult;
542     }
544     // Examine additional data for authentication
545     const oob = await this.receiveOOB();
546     if (!oob) {
547       dumpn("Invalid OOB data received");
548       return AuthenticationResult.DENY;
549     }
551     const { sha256, k } = oob;
552     // The OOB auth prompt should have transferred:
553     // hash(ClientCert) + K(random 128-bit number)
554     // from the client.
555     if (!sha256 || !k) {
556       dumpn("Invalid OOB data received");
557       return AuthenticationResult.DENY;
558     }
560     // Step B.10
561     // Server verifies that Client's cert matches hash(ClientCert) from
562     // out-of-band channel
563     if (client.cert.sha256 != sha256) {
564       dumpn("Client cert hash doesn't match OOB data");
565       return AuthenticationResult.DENY;
566     }
568     // Step B.11
569     // Server sends K to Client over TLS connection
570     transport.send({ authResult, k });
572     // Persist Client if we want to always allow in the future
573     if (authResult === AuthenticationResult.ALLOW_PERSIST) {
574       approvedClients[client.cert.sha256] = true;
575       await asyncStorage.setItem(storageKey, approvedClients);
576     }
578     // Client may decide to abort if K does not match.
579     // Server's portion of authentication is now complete.
581     // Step B.13
582     // Debugging begins
583     return authResult;
584   },
586   /**
587    * Prompt the user to accept or decline the incoming connection. The default
588    * implementation is used unless this is overridden on a particular
589    * authenticator instance.
590    *
591    * It is expected that the implementation of |allowConnection| will show a
592    * prompt to the user so that they can allow or deny the connection.
593    *
594    * @param session object
595    *        In OOB_CERT mode, the |session| includes:
596    *        {
597    *          authentication: "OOB_CERT",
598    *          client: {
599    *            host,
600    *            port,
601    *            cert: {
602    *              sha256
603    *            },
604    *          },
605    *          server: {
606    *            host,
607    *            port,
608    *            cert: {
609    *              sha256
610    *            }
611    *          }
612    *        }
613    * @return An AuthenticationResult value.
614    *         A promise that will be resolved to the above is also allowed.
615    */
616   allowConnection: prompt.Server.defaultAllowConnection,
618   /**
619    * The user must transfer some data through some out of band mechanism from
620    * the client to the server to authenticate the devices.
621    *
622    * @return An object containing:
623    *         * sha256: hash(ClientCert)
624    *         * k     : K(random 128-bit number)
625    *         A promise that will be resolved to the above is also allowed.
626    */
627   receiveOOB: prompt.Server.defaultReceiveOOB,
630 exports.Authenticators = {
631   get(mode) {
632     if (!mode) {
633       mode = Prompt.mode;
634     }
635     for (const key in Authenticators) {
636       const auth = Authenticators[key];
637       if (auth.mode === mode) {
638         return auth;
639       }
640     }
641     throw new Error("Unknown authenticator mode: " + mode);
642   },