Bug 1103120 - Part 14: Client: Send OOB data to server. r=past
authorJ. Ryan Stinnett <jryans@gmail.com>
Mon, 26 Jan 2015 12:47:13 -0600
changeset 239176 eba034ecc1287e8c1af9d46eb76088b2fdf08aec
parent 239175 869fb4fbada04dece6545789f59e2b68d54e03ad
child 239177 77d19984fec7f0bce8d13dc07f65f6249e32e523
push id487
push userbcampen@mozilla.com
push dateMon, 26 Jan 2015 23:32:56 +0000
reviewerspast
bugs1103120
milestone38.0a1
Bug 1103120 - Part 14: Client: Send OOB data to server. r=past
toolkit/devtools/security/auth.js
toolkit/devtools/security/prompt.js
toolkit/devtools/security/socket.js
toolkit/locales/en-US/chrome/global/devtools/debugger.properties
--- a/toolkit/devtools/security/auth.js
+++ b/toolkit/devtools/security/auth.js
@@ -1,21 +1,23 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 "use strict";
 
-let { Ci } = require("chrome");
+let { Ci, Cc } = require("chrome");
 let Services = require("Services");
 let promise = require("promise");
 loader.lazyRequireGetter(this, "prompt",
   "devtools/toolkit/security/prompt");
+loader.lazyRequireGetter(this, "cert",
+  "devtools/toolkit/security/cert");
 
 /**
  * A simple enum-like object with keys mirrored to values.
  * This makes comparison to a specfic value simpler without having to repeat and
  * mis-type the value.
  */
 function createEnum(obj) {
   for (let key in obj) {
@@ -238,32 +240,109 @@ OOBCert.Client.prototype = {
    * Debugging commences after this hook completes successfully.
    *
    * @param host string
    *        The host name or IP address of the debugger server.
    * @param port number
    *        The port number of the debugger server.
    * @param encryption boolean (optional)
    *        Whether the server requires encryption.  Defaults to false.
+   * @param cert object (optional)
+   *        The server's cert details.
    * @param transport DebuggerTransport
    *        A transport that can be used to communicate with the server.
    * @return A promise can be used if there is async behavior.
    */
-  authenticate({ transport }) {
+  authenticate({ host, port, cert, transport }) {
     let deferred = promise.defer();
+    let oobData;
+
+    let activeSendDialog;
+    let closeDialog = () => {
+      // Close any prompts the client may have been showing from previous
+      // authentication steps
+      if (activeSendDialog && activeSendDialog.close) {
+        activeSendDialog.close();
+        activeSendDialog = null;
+      }
+    };
+
     transport.hooks = {
-      onPacket(packet) {
+      onPacket: Task.async(function*(packet) {
+        closeDialog();
         let { authResult } = packet;
-        // TODO: Examine the result
+        switch (authResult) {
+          case AuthenticationResult.PENDING:
+            // Step B.8
+            // Client creates hash(ClientCert) + K(random 128-bit number)
+            oobData = yield this._createOOB();
+            activeSendDialog = this.sendOOB({
+              host,
+              port,
+              cert,
+              authResult,
+              oob: oobData
+            });
+            break;
+          default:
+            transport.close(new Error("Invalid auth result: " + authResult));
+            return;
+        }
+      }.bind(this)),
+      onClosed(reason) {
+        closeDialog();
+        // Transport died before auth completed
+        transport.hooks = null;
+        deferred.reject(reason);
       }
     };
     transport.ready();
     return deferred.promise;
   },
 
+  /**
+   * Create the package of data that needs to be transferred across the OOB
+   * channel.
+   */
+  _createOOB: Task.async(function*() {
+    let clientCert = yield cert.local.getOrCreate();
+    return {
+      sha256: clientCert.sha256Fingerprint,
+      k: this._createRandom()
+    };
+  }),
+
+  _createRandom() {
+    const length = 16; // 16 bytes / 128 bits
+    let rng = Cc["@mozilla.org/security/random-generator;1"]
+              .createInstance(Ci.nsIRandomGenerator);
+    let bytes = rng.generateRandomBytes(length);
+    return [for (byte of bytes) byte.toString(16)].join("");
+  },
+
+  /**
+   * Send data across the OOB channel to the server to authenticate the devices.
+   *
+   * @param host string
+   *        The host name or IP address of the debugger server.
+   * @param port number
+   *        The port number of the debugger server.
+   * @param cert object (optional)
+   *        The server's cert details.
+   * @param authResult AuthenticationResult
+   *        Authentication result sent from the server.
+   * @param oob object (optional)
+   *        The token data to be transferred during OOB_CERT step 8:
+   *        * sha256: hash(ClientCert)
+   *        * k     : K(random 128-bit number)
+   * @return object containing:
+   *         * close: Function to hide the notification
+   */
+  sendOOB: prompt.Client.defaultSendOOB,
+
 };
 
 OOBCert.Server = function() {};
 OOBCert.Server.prototype = {
 
   mode: OOBCert.mode,
 
   /**
--- a/toolkit/devtools/security/prompt.js
+++ b/toolkit/devtools/security/prompt.js
@@ -1,31 +1,111 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 "use strict";
 
+let { Ci } = require("chrome");
 let Services = require("Services");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 loader.lazyRequireGetter(this, "DebuggerSocket",
   "devtools/toolkit/security/socket", true);
 loader.lazyRequireGetter(this, "AuthenticationResult",
   "devtools/toolkit/security/auth", true);
 
 DevToolsUtils.defineLazyGetter(this, "bundle", () => {
   const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
   return Services.strings.createBundle(DBG_STRINGS_URI);
 });
 
+let Client = exports.Client = {};
 let Server = exports.Server = {};
 
 /**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually.  For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * @param host string
+ *        The host name or IP address of the debugger server.
+ * @param port number
+ *        The port number of the debugger server.
+ * @param cert object (optional)
+ *        The server's cert details.
+ * @param authResult AuthenticationResult
+ *        Authentication result sent from the server.
+ * @param oob object (optional)
+ *        The token data to be transferred during OOB_CERT step 8:
+ *        * sha256: hash(ClientCert)
+ *        * k     : K(random 128-bit number)
+ * @return object containing:
+ *         * close: Function to hide the notification
+ */
+Client.defaultSendOOB = ({ authResult, oob }) => {
+  // Only show in the PENDING state
+  if (authResult != AuthenticationResult.PENDING) {
+    throw new Error("Expected PENDING result, got " + authResult);
+  }
+  let title = bundle.GetStringFromName("clientSendOOBTitle");
+  let header = bundle.GetStringFromName("clientSendOOBHeader");
+  let hashMsg = bundle.formatStringFromName("clientSendOOBHash",
+                                            [oob.sha256], 1);
+  let token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
+  let tokenMsg = bundle.formatStringFromName("clientSendOOBToken",
+                                             [token], 1);
+  let msg =`${header}\n\n${hashMsg}\n${tokenMsg}`;
+  let prompt = Services.prompt;
+  let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_CANCEL;
+
+  // Listen for the window our prompt opens, so we can close it programatically
+  let promptWindow;
+  let windowListener = {
+    onOpenWindow(xulWindow) {
+      let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindow);
+      win.addEventListener("load", function listener() {
+        win.removeEventListener("load", listener, false);
+        if (win.document.documentElement.getAttribute("id") != "commonDialog") {
+          return;
+        }
+        // Found the window
+        promptWindow = win;
+        Services.wm.removeListener(windowListener);
+      }, false);
+    },
+    onCloseWindow() {},
+    onWindowTitleChange() {}
+  };
+  Services.wm.addListener(windowListener);
+
+  // nsIPrompt is typically a blocking API, so |executeSoon| to get around this
+  DevToolsUtils.executeSoon(() => {
+    prompt.confirmEx(null, title, msg, flags, null, null, null, null,
+                     { value: false });
+  });
+
+  return {
+    close() {
+      if (!promptWindow) {
+        return;
+      }
+      promptWindow.document.documentElement.acceptDialog();
+      promptWindow = null;
+    }
+  };
+};
+
+/**
  * Prompt the user to accept or decline the incoming connection. This is the
  * default implementation that products embedding the debugger server may
  * choose to override.  This can be overridden via |allowConnection| on the
  * socket's authenticator instance.
  *
  * @return An AuthenticationResult value.
  *         A promise that will be resolved to the above is also allowed.
  */
--- a/toolkit/devtools/security/socket.js
+++ b/toolkit/devtools/security/socket.js
@@ -1,17 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 "use strict";
 
-let { Ci, Cc, CC, Cr } = require("chrome");
+let { Ci, Cc, CC, Cr, Cu } = require("chrome");
 
 // Ensure PSM is initialized to support TLS sockets
 Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
 
 let Services = require("Services");
 let promise = require("promise");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let { dumpn, dumpv } = DevToolsUtils;
@@ -61,30 +61,33 @@ let DebuggerSocket = {};
  *        The host name or IP address of the debugger server.
  * @param port number
  *        The port number of the debugger server.
  * @param encryption boolean (optional)
  *        Whether the server requires encryption.  Defaults to false.
  * @param authenticator Authenticator (optional)
  *        |Authenticator| instance matching the mode in use by the server.
  *        Defaults to a PROMPT instance if not supplied.
+ * @param cert object (optional)
+ *        The server's cert details.  Used with OOB_CERT authentication.
  * @return promise
  *         Resolved to a DebuggerTransport instance.
  */
 DebuggerSocket.connect = Task.async(function*(settings) {
-  let { host, port, encryption, authenticator } = settings;
+  let { host, port, encryption, authenticator, cert } = settings;
   let transport = yield _getTransport(settings);
 
   // Default to PROMPT |Authenticator| instance if not supplied
   authenticator = authenticator || new (Authenticators.get().Client)();
 
   yield authenticator.authenticate({
     host,
     port,
     encryption,
+    cert,
     transport
   });
   return transport;
 });
 
 /**
  * Try very hard to create a DevTools transport, potentially making several
  * connect attempts in the process.
--- a/toolkit/locales/en-US/chrome/global/devtools/debugger.properties
+++ b/toolkit/locales/en-US/chrome/global/devtools/debugger.properties
@@ -30,8 +30,23 @@ remoteIncomingPromptServerEndpoint=Serve
 # LOCALIZATION NOTE (remoteIncomingPromptFooter): Footer displayed on the
 # dialog that prompts the user to allow the incoming connection.
 remoteIncomingPromptFooter=Allow connection?
 
 # LOCALIZATION NOTE (remoteIncomingPromptDisable): The label displayed on the
 # third button in the incoming connection dialog that lets the user disable the
 # remote debugger server.
 remoteIncomingPromptDisable=Disable
+
+# LOCALIZATION NOTE (clientSendOOBTitle): The title displayed on the dialog that
+# instructs the user to transfer an authentication token to the server.
+clientSendOOBTitle=Client Identification
+# LOCALIZATION NOTE (clientSendOOBHeader): Header displayed on the dialog that
+# instructs the user to transfer an authentication token to the server.
+clientSendOOBHeader=The endpoint you are connecting to needs more information to authenticate this connection.  Please provide the token below in the prompt that appears on the other end.
+# LOCALIZATION NOTE (clientSendOOBHash): Part of the dialog that instructs the
+# user to transfer an authentication token to the server.
+# %1$S: The client's cert fingerprint
+clientSendOOBHash=My Cert: %1$S
+# LOCALIZATION NOTE (clientSendOOBToken): Part of the dialog that instructs the
+# user to transfer an authentication token to the server.
+# %1$S: The authentication token that the user will transfer.
+clientSendOOBToken=Token: %1$S