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

sysrqb at torproject.org sysrqb at torproject.org
Fri Feb 21 21:46:34 UTC 2020


commit aed69dc95387429e18b18ad578fb78d4a83d91f2
Author: Kathy Brade <brade at pearlcrescent.com>
Date:   Thu Feb 13 15:06:38 2020 -0500

    squash! Bug 30237: Add v3 onion services client authentication prompt
    
    Also fixes bug 19757:
     Add a "Remember this key" checkbox to the client auth prompt.
    
     Add an "Onion Services Authentication" section within the
     about:preferences "Privacy & Security section" to allow
     viewing and removal of v3 onion client auth keys that have
     been stored on disk.
---
 .../onionservices/content/authPopup.inc.xul        |   2 +
 .../onionservices/content/authPreferences.css      |  20 ++
 .../onionservices/content/authPreferences.inc.xul  |  20 ++
 .../onionservices/content/authPreferences.js       |  63 +++++
 .../components/onionservices/content/authPrompt.js |  40 ++--
 .../components/onionservices/content/authUtil.jsm  |  27 ++-
 .../onionservices/content/savedKeysDialog.js       | 259 +++++++++++++++++++++
 .../onionservices/content/savedKeysDialog.xul      |  42 ++++
 browser/components/onionservices/jar.mn            |   4 +
 .../preferences/in-content/preferences.xul         |   1 +
 .../components/preferences/in-content/privacy.js   |   7 +
 .../components/preferences/in-content/privacy.xul  |   2 +
 browser/modules/TorStrings.jsm                     |  15 +-
 13 files changed, 477 insertions(+), 25 deletions(-)

diff --git a/browser/components/onionservices/content/authPopup.inc.xul b/browser/components/onionservices/content/authPopup.inc.xul
index d327e4c6a88d..bd0ec3aa0b00 100644
--- a/browser/components/onionservices/content/authPopup.inc.xul
+++ b/browser/components/onionservices/content/authPopup.inc.xul
@@ -9,6 +9,8 @@
     <html:div>
       <html:input id="tor-clientauth-notification-key" type="password"/>
       <html:div id="tor-clientauth-warning"/>
+      <checkbox id="tor-clientauth-persistkey-checkbox"
+                label="&torbutton.onionServices.authPrompt.persistCheckboxLabel;"/>
     </html:div>
   </popupnotificationcontent>
 </popupnotification>
diff --git a/browser/components/onionservices/content/authPreferences.css b/browser/components/onionservices/content/authPreferences.css
new file mode 100644
index 000000000000..b3fb79b26ddc
--- /dev/null
+++ b/browser/components/onionservices/content/authPreferences.css
@@ -0,0 +1,20 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+#torOnionServiceKeys-overview-container {
+  margin-right: 30px;
+}
+
+#onionservices-savedkeys-tree treechildren::-moz-tree-cell-text {
+  font-size: 80%;
+}
+
+#onionservices-savedkeys-errorContainer {
+  margin-top: 4px;
+  min-height: 3em;
+}
+
+#onionservices-savedkeys-errorIcon {
+  margin-right: 4px;
+  list-style-image: url("chrome://browser/skin/warning.svg");
+  visibility: hidden;
+}
diff --git a/browser/components/onionservices/content/authPreferences.inc.xul b/browser/components/onionservices/content/authPreferences.inc.xul
new file mode 100644
index 000000000000..0b6ce98efa31
--- /dev/null
+++ b/browser/components/onionservices/content/authPreferences.inc.xul
@@ -0,0 +1,20 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<groupbox id="torOnionServiceKeys" orient="vertical"
+          data-category="panePrivacy" hidden="true">
+  <label><html:h2 id="torOnionServiceKeys-header"/></label>
+  <hbox>
+    <description id="torOnionServiceKeys-overview-container" flex="1">
+      <html:span id="torOnionServiceKeys-overview"
+                 class="tail-with-learn-more"/>
+      <label id="torOnionServiceKeys-learnMore" class="learnMore text-link"
+             is="text-link"/>
+    </description>
+    <vbox align="end">
+      <button id="torOnionServiceKeys-savedKeys"
+              is="highlightable-button"
+              class="accessory-button"
+              oncommand="OnionServicesAuthPreferences.onViewSavedKeys()"/>
+    </vbox>
+  </hbox>
+</groupbox>
diff --git a/browser/components/onionservices/content/authPreferences.js b/browser/components/onionservices/content/authPreferences.js
new file mode 100644
index 000000000000..c388fbee6b3e
--- /dev/null
+++ b/browser/components/onionservices/content/authPreferences.js
@@ -0,0 +1,63 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorStrings",
+  "resource:///modules/TorStrings.jsm"
+);
+
+/*
+  Onion Services Client Authentication Preferences Code
+
+  Code to handle init and update of onion services authentication section
+  in about:preferences#privacy
+*/
+
+const OnionServicesAuthPreferences = {
+  selector: {
+    groupBox: "#torOnionServiceKeys",
+    header: "#torOnionServiceKeys-header",
+    overview: "#torOnionServiceKeys-overview",
+    learnMore: "#torOnionServiceKeys-learnMore",
+    savedKeysButton: "#torOnionServiceKeys-savedKeys",
+  },
+
+  init() {
+    // populate XUL with localized strings
+    this._populateXUL();
+  },
+
+  _populateXUL() {
+    const groupbox = document.querySelector(this.selector.groupBox);
+
+    let elem = groupbox.querySelector(this.selector.header);
+    elem.textContent = TorStrings.onionServices.authPreferences.header;
+
+    elem = groupbox.querySelector(this.selector.overview);
+    elem.textContent = TorStrings.onionServices.authPreferences.overview;
+
+    elem = groupbox.querySelector(this.selector.learnMore);
+    elem.setAttribute("value", TorStrings.onionServices.learnMore);
+    elem.setAttribute("href", TorStrings.onionServices.learnMoreURL);
+
+    elem = groupbox.querySelector(this.selector.savedKeysButton);
+    elem.setAttribute(
+      "label",
+      TorStrings.onionServices.authPreferences.savedKeys
+    );
+  },
+
+  onViewSavedKeys() {
+    gSubDialog.open(
+      "chrome://browser/content/onionservices/savedKeysDialog.xul"
+    );
+  },
+}; // OnionServicesAuthPreferences
+
+Object.defineProperty(this, "OnionServicesAuthPreferences", {
+  value: OnionServicesAuthPreferences,
+  enumerable: true,
+  writable: false,
+});
diff --git a/browser/components/onionservices/content/authPrompt.js b/browser/components/onionservices/content/authPrompt.js
index 2d4ebcafd688..f7a10e75158a 100644
--- a/browser/components/onionservices/content/authPrompt.js
+++ b/browser/components/onionservices/content/authPrompt.js
@@ -56,14 +56,14 @@ const OnionAuthPrompt = (function() {
       };
 
       this._prompt = PopupNotifications.show(this._browser,
-                       OnionAuthUtil.string.notificationID, "",
-                       OnionAuthUtil.string.anchorID,
+                       OnionAuthUtil.domid.notification, "",
+                       OnionAuthUtil.domid.anchor,
                        mainAction, [cancelAction], options);
     },
 
     _onPromptShowing(aWarningMessage) {
       let xulDoc = this._browser.ownerDocument;
-      let descElem = xulDoc.getElementById(OnionAuthUtil.string.descriptionID);
+      let descElem = xulDoc.getElementById(OnionAuthUtil.domid.description);
       if (descElem) {
         // Handle replacement of the onion name within the localized
         // string ourselves so we can show the onion name as bold text.
@@ -89,7 +89,7 @@ const OnionAuthPrompt = (function() {
         span.textContent = prefix;
         descElem.appendChild(span);
         span = xulDoc.createElementNS(kHTMLNS, "span");
-        span.id = OnionAuthUtil.string.onionNameSpanID;
+        span.id = OnionAuthUtil.domid.onionNameSpan;
         span.textContent = this._onionName;
         descElem.appendChild(span);
         span = xulDoc.createElementNS(kHTMLNS, "span");
@@ -98,13 +98,17 @@ const OnionAuthPrompt = (function() {
       }
 
       // Set "Learn More" label and href.
-      let learnMoreElem = xulDoc.getElementById(OnionAuthUtil.string.learnMoreID);
+      let learnMoreElem = xulDoc.getElementById(OnionAuthUtil.domid.learnMore);
       if (learnMoreElem) {
         learnMoreElem.setAttribute("value", TorStrings.onionServices.learnMore);
         learnMoreElem.setAttribute("href", TorStrings.onionServices.learnMoreURL);
       }
 
       this._showWarning(aWarningMessage);
+      let checkboxElem = this._getCheckboxElement();
+      if (checkboxElem) {
+        checkboxElem.checked = false;
+      }
     },
 
     _onPromptShown() {
@@ -170,7 +174,9 @@ const OnionAuthPrompt = (function() {
           this.show(controllerFailureMsg);
         });
         let onionAddr = this._onionName.toLowerCase().replace(/\.onion$/, "");
-        torController.onionAuthAdd(onionAddr, base64key)
+        let checkboxElem = this._getCheckboxElement();
+        let isPermanent = (checkboxElem && checkboxElem.checked);
+        torController.onionAuthAdd(onionAddr, base64key, isPermanent)
         .then(aResponse => {
           // Success! Reload the page.
           this._browser.messageManager.sendAsyncMessage("Browser:Reload", {});
@@ -189,19 +195,24 @@ const OnionAuthPrompt = (function() {
     _onCancel() {
       // Arrange for an error page to be displayed.
       this._browser.messageManager.sendAsyncMessage(
-                               OnionAuthUtil.string.authPromptCanceledMessage,
+                               OnionAuthUtil.message.authPromptCanceled,
                                {failedURI: this._failedURI.spec});
     },
 
     _getKeyElement() {
       let xulDoc = this._browser.ownerDocument;
-      return xulDoc.getElementById(OnionAuthUtil.string.keyElementID);
+      return xulDoc.getElementById(OnionAuthUtil.domid.keyElement);
+    },
+
+    _getCheckboxElement() {
+      let xulDoc = this._browser.ownerDocument;
+      return xulDoc.getElementById(OnionAuthUtil.domid.checkboxElement);
     },
 
     _showWarning(aWarningMessage) {
       let xulDoc = this._browser.ownerDocument;
       let warningElem =
-                 xulDoc.getElementById(OnionAuthUtil.string.warningElementID);
+                 xulDoc.getElementById(OnionAuthUtil.domid.warningElement);
       let keyElem = this._getKeyElement();
       if (warningElem) {
         if (aWarningMessage) {
@@ -225,9 +236,12 @@ const OnionAuthPrompt = (function() {
       let base64key;
       if (aKeyString.length == 52) {
         // The key is probably base32-encoded. Attempt to decode.
+        // Although base32 specifies uppercase letters, we accept lowercase
+        // as well because users may type in lowercase or copy a key out of
+        // a tor onion-auth file (which uses lowercase).
         let rawKey;
         try {
-          rawKey = CommonUtils.decodeBase32(aKeyString);
+          rawKey = CommonUtils.decodeBase32(aKeyString.toUpperCase());
         } catch (e) {}
 
         if (rawKey) try {
@@ -247,17 +261,17 @@ const OnionAuthPrompt = (function() {
 
   let retval = {
     init() {
-      Services.obs.addObserver(this, OnionAuthUtil.string.authPromptTopic);
+      Services.obs.addObserver(this, OnionAuthUtil.topic.authPrompt);
     },
 
     uninit() {
-      Services.obs.removeObserver(this, OnionAuthUtil.string.authPromptTopic);
+      Services.obs.removeObserver(this, OnionAuthUtil.topic.authPrompt);
     },
 
     // 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) {
+      if (aTopic != OnionAuthUtil.topic.authPrompt) {
         return;
       }
 
diff --git a/browser/components/onionservices/content/authUtil.jsm b/browser/components/onionservices/content/authUtil.jsm
index 8547fba83a62..e9446f51cfcb 100644
--- a/browser/components/onionservices/content/authUtil.jsm
+++ b/browser/components/onionservices/content/authUtil.jsm
@@ -9,20 +9,25 @@ var EXPORTED_SYMBOLS = [
 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",
+  topic: {
+    authPrompt: "tor-onion-services-auth-prompt",
+  },
+  message: {
+    authPromptCanceled: "Tor:OnionServicesAuthPromptCanceled",
+  },
+  domid: {
+    anchor: "tor-clientauth-notification-icon",
+    notification: "tor-clientauth",
+    description: "tor-clientauth-notification-desc",
+    learnMore: "tor-clientauth-notification-learnmore",
+    onionNameSpan: "tor-clientauth-notification-onionname",
+    keyElement: "tor-clientauth-notification-key",
+    warningElement: "tor-clientauth-warning",
+    checkboxElement: "tor-clientauth-persistkey-checkbox",
   },
 
   addCancelMessageListener(aTabContent, aDocShell) {
-    aTabContent.addMessageListener(this.string.authPromptCanceledMessage,
+    aTabContent.addMessageListener(this.message.authPromptCanceled,
                                    (aMessage) => {
       let failedURI = Services.io.newURI(aMessage.data.failedURI);
       aDocShell.displayLoadError(Cr.NS_ERROR_CONNECTION_REFUSED, failedURI,
diff --git a/browser/components/onionservices/content/savedKeysDialog.js b/browser/components/onionservices/content/savedKeysDialog.js
new file mode 100644
index 000000000000..b1376bbabe85
--- /dev/null
+++ b/browser/components/onionservices/content/savedKeysDialog.js
@@ -0,0 +1,259 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorStrings",
+  "resource:///modules/TorStrings.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "controller",
+  "resource://torbutton/modules/tor-control-port.js"
+);
+
+var gOnionServicesSavedKeysDialog = {
+  selector: {
+    dialog: "#onionservices-savedkeys-dialog",
+    intro: "#onionservices-savedkeys-intro",
+    tree: "#onionservices-savedkeys-tree",
+    onionSiteCol: "#onionservices-savedkeys-siteCol",
+    onionKeyCol: "#onionservices-savedkeys-keyCol",
+    errorIcon: "#onionservices-savedkeys-errorIcon",
+    errorMessage: "#onionservices-savedkeys-errorMessage",
+    removeButton: "#onionservices-savedkeys-remove",
+    removeAllButton: "#onionservices-savedkeys-removeall",
+  },
+
+  _tree: undefined,
+  _isBusy: false, // true when loading data, deleting a key, etc.
+
+  // Public functions (called from outside this file).
+  async deleteSelectedKeys() {
+    this._setBusyState(true);
+
+    const indexesToDelete = [];
+    const count = this._tree.view.selection.getRangeCount();
+    for (let i = 0; i < count; ++i) {
+      const minObj = {};
+      const maxObj = {};
+      this._tree.view.selection.getRangeAt(i, minObj, maxObj);
+      for (let idx = minObj.value; idx <= maxObj.value; ++idx) {
+        indexesToDelete.push(idx);
+      }
+    }
+
+    if (indexesToDelete.length > 0) {
+      const controllerFailureMsg =
+        TorStrings.onionServices.authPreferences.failedToRemoveKey;
+      try {
+        const torController = controller(aError => {
+          this._showError(controllerFailureMsg);
+        });
+
+        // Remove in reverse index order to avoid issues caused by index changes.
+        for (let i = indexesToDelete.length - 1; i >= 0; --i) {
+          await this._deleteOneKey(torController, indexesToDelete[i]);
+        }
+      } catch (e) {
+        if (e.torMessage) {
+          this._showError(e.torMessage);
+        } else {
+          this._showError(controllerFailureMsg);
+        }
+      }
+    }
+
+    this._setBusyState(false);
+  },
+
+  async deleteAllKeys() {
+    this._tree.view.selection.selectAll();
+    await this.deleteSelectedKeys();
+  },
+
+  updateButtonsState() {
+    const haveSelection = this._tree.view.selection.getRangeCount() > 0;
+    const dialog = document.querySelector(this.selector.dialog);
+    const removeSelectedBtn = dialog.querySelector(this.selector.removeButton);
+    removeSelectedBtn.disabled = this._isBusy || !haveSelection;
+    const removeAllBtn = dialog.querySelector(this.selector.removeAllButton);
+    removeAllBtn.disabled = this._isBusy || this.rowCount === 0;
+  },
+
+  // Private functions.
+  _onLoad() {
+    document.mozSubdialogReady = this._init();
+  },
+
+  async _init() {
+    await this._populateXUL();
+
+    window.addEventListener("keypress", this._onWindowKeyPress.bind(this));
+
+    // We don't use await here because we want _loadSavedKeys() to run
+    // in the background and not block loading of this dialog.
+    this._loadSavedKeys();
+  },
+
+  async _populateXUL() {
+    const dialog = document.querySelector(this.selector.dialog);
+    const authPrefStrings = TorStrings.onionServices.authPreferences;
+    dialog.setAttribute("title", authPrefStrings.dialogTitle);
+
+    let elem = dialog.querySelector(this.selector.intro);
+    elem.textContent = authPrefStrings.dialogIntro;
+
+    elem = dialog.querySelector(this.selector.onionSiteCol);
+    elem.setAttribute("label", authPrefStrings.onionSite);
+
+    elem = dialog.querySelector(this.selector.onionKeyCol);
+    elem.setAttribute("label", authPrefStrings.onionKey);
+
+    elem = dialog.querySelector(this.selector.removeButton);
+    elem.setAttribute("label", authPrefStrings.remove);
+
+    elem = dialog.querySelector(this.selector.removeAllButton);
+    elem.setAttribute("label", authPrefStrings.removeAll);
+
+    this._tree = dialog.querySelector(this.selector.tree);
+  },
+
+  async _loadSavedKeys() {
+    const controllerFailureMsg =
+      TorStrings.onionServices.authPreferences.failedToGetKeys;
+    this._setBusyState(true);
+
+    try {
+      this._tree.view = this;
+
+      const torController = controller(aError => {
+        this._showError(controllerFailureMsg);
+      });
+
+      const keyInfoList = await torController.onionAuthViewKeys();
+      if (keyInfoList) {
+        // Filter out temporary keys.
+        this._keyInfoList = keyInfoList.filter(aKeyInfo => {
+          if (!aKeyInfo.Flags) {
+            return false;
+          }
+
+          const flags = aKeyInfo.Flags.split(",");
+          return flags.includes("Permanent");
+        });
+
+        // Sort by the .onion address.
+        this._keyInfoList.sort((aObj1, aObj2) => {
+          const hsAddr1 = aObj1.hsAddress.toLowerCase();
+          const hsAddr2 = aObj2.hsAddress.toLowerCase();
+          if (hsAddr1 < hsAddr2) {
+            return -1;
+          }
+          return hsAddr1 > hsAddr2 ? 1 : 0;
+        });
+      }
+
+      // Render the tree content.
+      this._tree.rowCountChanged(0, this.rowCount);
+    } catch (e) {
+      if (e.torMessage) {
+        this._showError(e.torMessage);
+      } else {
+        this._showError(controllerFailureMsg);
+      }
+    }
+
+    this._setBusyState(false);
+  },
+
+  // This method may throw; callers should catch errors.
+  async _deleteOneKey(aTorController, aIndex) {
+    const keyInfoObj = this._keyInfoList[aIndex];
+    await aTorController.onionAuthRemove(keyInfoObj.hsAddress);
+    this._tree.view.selection.clearRange(aIndex, aIndex);
+    this._keyInfoList.splice(aIndex, 1);
+    this._tree.rowCountChanged(aIndex + 1, -1);
+  },
+
+  _setBusyState(aIsBusy) {
+    this._isBusy = aIsBusy;
+    this.updateButtonsState();
+  },
+
+  _onWindowKeyPress(event) {
+    if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
+      window.close();
+    } else if (event.keyCode === KeyEvent.DOM_VK_DELETE) {
+      this.deleteSelectedKeys();
+    }
+  },
+
+  _showError(aMessage) {
+    const dialog = document.querySelector(this.selector.dialog);
+    const errorIcon = dialog.querySelector(this.selector.errorIcon);
+    errorIcon.style.visibility = aMessage ? "visible" : "hidden";
+    const errorDesc = dialog.querySelector(this.selector.errorMessage);
+    errorDesc.textContent = aMessage ? aMessage : "";
+  },
+
+  // XUL tree widget view implementation.
+  get rowCount() {
+    return this._keyInfoList ? this._keyInfoList.length : 0;
+  },
+
+  getCellText(aRow, aCol) {
+    let val = "";
+    if (this._keyInfoList && aRow < this._keyInfoList.length) {
+      const keyInfo = this._keyInfoList[aRow];
+      if (aCol.id.endsWith("-siteCol")) {
+        val = keyInfo.hsAddress;
+      } else if (aCol.id.endsWith("-keyCol")) {
+        val = keyInfo.typeAndKey;
+        // Omit keyType because it is always "x25519".
+        const idx = val.indexOf(":");
+        if (idx > 0) {
+          val = val.substring(idx + 1);
+        }
+      }
+    }
+
+    return val;
+  },
+
+  isSeparator(index) {
+    return false;
+  },
+
+  isSorted() {
+    return false;
+  },
+
+  isContainer(index) {
+    return false;
+  },
+
+  setTree(tree) {},
+
+  getImageSrc(row, column) {},
+
+  getCellValue(row, column) {},
+
+  cycleHeader(column) {},
+
+  getRowProperties(row) {
+    return "";
+  },
+
+  getColumnProperties(column) {
+    return "";
+  },
+
+  getCellProperties(row, column) {
+    return "";
+  },
+};
+
+window.addEventListener("load", () => gOnionServicesSavedKeysDialog._onLoad());
diff --git a/browser/components/onionservices/content/savedKeysDialog.xul b/browser/components/onionservices/content/savedKeysDialog.xul
new file mode 100644
index 000000000000..3db9bb05ea82
--- /dev/null
+++ b/browser/components/onionservices/content/savedKeysDialog.xul
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!-- Copyright (c) 2020, The Tor Project, Inc. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/onionservices/authPreferences.css" type="text/css"?>
+
+<window id="onionservices-savedkeys-dialog"
+    windowtype="OnionServices:SavedKeys"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    style="width: 45em;">
+
+  <script src="chrome://browser/content/onionservices/savedKeysDialog.js"/>
+
+  <vbox id="onionservices-savedkeys" class="contentPane" flex="1">
+    <label id="onionservices-savedkeys-intro"
+           control="onionservices-savedkeys-tree"/>
+    <separator class="thin"/>
+    <tree id="onionservices-savedkeys-tree" flex="1" hidecolumnpicker="true"
+          width="750"
+          style="height: 20em;"
+          onselect="gOnionServicesSavedKeysDialog.updateButtonsState();">
+      <treecols>
+        <treecol id="onionservices-savedkeys-siteCol" flex="1" persist="width"/>
+        <splitter class="tree-splitter"/>
+        <treecol id="onionservices-savedkeys-keyCol" flex="1" persist="width"/>
+      </treecols>
+      <treechildren/>
+    </tree>
+    <hbox id="onionservices-savedkeys-errorContainer" align="baseline" flex="1">
+      <image id="onionservices-savedkeys-errorIcon"/>
+      <description id="onionservices-savedkeys-errorMessage" flex="1"/>
+    </hbox>
+    <separator class="thin"/>
+    <hbox id="onionservices-savedkeys-buttons">
+      <button id="onionservices-savedkeys-remove" disabled="true"
+              oncommand="gOnionServicesSavedKeysDialog.deleteSelectedKeys();"/>
+      <button id="onionservices-savedkeys-removeall"
+              oncommand="gOnionServicesSavedKeysDialog.deleteAllKeys();"/>
+    </hbox>
+  </vbox>
+</window>
diff --git a/browser/components/onionservices/jar.mn b/browser/components/onionservices/jar.mn
index 06cf2df6e7ac..583ab77bc6d8 100644
--- a/browser/components/onionservices/jar.mn
+++ b/browser/components/onionservices/jar.mn
@@ -1,4 +1,8 @@
 browser.jar:
+    content/browser/onionservices/authPreferences.css              (content/authPreferences.css)
+    content/browser/onionservices/authPreferences.js               (content/authPreferences.js)
     content/browser/onionservices/authPrompt.js                    (content/authPrompt.js)
     content/browser/onionservices/authUtil.jsm                     (content/authUtil.jsm)
     content/browser/onionservices/onionservices.css                (content/onionservices.css)
+    content/browser/onionservices/savedKeysDialog.js               (content/savedKeysDialog.js)
+    content/browser/onionservices/savedKeysDialog.xul              (content/savedKeysDialog.xul)
diff --git a/browser/components/preferences/in-content/preferences.xul b/browser/components/preferences/in-content/preferences.xul
index 7a01443ab048..30915e3d358f 100644
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -15,6 +15,7 @@
 <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/search.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/containers.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/privacy.css"?>
+<?xml-stylesheet href="chrome://browser/content/onionservices/authPreferences.css"?>
 <?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
 <?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
 
diff --git a/browser/components/preferences/in-content/privacy.js b/browser/components/preferences/in-content/privacy.js
index 297d07fadf1f..ee86b4158d7c 100644
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -62,6 +62,12 @@ XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function() {
   }
 });
 
+XPCOMUtils.defineLazyScriptGetter(
+  this,
+  ["OnionServicesAuthPreferences"],
+  "chrome://browser/content/onionservices/authPreferences.js"
+);
+
 // TODO: module import via ChromeUtils.defineModuleGetter
 XPCOMUtils.defineLazyScriptGetter(
   this,
@@ -369,6 +375,7 @@ var gPrivacyPane = {
     this.trackingProtectionReadPrefs();
     this.networkCookieBehaviorReadPrefs();
     this._initTrackingProtectionExtensionControl();
+    OnionServicesAuthPreferences.init();
     this._initSecurityLevel();
 
     Services.telemetry.setEventRecordingEnabled("pwmgr", true);
diff --git a/browser/components/preferences/in-content/privacy.xul b/browser/components/preferences/in-content/privacy.xul
index 013fe147bc82..e807ac69f1f1 100644
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -468,6 +468,8 @@
   </hbox>
 </groupbox>
 
+#include ../../onionservices/content/authPreferences.inc.xul
+
 <!-- The form autofill section is inserted in to this box
      after the form autofill extension has initialized. -->
 <groupbox id="formAutofillGroupBox"
diff --git a/browser/modules/TorStrings.jsm b/browser/modules/TorStrings.jsm
index f68d60cf1343..e9a8b3969297 100644
--- a/browser/modules/TorStrings.jsm
+++ b/browser/modules/TorStrings.jsm
@@ -329,7 +329,7 @@ var TorStrings = {
     };
 
     let retval = {
-      learnMore: getString("torPreferences.learnMore", "Learn More"),
+      learnMore: getString("learnMore", "Learn more"),
       learnMoreURL: `https://2019.www.torproject.org/docs/tor-manual-dev.html.${getLocale()}#_client_authorization`,
       authPrompt: {
         description:
@@ -341,6 +341,19 @@ var TorStrings = {
         failedToSetKey:
           getString("authPrompt.failedToSetKey", "Failed to set key"),
       },
+      authPreferences: {
+        header: getString("authPreferences.header", "Onion Services Authentication"),
+        overview: getString("authPreferences.overview", "Some onion services require that you identify yourself with a key"),
+        savedKeys: getString("authPreferences.savedKeys", "Saved Keys"),
+        dialogTitle: getString("authPreferences.dialogTitle", "Onion Services Keys"),
+        dialogIntro: getString("authPreferences.dialogIntro", "Keys for the following onionsites are stored on your computer"),
+        onionSite: getString("authPreferences.onionSite", "Onionsite"),
+        onionKey: getString("authPreferences.onionKey", "Key"),
+        remove: getString("authPreferences.remove", "Remove"),
+        removeAll: getString("authPreferences.removeAll", "Remove All"),
+        failedToGetKeys: getString("authPreferences.failedToGetKeys", "Failed to get keys"),
+        failedToRemoveKey: getString("authPreferences.failedToRemoveKey", "Failed to remove key"),
+      },
     };
 
     return retval;



More information about the tor-commits mailing list