[tbb-commits] [tor-browser/tor-browser-68.4.1esr-9.5-1] Bug 30237: Add v3 onion services client authentication prompt

sysrqb at torproject.org sysrqb at torproject.org
Wed Feb 5 00:04:02 UTC 2020


commit 6cac185d0c10e4f26ca7eaf000c31fae36d13bfc
Author: Kathy Brade <brade at pearlcrescent.com>
Date:   Tue Nov 12 16:11:05 2019 -0500

    Bug 30237: Add v3 onion services client authentication prompt
    
    When Tor informs the browser that client authentication is needed,
    temporarily load about:blank instead of about:neterror and prompt
    for the user's key.
    
    If a correctly formatted key is entered, use Tor's ONION_CLIENT_AUTH_ADD
    control port command to add the key (via Torbutton's control port
    module) and reload the page.
    
    If the user cancels the prompt, display the standard about:neterror
    "Unable to connect" page. This requires a small change to
    browser/actors/NetErrorChild.jsm to account for the fact that the
    docShell no longer has the failedChannel information. The failedChannel
    is used to extract TLS-related error info, which is not applicable
    in the case of a canceled .onion authentication prompt.
    
    Add a leaveOpen option to PopupNotifications.show so we can display
    error messages within the popup notification doorhanger without
    closing the prompt.
    
    Add support for onion services strings to the TorStrings module.
    
    Add support for Tor extended SOCKS errors (Tor proposal 304) to the
    socket transport and SOCKS layers. Improved display of all of these
    errors will be implemented as part of bug 30025.
---
 browser/actors/NetErrorChild.jsm                   |   2 +-
 browser/base/content/browser.js                    |  10 +
 browser/base/content/browser.xul                   |   3 +
 browser/base/content/tab-content.js                |   5 +
 browser/components/moz.build                       |   1 +
 .../content/authNotificationIcon.inc.xul           |   6 +
 .../onionservices/content/authPopup.inc.xul        |  14 +
 .../components/onionservices/content/authPrompt.js | 290 +++++++++++++++++++++
 .../components/onionservices/content/authUtil.jsm  |  32 +++
 .../onionservices/content/onionservices.css        |  69 +++++
 browser/components/onionservices/jar.mn            |   4 +
 browser/components/onionservices/moz.build         |   1 +
 browser/modules/TorStrings.jsm                     |  74 +++++-
 browser/themes/shared/notification-icons.inc.css   |   3 +
 docshell/base/nsDocShell.cpp                       |  62 +++++
 dom/ipc/BrowserParent.cpp                          |  23 ++
 dom/ipc/BrowserParent.h                            |   3 +
 dom/ipc/PBrowser.ipdl                              |   8 +
 js/xpconnect/src/xpc.msg                           |   8 +
 netwerk/base/nsSocketTransport2.cpp                |   6 +
 netwerk/socket/nsSOCKSIOLayer.cpp                  |  30 +++
 toolkit/modules/PopupNotifications.jsm             |   6 +
 xpcom/base/ErrorList.py                            |  18 ++
 23 files changed, 672 insertions(+), 6 deletions(-)

diff --git a/browser/actors/NetErrorChild.jsm b/browser/actors/NetErrorChild.jsm
index 173a93ad5f29..7834f7168a42 100644
--- a/browser/actors/NetErrorChild.jsm
+++ b/browser/actors/NetErrorChild.jsm
@@ -840,7 +840,7 @@ class NetErrorChild extends ActorChild {
     }
     if (this.isAboutNetError(win.document)) {
       let docShell = win.docShell;
-      if (docShell) {
+      if (docShell && docShell.failedChannel) {
         let { securityInfo } = docShell.failedChannel;
         // We don't have a securityInfo when this is for example a DNS error.
         if (securityInfo) {
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index b213d72551fc..11857f16b8e9 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -216,6 +216,11 @@ XPCOMUtils.defineLazyScriptGetter(
 );
 XPCOMUtils.defineLazyScriptGetter(
   this,
+  ["OnionAuthPrompt"],
+  "chrome://browser/content/onionservices/authPrompt.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+  this,
   "gEditItemOverlay",
   "chrome://browser/content/places/editBookmark.js"
 );
@@ -1855,6 +1860,9 @@ var gBrowserInit = {
     // Init the SecuritySettingsButton
     SecurityLevelButton.init();
 
+    // Init the OnionAuthPrompt
+    OnionAuthPrompt.init();
+
     // Certain kinds of automigration rely on this notification to complete
     // their tasks BEFORE the browser window is shown. SessionStore uses it to
     // restore tabs into windows AFTER important parts like gMultiProcessBrowser
@@ -2488,6 +2496,8 @@ var gBrowserInit = {
 
     SecurityLevelButton.uninit();
 
+    OnionAuthPrompt.uninit();
+
     gAccessibilityServiceIndicator.uninit();
 
     AccessibilityRefreshBlocker.uninit();
diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul
index 8e47fd36fb75..d2f72eea8edb 100644
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -33,6 +33,7 @@
 <?xml-stylesheet href="chrome://browser/skin/places/editBookmark.css" type="text/css"?>
 <?xml-stylesheet href="chrome://torbutton/skin/tor-circuit-display.css" type="text/css"?>
 <?xml-stylesheet href="chrome://torbutton/skin/torbutton.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/onionservices/onionservices.css" type="text/css"?>
 
 # All DTD information is stored in a separate file so that it can be shared by
 # hiddenWindow.xul.
@@ -623,6 +624,7 @@
 #include ../../components/controlcenter/content/protectionsPanel.inc.xul
 #include ../../components/downloads/content/downloadsPanel.inc.xul
 #include ../../components/securitylevel/content/securityLevelPanel.inc.xul
+#include ../../components/onionservices/content/authPopup.inc.xul
 #include browser-allTabsMenu.inc.xul
 
     <hbox id="downloads-animation-container" mousethrough="always">
@@ -922,6 +924,7 @@
                          tooltiptext="&urlbar.indexedDBNotificationAnchor.tooltip;"/>
                   <image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button"
                          tooltiptext="&urlbar.passwordNotificationAnchor.tooltip;"/>
+#include ../../components/onionservices/content/authNotificationIcon.inc.xul
                   <stack id="plugins-notification-icon" class="notification-anchor-icon" role="button" align="center"
                          tooltiptext="&urlbar.pluginsNotificationAnchor.tooltip;">
                     <image class="plugin-icon" />
diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js
index 57f3925a1ca5..14311242f5d1 100644
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -19,6 +19,9 @@ ChromeUtils.defineModuleGetter(
   "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm"
 );
+var { OnionAuthUtil } = ChromeUtils.import(
+  "chrome://browser/content/onionservices/authUtil.jsm"
+);
 
 var { ActorManagerChild } = ChromeUtils.import(
   "resource://gre/modules/ActorManagerChild.jsm"
@@ -118,3 +121,5 @@ addEventListener("MozAfterPaint", function onFirstNonBlankPaint() {
   removeEventListener("MozAfterPaint", onFirstNonBlankPaint);
   sendAsyncMessage("Browser:FirstNonBlankPaint");
 });
+
+OnionAuthUtil.addCancelMessageListener(this, docShell);
diff --git a/browser/components/moz.build b/browser/components/moz.build
index c0c9629cac65..3331a51fbe88 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -42,6 +42,7 @@ DIRS += [
     'library',
     'migration',
     'newtab',
+    'onionservices',
     'originattributes',
     'places',
     'pocket',
diff --git a/browser/components/onionservices/content/authNotificationIcon.inc.xul b/browser/components/onionservices/content/authNotificationIcon.inc.xul
new file mode 100644
index 000000000000..91274d612739
--- /dev/null
+++ b/browser/components/onionservices/content/authNotificationIcon.inc.xul
@@ -0,0 +1,6 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<image id="tor-clientauth-notification-icon"
+       class="notification-anchor-icon tor-clientauth-icon"
+       role="button"
+       tooltiptext="&torbutton.onionServices.authPrompt.tooltip;"/>
diff --git a/browser/components/onionservices/content/authPopup.inc.xul b/browser/components/onionservices/content/authPopup.inc.xul
new file mode 100644
index 000000000000..d327e4c6a88d
--- /dev/null
+++ b/browser/components/onionservices/content/authPopup.inc.xul
@@ -0,0 +1,14 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<popupnotification id="tor-clientauth-notification" hidden="true">
+  <popupnotificationcontent orient="vertical">
+    <description id="tor-clientauth-notification-desc"/>
+    <label id="tor-clientauth-notification-learnmore"
+           class="text-link popup-notification-learnmore-link"
+           is="text-link"/>
+    <html:div>
+      <html:input id="tor-clientauth-notification-key" type="password"/>
+      <html:div id="tor-clientauth-warning"/>
+    </html:div>
+  </popupnotificationcontent>
+</popupnotification>
diff --git a/browser/components/onionservices/content/authPrompt.js b/browser/components/onionservices/content/authPrompt.js
new file mode 100644
index 000000000000..2d4ebcafd688
--- /dev/null
+++ b/browser/components/onionservices/content/authPrompt.js
@@ -0,0 +1,290 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm",
+  CommonUtils: "resource://services-common/utils.js",
+  TorStrings: "resource:///modules/TorStrings.jsm",
+});
+
+const OnionAuthPrompt = (function() {
+  // OnionServicesAuthPrompt objects run within the main/chrome process.
+  function OnionServicesAuthPrompt(aBrowser, aFailedURI, aOnionName) {
+    this._browser = aBrowser;
+    this._failedURI = aFailedURI;
+    this._onionName = aOnionName;
+  }
+
+  OnionServicesAuthPrompt.prototype = {
+    show(aWarningMessage) {
+      let mainAction = {
+        label: TorStrings.onionServices.authPrompt.done,
+        accessKey: TorStrings.onionServices.authPrompt.doneAccessKey,
+        leaveOpen: true, // Callback is responsible for closing the notification.
+        callback: this._onDone.bind(this),
+      };
+
+      let dialogBundle = Services.strings.createBundle(
+                           "chrome://global/locale/dialog.properties");
+
+      let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
+      if (!cancelAccessKey)
+        cancelAccessKey = "c"; // required by PopupNotifications.show()
+
+      let cancelAction = {
+        label: dialogBundle.GetStringFromName("button-cancel"),
+        accessKey: cancelAccessKey,
+        callback: this._onCancel.bind(this),
+      };
+
+      let _this = this;
+      let options = {
+        autofocus: true,
+        hideClose: true,
+        persistent: true,
+        removeOnDismissal: false,
+        eventCallback(aTopic) {
+          if (aTopic === "showing") {
+            _this._onPromptShowing(aWarningMessage);
+          } else if (aTopic === "shown") {
+            _this._onPromptShown();
+          } else if (aTopic === "removed") {
+            _this._onPromptRemoved();
+          }
+        }
+      };
+
+      this._prompt = PopupNotifications.show(this._browser,
+                       OnionAuthUtil.string.notificationID, "",
+                       OnionAuthUtil.string.anchorID,
+                       mainAction, [cancelAction], options);
+    },
+
+    _onPromptShowing(aWarningMessage) {
+      let xulDoc = this._browser.ownerDocument;
+      let descElem = xulDoc.getElementById(OnionAuthUtil.string.descriptionID);
+      if (descElem) {
+        // Handle replacement of the onion name within the localized
+        // string ourselves so we can show the onion name as bold text.
+        // We do this by splitting the localized string and creating
+        // several HTML <span> elements.
+        while (descElem.firstChild)
+          descElem.removeChild(descElem.firstChild);
+
+        let fmtString = TorStrings.onionServices.authPrompt.description;
+        let prefix = "";
+        let suffix = "";
+        const kToReplace = "%S";
+        let idx = fmtString.indexOf(kToReplace);
+        if (idx < 0) {
+          prefix = fmtString;
+        } else {
+          prefix = fmtString.substring(0, idx);
+          suffix = fmtString.substring(idx + kToReplace.length);
+        }
+
+        const kHTMLNS = "http://www.w3.org/1999/xhtml";
+        let span = xulDoc.createElementNS(kHTMLNS, "span");
+        span.textContent = prefix;
+        descElem.appendChild(span);
+        span = xulDoc.createElementNS(kHTMLNS, "span");
+        span.id = OnionAuthUtil.string.onionNameSpanID;
+        span.textContent = this._onionName;
+        descElem.appendChild(span);
+        span = xulDoc.createElementNS(kHTMLNS, "span");
+        span.textContent = suffix;
+        descElem.appendChild(span);
+      }
+
+      // Set "Learn More" label and href.
+      let learnMoreElem = xulDoc.getElementById(OnionAuthUtil.string.learnMoreID);
+      if (learnMoreElem) {
+        learnMoreElem.setAttribute("value", TorStrings.onionServices.learnMore);
+        learnMoreElem.setAttribute("href", TorStrings.onionServices.learnMoreURL);
+      }
+
+      this._showWarning(aWarningMessage);
+    },
+
+    _onPromptShown() {
+      let keyElem = this._getKeyElement();
+      if (keyElem) {
+        keyElem.setAttribute("placeholder",
+                          TorStrings.onionServices.authPrompt.keyPlaceholder);
+        this._boundOnKeyFieldKeyPress = this._onKeyFieldKeyPress.bind(this);
+        this._boundOnKeyFieldInput = this._onKeyFieldInput.bind(this);
+        keyElem.addEventListener("keypress", this._boundOnKeyFieldKeyPress);
+        keyElem.addEventListener("input", this._boundOnKeyFieldInput);
+        keyElem.focus();
+      }
+    },
+
+    _onPromptRemoved() {
+      if (this._boundOnKeyFieldKeyPress) {
+        let keyElem = this._getKeyElement();
+        if (keyElem) {
+          keyElem.value = "";
+          keyElem.removeEventListener("keypress",
+                                      this._boundOnKeyFieldKeyPress);
+          this._boundOnKeyFieldKeyPress = undefined;
+          keyElem.removeEventListener("input", this._boundOnKeyFieldInput);
+          this._boundOnKeyFieldInput = undefined;
+        }
+      }
+    },
+
+    _onKeyFieldKeyPress(aEvent) {
+      if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
+        this._onDone();
+      } else if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+        this._prompt.remove();
+        this._onCancel();
+      }
+    },
+
+    _onKeyFieldInput(aEvent) {
+      this._showWarning(undefined); // Remove the warning.
+    },
+
+    _onDone() {
+      let keyElem = this._getKeyElement();
+      if (!keyElem)
+        return;
+
+      let base64key = this._keyToBase64(keyElem.value);
+      if (!base64key) {
+        this._showWarning(TorStrings.onionServices.authPrompt.invalidKey);
+        return;
+      }
+
+      this._prompt.remove();
+
+      // Use Torbutton's controller module to add the private key to Tor.
+      let controllerFailureMsg =
+        TorStrings.onionServices.authPrompt.failedToSetKey;
+      try {
+        let { controller } =
+            Cu.import("resource://torbutton/modules/tor-control-port.js", {});
+        let torController = controller(aError => {
+          this.show(controllerFailureMsg);
+        });
+        let onionAddr = this._onionName.toLowerCase().replace(/\.onion$/, "");
+        torController.onionAuthAdd(onionAddr, base64key)
+        .then(aResponse => {
+          // Success! Reload the page.
+          this._browser.messageManager.sendAsyncMessage("Browser:Reload", {});
+        })
+        .catch(aError => {
+          if (aError.torMessage)
+            this.show(aError.torMessage);
+          else
+            this.show(controllerFailureMsg);
+        });
+      } catch (e) {
+        this.show(controllerFailureMsg);
+      }
+    },
+
+    _onCancel() {
+      // Arrange for an error page to be displayed.
+      this._browser.messageManager.sendAsyncMessage(
+                               OnionAuthUtil.string.authPromptCanceledMessage,
+                               {failedURI: this._failedURI.spec});
+    },
+
+    _getKeyElement() {
+      let xulDoc = this._browser.ownerDocument;
+      return xulDoc.getElementById(OnionAuthUtil.string.keyElementID);
+    },
+
+    _showWarning(aWarningMessage) {
+      let xulDoc = this._browser.ownerDocument;
+      let warningElem =
+                 xulDoc.getElementById(OnionAuthUtil.string.warningElementID);
+      let keyElem = this._getKeyElement();
+      if (warningElem) {
+        if (aWarningMessage) {
+          warningElem.textContent = aWarningMessage;
+          warningElem.removeAttribute("hidden");
+          if (keyElem)
+            keyElem.className = "invalid";
+        } else {
+          warningElem.setAttribute("hidden", "true");
+          if (keyElem)
+            keyElem.className = "";
+        }
+      }
+    },
+
+    // Returns undefined if the key is the wrong length or format.
+    _keyToBase64(aKeyString) {
+      if (!aKeyString)
+        return undefined;
+
+      let base64key;
+      if (aKeyString.length == 52) {
+        // The key is probably base32-encoded. Attempt to decode.
+        let rawKey;
+        try {
+          rawKey = CommonUtils.decodeBase32(aKeyString);
+        } catch (e) {}
+
+        if (rawKey) try {
+          base64key = btoa(rawKey);
+        } catch (e) {}
+      } else if ((aKeyString.length == 44) &&
+                 /^[a-zA-Z0-9+/]*=*$/.test(aKeyString)) {
+        // The key appears to be a correctly formatted base64 value. If not,
+        // tor will return an error when we try to add the key via the
+        // control port.
+        base64key = aKeyString;
+      }
+
+      return base64key;
+    },
+  };
+
+  let retval = {
+    init() {
+      Services.obs.addObserver(this, OnionAuthUtil.string.authPromptTopic);
+    },
+
+    uninit() {
+      Services.obs.removeObserver(this, OnionAuthUtil.string.authPromptTopic);
+    },
+
+    // aSubject is the DOM Window or browser where the prompt should be shown.
+    // aData contains the .onion name.
+    observe(aSubject, aTopic, aData) {
+      if (aTopic != OnionAuthUtil.string.authPromptTopic) {
+        return;
+      }
+
+      let browser;
+      if (aSubject instanceof Ci.nsIDOMWindow) {
+        let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+        browser = contentWindow.docShell.chromeEventHandler;
+      } else {
+        browser = aSubject.QueryInterface(Ci.nsIBrowser);
+      }
+
+      if (!gBrowser.browsers.some(aBrowser => aBrowser == browser)) {
+        return; // This window does not contain the subject browser; ignore.
+      }
+
+      let failedURI = browser.currentURI;
+      let authPrompt = new OnionServicesAuthPrompt(browser, failedURI, aData);
+      authPrompt.show(undefined);
+    }
+  };
+
+  return retval;
+})(); /* OnionAuthPrompt */
+
+
+Object.defineProperty(this, "OnionAuthPrompt", {
+  value: OnionAuthPrompt,
+  enumerable: true,
+  writable: false
+});
diff --git a/browser/components/onionservices/content/authUtil.jsm b/browser/components/onionservices/content/authUtil.jsm
new file mode 100644
index 000000000000..8547fba83a62
--- /dev/null
+++ b/browser/components/onionservices/content/authUtil.jsm
@@ -0,0 +1,32 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "OnionAuthUtil",
+];
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const OnionAuthUtil = {
+  string: {
+    authPromptTopic: "tor-onion-services-auth-prompt",
+    authPromptCanceledMessage: "Tor:OnionServicesAuthPromptCanceled",
+    anchorID: "tor-clientauth-notification-icon",
+    notificationID: "tor-clientauth",
+    descriptionID: "tor-clientauth-notification-desc",
+    learnMoreID: "tor-clientauth-notification-learnmore",
+    onionNameSpanID: "tor-clientauth-notification-onionname",
+    keyElementID: "tor-clientauth-notification-key",
+    warningElementID: "tor-clientauth-warning",
+  },
+
+  addCancelMessageListener(aTabContent, aDocShell) {
+    aTabContent.addMessageListener(this.string.authPromptCanceledMessage,
+                                   (aMessage) => {
+      let failedURI = Services.io.newURI(aMessage.data.failedURI);
+      aDocShell.displayLoadError(Cr.NS_ERROR_CONNECTION_REFUSED, failedURI,
+                                 undefined, undefined);
+    });
+  },
+};
diff --git a/browser/components/onionservices/content/onionservices.css b/browser/components/onionservices/content/onionservices.css
new file mode 100644
index 000000000000..e2621ec8266d
--- /dev/null
+++ b/browser/components/onionservices/content/onionservices.css
@@ -0,0 +1,69 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+ at namespace html url("http://www.w3.org/1999/xhtml");
+
+html|*#tor-clientauth-notification-onionname {
+  font-weight: bold;
+}
+
+html|*#tor-clientauth-notification-key {
+  box-sizing: border-box;
+  width: 100%;
+  margin-top: 15px;
+  padding: 6px;
+}
+
+/* Start of rules adapted from
+ * browser/components/newtab/css/activity-stream-mac.css (linux and windows
+ * use the same rules).
+ */
+html|*#tor-clientauth-notification-key.invalid {
+  border: 1px solid #D70022;
+  box-shadow: 0 0 0 1px #D70022, 0 0 0 4px rgba(215, 0, 34, 0.3);
+}
+
+html|*#tor-clientauth-warning {
+  display: inline-block;
+  animation: fade-up-tt 450ms;
+  background: #D70022;
+  border-radius: 2px;
+  color: #FFF;
+  inset-inline-start: 3px;
+  padding: 5px 12px;
+  position: relative;
+  top: 6px;
+  z-index: 1;
+}
+
+html|*#tor-clientauth-warning[hidden] {
+  display: none;
+}
+
+html|*#tor-clientauth-warning::before {
+  background: #D70022;
+  bottom: -8px;
+  content: '.';
+  height: 16px;
+  inset-inline-start: 12px;
+  position: absolute;
+  text-indent: -999px;
+  top: -7px;
+  transform: rotate(45deg);
+  white-space: nowrap;
+  width: 16px;
+  z-index: -1;
+}
+
+ at keyframes fade-up-tt {
+  0% {
+    opacity: 0;
+    transform: translateY(15px);
+  }
+  100% {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+/* End of rules adapted from
+ * browser/components/newtab/css/activity-stream-mac.css
+ */
diff --git a/browser/components/onionservices/jar.mn b/browser/components/onionservices/jar.mn
new file mode 100644
index 000000000000..06cf2df6e7ac
--- /dev/null
+++ b/browser/components/onionservices/jar.mn
@@ -0,0 +1,4 @@
+browser.jar:
+    content/browser/onionservices/authPrompt.js                    (content/authPrompt.js)
+    content/browser/onionservices/authUtil.jsm                     (content/authUtil.jsm)
+    content/browser/onionservices/onionservices.css                (content/onionservices.css)
diff --git a/browser/components/onionservices/moz.build b/browser/components/onionservices/moz.build
new file mode 100644
index 000000000000..7e103239c8d6
--- /dev/null
+++ b/browser/components/onionservices/moz.build
@@ -0,0 +1 @@
+JAR_MANIFESTS += ['jar.mn']
diff --git a/browser/modules/TorStrings.jsm b/browser/modules/TorStrings.jsm
index 0b682ceeeb83..f68d60cf1343 100644
--- a/browser/modules/TorStrings.jsm
+++ b/browser/modules/TorStrings.jsm
@@ -5,6 +5,9 @@ var EXPORTED_SYMBOLS = ["TorStrings"];
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
+const { Services } = ChromeUtils.import(
+  "resource://gre/modules/Services.jsm"
+);
 const { getLocale } = ChromeUtils.import(
   "resource://torbutton/modules/utils.js"
 );
@@ -17,11 +20,11 @@ XPCOMUtils.defineLazyGetter(this, "domParser", () => {
 });
 
 /*
-  Tor String Bundle
+  Tor DTD String Bundle
 
-  Strings loaded from torbutton/tor-launcher, but provide a fallback in case they aren't available
+  DTD strings loaded from torbutton/tor-launcher, but provide a fallback in case they aren't available
 */
-class TorStringBundle {
+class TorDTDStringBundle {
   constructor(aBundleURLs, aPrefix) {
     let locations = [];
     for (const [index, url] of aBundleURLs.entries()) {
@@ -65,6 +68,36 @@ class TorStringBundle {
 }
 
 /*
+  Tor Property String Bundle
+
+  Property strings loaded from torbutton/tor-launcher, but provide a fallback in case they aren't available
+*/
+class TorPropertyStringBundle {
+  constructor(aBundleURL, aPrefix) {
+    try {
+      this._bundle = Services.strings.createBundle(aBundleURL);
+    } catch (e) {}
+
+    this._prefix = aPrefix;
+  }
+
+  getString(key, fallback) {
+    if (key) {
+      try {
+        return this._bundle.GetStringFromName(`${this._prefix}${key}`);
+      } catch (e) {}
+    }
+
+    // on failure, assign the fallback if it exists
+    if (fallback) {
+      return fallback;
+    }
+    // otherwise return string key
+    return `$(${key})`;
+  }
+}
+
+/*
   Security Level Strings
 */
 var TorStrings = {
@@ -72,7 +105,7 @@ var TorStrings = {
     Tor Browser Security Level Strings
   */
   securityLevel: (function() {
-    let tsb = new TorStringBundle(
+    let tsb = new TorDTDStringBundle(
       ["chrome://torbutton/locale/torbutton.dtd"],
       "torbutton.prefs.sec_"
     );
@@ -157,7 +190,7 @@ var TorStrings = {
     Tor about:preferences#tor Strings
   */
   settings: (function() {
-    let tsb = new TorStringBundle(
+    let tsb = new TorDTDStringBundle(
       ["chrome://torlauncher/locale/network-settings.dtd"],
       ""
     );
@@ -284,6 +317,37 @@ var TorStrings = {
   })() /* Tor Network Settings Strings */,
 
   /*
+    Tor Onion Services Strings, e.g., for the authentication prompt.
+  */
+  onionServices: (function() {
+    let tsb = new TorPropertyStringBundle(
+      "chrome://torbutton/locale/torbutton.properties",
+      "onionServices."
+    );
+    let getString = function(key, fallback) {
+      return tsb.getString(key, fallback);
+    };
+
+    let retval = {
+      learnMore: getString("torPreferences.learnMore", "Learn More"),
+      learnMoreURL: `https://2019.www.torproject.org/docs/tor-manual-dev.html.${getLocale()}#_client_authorization`,
+      authPrompt: {
+        description:
+          getString("authPrompt.description", "%S is requesting your private key."),
+        keyPlaceholder: getString("authPrompt.keyPlaceholder", "Enter your key"),
+        done: getString("authPrompt.done", "Done"),
+        doneAccessKey: getString("authPrompt.doneAccessKey", "d"),
+        invalidKey: getString("authPrompt.invalidKey", "Invalid key"),
+        failedToSetKey:
+          getString("authPrompt.failedToSetKey", "Failed to set key"),
+      },
+    };
+
+    return retval;
+  })() /* Tor Onion Services Strings */,
+
+
+  /*
     Tor Deamon Configuration Key Strings
   */
 
diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css
index 6d461a89488c..d35f292a0860 100644
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -113,6 +113,9 @@
   list-style-image: url(chrome://browser/skin/notification-icons/indexedDB.svg);
 }
 
+/* Reuse Firefox's login (key) icon for the Tor onion services auth. prompt */
+.popup-notification-icon[popupid="tor-clientauth"],
+.tor-clientauth-icon,
 .popup-notification-icon[popupid="password"],
 .login-icon {
   list-style-image: url(chrome://browser/skin/login.svg);
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index 3cf2086206a4..cc329a50c109 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -4363,6 +4363,26 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
         // HTTP/2 stack detected a protocol error
         error = "networkProtocolError";
         break;
+      case NS_ERROR_TOR_ONION_SVC_NOT_FOUND:
+      case NS_ERROR_TOR_ONION_SVC_IS_INVALID:
+      case NS_ERROR_TOR_ONION_SVC_INTRO_FAILED:
+      case NS_ERROR_TOR_ONION_SVC_REND_FAILED:
+        // For now, handle these Tor onion service errors the same as
+        // NS_ERROR_CONNECTION_REFUSED.
+        NS_ENSURE_ARG_POINTER(aURI);
+        addHostPort = true;
+        error = "connectionFailure";
+        break;
+      case NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH:
+        // For now, we let this fall through but it should be handled in
+        // a special way, e.g., tell the user in our auth prompt that the
+        // key they provided was bad. This will be done as part of #30025.
+        MOZ_FALLTHROUGH;
+      case NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH:
+        error = "onionServices.clientAuthMissing";
+        // Display about:blank while the Tor client auth prompt is open.
+        errorPage.AssignLiteral("blank");
+        break;
       default:
         break;
     }
@@ -4442,6 +4462,20 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
     nsAutoString str;
     rv = stringBundle->FormatStringFromName(errorDescriptionID, strs,
                                             formatStrCount, str);
+    if (NS_FAILED(rv)) {
+      // As a fallback, check torbutton.properties for the error string.
+      const char bundleURL[] = "chrome://torbutton/locale/torbutton.properties";
+      nsCOMPtr<nsIStringBundleService> stringBundleService =
+        mozilla::services::GetStringBundleService();
+      if (stringBundleService) {
+        nsCOMPtr<nsIStringBundle> tbStringBundle;
+        if (NS_SUCCEEDED(stringBundleService->CreateBundle(bundleURL,
+                                       getter_AddRefs(tbStringBundle)))) {
+          rv = tbStringBundle->FormatStringFromName(errorDescriptionID, strs,
+                                                    formatStrCount, str);
+        }
+      }
+    }
     NS_ENSURE_SUCCESS(rv, rv);
     messageStr.Assign(str.get());
   }
@@ -6964,6 +6998,7 @@ nsresult nsDocShell::EndPageLoad(nsIWebProgress* aProgress,
                aStatus == NS_ERROR_INTERCEPTION_FAILED ||
                aStatus == NS_ERROR_NET_INADEQUATE_SECURITY ||
                aStatus == NS_ERROR_NET_HTTP2_SENT_GOAWAY ||
+               NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_TOR ||
                NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_SECURITY) {
       // Errors to be shown for any frame
       DisplayLoadError(aStatus, url, nullptr, aChannel);
@@ -8371,6 +8406,33 @@ nsresult nsDocShell::CreateContentViewer(const nsACString& aContentType,
     FireOnLocationChange(this, aRequest, mCurrentURI, locationFlags);
   }
 
+  // Arrange to show a Tor onion service client authentication prompt if
+  // appropriate.
+  if ((mLoadType == LOAD_ERROR_PAGE) && failedChannel) {
+    nsresult status = NS_OK;
+    if (NS_SUCCEEDED(failedChannel->GetStatus(&status)) &&
+        ((status == NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH) ||
+         (status == NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH))) {
+      nsAutoCString onionHost;
+      failedURI->GetHost(onionHost);
+      if (XRE_IsContentProcess()) {
+        nsCOMPtr<nsIBrowserChild> browserChild = GetBrowserChild();
+        if (browserChild) {
+          static_cast<BrowserChild*>(browserChild.get())
+              ->SendShowOnionServicesAuthPrompt(onionHost);
+        }
+      } else {
+        nsCOMPtr<nsPIDOMWindowOuter> browserWin = GetWindow();
+        nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
+        if (browserWin && obsSvc) {
+          const char* topic = "tor-onion-services-auth-prompt";
+          obsSvc->NotifyObservers(browserWin, topic,
+                                  NS_ConvertUTF8toUTF16(onionHost).get());
+        }
+      }
+    }
+  }
+
   return NS_OK;
 }
 
diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp
index 42e5cbd10cc1..88605c949502 100644
--- a/dom/ipc/BrowserParent.cpp
+++ b/dom/ipc/BrowserParent.cpp
@@ -3776,6 +3776,29 @@ mozilla::ipc::IPCResult BrowserParent::RecvShowCanvasPermissionPrompt(
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult BrowserParent::RecvShowOnionServicesAuthPrompt(
+    const nsCString& aOnionName) {
+  nsCOMPtr<nsIBrowser> browser =
+      mFrameElement ? mFrameElement->AsBrowser() : nullptr;
+  if (!browser) {
+    // If the tab is being closed, the browser may not be available.
+    // In this case we can ignore the request.
+    return IPC_OK();
+  }
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  if (!os) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  nsresult rv = os->NotifyObservers(
+      browser,
+      "tor-onion-services-auth-prompt",
+      NS_ConvertUTF8toUTF16(aOnionName).get());
+  if (NS_FAILED(rv)) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  return IPC_OK();
+}
+
 mozilla::ipc::IPCResult BrowserParent::RecvVisitURI(
     const URIParams& aURI, const Maybe<URIParams>& aLastVisitedURI,
     const uint32_t& aFlags) {
diff --git a/dom/ipc/BrowserParent.h b/dom/ipc/BrowserParent.h
index 1be1da2e4d81..23c189cf6243 100644
--- a/dom/ipc/BrowserParent.h
+++ b/dom/ipc/BrowserParent.h
@@ -718,6 +718,9 @@ class BrowserParent final : public PBrowserParent,
   mozilla::ipc::IPCResult RecvShowCanvasPermissionPrompt(
       const nsCString& aOrigin, const bool& aHideDoorHanger);
 
+  mozilla::ipc::IPCResult RecvShowOnionServicesAuthPrompt(
+      const nsCString& aOnionName);
+
   mozilla::ipc::IPCResult RecvSetSystemFont(const nsCString& aFontName);
   mozilla::ipc::IPCResult RecvGetSystemFont(nsCString* aFontName);
 
diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl
index 55ce57441a2d..b99d7f04fe39 100644
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -562,6 +562,14 @@ parent:
                              int32_t[] aPositionDescendants,
                              uint32_t aFlushId);
 
+    /**
+     * This function is used to notify the parent that it should display a
+     * onion services client authentication prompt.
+     *
+     * @param aOnionHost The hostname of the .onion that needs authentication.
+     */
+    async ShowOnionServicesAuthPrompt(nsCString aOnionHost);
+
 child:
     async NativeSynthesisResponse(uint64_t aObserverId, nsCString aResponse);
     async FlushTabState(uint32_t aFlushId);
diff --git a/js/xpconnect/src/xpc.msg b/js/xpconnect/src/xpc.msg
index d0088cb2fb07..d9b64fe87fce 100644
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -246,5 +246,13 @@ XPC_MSG_DEF(NS_ERROR_HARMFUL_URI                      , "The URI is harmful")
 XPC_MSG_DEF(NS_ERROR_FINGERPRINTING_URI               , "The URI is fingerprinting")
 XPC_MSG_DEF(NS_ERROR_CRYPTOMINING_URI                 , "The URI is cryptomining")
 
+/* Codes related to Tor */
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_NOT_FOUND          , "Tor onion service descriptor cannot be found")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_IS_INVALID         , "Tor onion service descriptor is invalid")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_INTRO_FAILED       , "Tor onion service introduction failed")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_REND_FAILED        , "Tor onion service rendezvous failed")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH, "Tor onion service missing client authorization")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH    , "Tor onion service wrong client authorization")
+
 /* Profile manager error codes */
 XPC_MSG_DEF(NS_ERROR_DATABASE_CHANGED                 , "Flushing the profiles to disk would have overwritten changes made elsewhere.")
diff --git a/netwerk/base/nsSocketTransport2.cpp b/netwerk/base/nsSocketTransport2.cpp
index 04f692dcd7e0..e6a32bddb16f 100644
--- a/netwerk/base/nsSocketTransport2.cpp
+++ b/netwerk/base/nsSocketTransport2.cpp
@@ -213,6 +213,12 @@ nsresult ErrorAccordingToNSPR(PRErrorCode errorCode) {
     default:
       if (psm::IsNSSErrorCode(errorCode)) {
         rv = psm::GetXPCOMFromNSSError(errorCode);
+      } else {
+        // If we received a Tor extended error code via SOCKS, pass it through.
+        nsresult res = nsresult(errorCode);
+        if (NS_ERROR_GET_MODULE(res) == NS_ERROR_MODULE_TOR) {
+          rv = res;
+        }
       }
       break;
 
diff --git a/netwerk/socket/nsSOCKSIOLayer.cpp b/netwerk/socket/nsSOCKSIOLayer.cpp
index 6909ade1c958..6649056da1e7 100644
--- a/netwerk/socket/nsSOCKSIOLayer.cpp
+++ b/netwerk/socket/nsSOCKSIOLayer.cpp
@@ -1012,6 +1012,36 @@ PRStatus nsSOCKSSocketInfo::ReadV5ConnectResponseTop() {
              "08, Address type not supported."));
         c = PR_BAD_ADDRESS_ERROR;
         break;
+      case 0xF0:  // Tor SOCKS5_HS_NOT_FOUND
+        LOGERROR(("socks5: connect failed: F0,"
+                  " Tor onion service descriptor can not be found."));
+        c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_NOT_FOUND);
+        break;
+      case 0xF1:  // Tor SOCKS5_HS_IS_INVALID
+        LOGERROR(("socks5: connect failed: F1,"
+                  " Tor onion service descriptor is invalid."));
+        c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_IS_INVALID);
+        break;
+      case 0xF2:  // Tor SOCKS5_HS_INTRO_FAILED
+        LOGERROR(("socks5: connect failed: F2,"
+                  " Tor onion service introduction failed."));
+        c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_INTRO_FAILED);
+        break;
+      case 0xF3:  // Tor SOCKS5_HS_REND_FAILED
+        LOGERROR(("socks5: connect failed: F3,"
+                  " Tor onion service rendezvous failed."));
+        c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_REND_FAILED);
+        break;
+      case 0xF4:  // Tor SOCKS5_HS_MISSING_CLIENT_AUTH
+        LOGERROR(("socks5: connect failed: F4,"
+                  " Tor onion service missing client authorization."));
+        c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH);
+        break;
+      case 0xF5:  // Tor SOCKS5_HS_BAD_CLIENT_AUTH
+        LOGERROR(("socks5: connect failed: F5,"
+                  " Tor onion service wrong client authorization."));
+        c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH);
+        break;
       default:
         LOGERROR(("socks5: connect failed."));
         break;
diff --git a/toolkit/modules/PopupNotifications.jsm b/toolkit/modules/PopupNotifications.jsm
index a6f8be0be40c..fe8e6d4949a4 100644
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -400,6 +400,8 @@ PopupNotifications.prototype = {
    *                - "menucommand" if a menu was activated.
    *          - [optional] dismiss (boolean): If this is true, the notification
    *            will be dismissed instead of removed after running the callback.
+   *          - [optional] leaveOpen (boolean): If this is true, the notification
+   *            will not be removed after running the callback.
    *          - [optional] disableHighlight (boolean): If this is true, the button
    *            will not apply the default highlight style.
    *        If null, the notification will have a default "OK" action button
@@ -1863,6 +1865,10 @@ PopupNotifications.prototype = {
         this._dismiss();
         return;
       }
+
+      if (action.leaveOpen) {
+        return;
+      }
     }
 
     this._remove(notification);
diff --git a/xpcom/base/ErrorList.py b/xpcom/base/ErrorList.py
index 48b3149a4255..24cabd355b0d 100755
--- a/xpcom/base/ErrorList.py
+++ b/xpcom/base/ErrorList.py
@@ -84,6 +84,7 @@ modules["URL_CLASSIFIER"] = Mod(42)
 # ErrorResult gets its own module to reduce the chance of someone accidentally
 # defining an error code matching one of the ErrorResult ones.
 modules["ERRORRESULT"] = Mod(43)
+modules["TOR"] = Mod(44)
 
 # NS_ERROR_MODULE_GENERAL should be used by modules that do not
 # care if return code values overlap. Callers of methods that
@@ -1120,6 +1121,23 @@ with modules["ERRORRESULT"]:
 
 
 # =======================================================================
+# 44: Tor-specific error codes.
+# =======================================================================
+with modules["TOR"]:
+    # Tor onion service descriptor can not be found.
+    errors["NS_ERROR_TOR_ONION_SVC_NOT_FOUND"] = FAILURE(1)
+    # Tor onion service descriptor is invalid.
+    errors["NS_ERROR_TOR_ONION_SVC_IS_INVALID"] = FAILURE(2)
+    # Tor onion service introduction failed.
+    errors["NS_ERROR_TOR_ONION_SVC_INTRO_FAILED"] = FAILURE(3)
+    # Tor onion service rendezvous failed.
+    errors["NS_ERROR_TOR_ONION_SVC_REND_FAILED"] = FAILURE(4)
+    # Tor onion service missing client authorization.
+    errors["NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH"] = FAILURE(5)
+    # Tor onion service wrong client authorization.
+    errors["NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH"] = FAILURE(6)
+
+# =======================================================================
 # 51: NS_ERROR_MODULE_GENERAL
 # =======================================================================
 with modules["GENERAL"]:





More information about the tbb-commits mailing list