| ... | ... | @@ -2,350 +2,417 @@ | 
| 2 | 2 |  
 | 
| 3 | 3 |  "use strict";
 | 
| 4 | 4 |  
 | 
| 5 |  | -const OnionAuthPrompt = (function () {
 | 
|  | 5 | +var OnionAuthPrompt = {
 | 
| 6 | 6 |    // Only import to our internal scope, rather than the global scope of
 | 
| 7 | 7 |    // browser.xhtml.
 | 
| 8 |  | -  const lazy = {};
 | 
| 9 |  | -  ChromeUtils.defineESModuleGetters(lazy, {
 | 
| 10 |  | -    TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
 | 
| 11 |  | -    TorStrings: "resource://gre/modules/TorStrings.sys.mjs",
 | 
| 12 |  | -    CommonUtils: "resource://services-common/utils.sys.mjs",
 | 
| 13 |  | -  });
 | 
| 14 |  | -
 | 
| 15 |  | -  // OnionServicesAuthPrompt objects run within the main/chrome process.
 | 
| 16 |  | -  // aReason is the topic passed within the observer notification that is
 | 
| 17 |  | -  // causing this auth prompt to be displayed.
 | 
| 18 |  | -  function OnionServicesAuthPrompt(aBrowser, aFailedURI, aReason, aOnionName) {
 | 
| 19 |  | -    this._browser = aBrowser;
 | 
| 20 |  | -    this._failedURI = aFailedURI;
 | 
| 21 |  | -    this._reasonForPrompt = aReason;
 | 
| 22 |  | -    this._onionHostname = aOnionName;
 | 
| 23 |  | -  }
 | 
| 24 |  | -
 | 
| 25 |  | -  const topics = {
 | 
|  | 8 | +  _lazy: {},
 | 
|  | 9 | +
 | 
|  | 10 | +  /**
 | 
|  | 11 | +   * The topics to listen to.
 | 
|  | 12 | +   *
 | 
|  | 13 | +   * @type {Object<string, string>}
 | 
|  | 14 | +   */
 | 
|  | 15 | +  _topics: {
 | 
| 26 | 16 |      clientAuthMissing: "tor-onion-services-clientauth-missing",
 | 
| 27 | 17 |      clientAuthIncorrect: "tor-onion-services-clientauth-incorrect",
 | 
| 28 |  | -  };
 | 
| 29 |  | -
 | 
| 30 |  | -  OnionServicesAuthPrompt.prototype = {
 | 
| 31 |  | -    show(aWarningMessage) {
 | 
| 32 |  | -      let mainAction = {
 | 
| 33 |  | -        label: lazy.TorStrings.onionServices.authPrompt.done,
 | 
| 34 |  | -        accessKey: lazy.TorStrings.onionServices.authPrompt.doneAccessKey,
 | 
| 35 |  | -        leaveOpen: true, // Callback is responsible for closing the notification.
 | 
| 36 |  | -        callback: this._onDone.bind(this),
 | 
| 37 |  | -      };
 | 
| 38 |  | -
 | 
| 39 |  | -      let dialogBundle = Services.strings.createBundle(
 | 
| 40 |  | -        "chrome://global/locale/dialog.properties"
 | 
| 41 |  | -      );
 | 
| 42 |  | -
 | 
| 43 |  | -      let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
 | 
| 44 |  | -      if (!cancelAccessKey) {
 | 
| 45 |  | -        cancelAccessKey = "c";
 | 
| 46 |  | -      } // required by PopupNotifications.show()
 | 
| 47 |  | -
 | 
| 48 |  | -      let cancelAction = {
 | 
| 49 |  | -        label: dialogBundle.GetStringFromName("button-cancel"),
 | 
| 50 |  | -        accessKey: cancelAccessKey,
 | 
| 51 |  | -        callback: this._onCancel.bind(this),
 | 
| 52 |  | -      };
 | 
| 53 |  | -
 | 
| 54 |  | -      let _this = this;
 | 
| 55 |  | -      let options = {
 | 
| 56 |  | -        autofocus: true,
 | 
| 57 |  | -        hideClose: true,
 | 
| 58 |  | -        persistent: true,
 | 
| 59 |  | -        removeOnDismissal: false,
 | 
| 60 |  | -        eventCallback(aTopic) {
 | 
| 61 |  | -          if (aTopic === "showing") {
 | 
| 62 |  | -            _this._onPromptShowing(aWarningMessage);
 | 
| 63 |  | -          } else if (aTopic === "shown") {
 | 
| 64 |  | -            _this._onPromptShown();
 | 
| 65 |  | -          } else if (aTopic === "removed") {
 | 
| 66 |  | -            _this._onPromptRemoved();
 | 
| 67 |  | -          }
 | 
| 68 |  | -        },
 | 
| 69 |  | -      };
 | 
| 70 |  | -
 | 
| 71 |  | -      this._prompt = PopupNotifications.show(
 | 
| 72 |  | -        this._browser,
 | 
| 73 |  | -        "tor-clientauth",
 | 
| 74 |  | -        "",
 | 
| 75 |  | -        "tor-clientauth-notification-icon",
 | 
| 76 |  | -        mainAction,
 | 
| 77 |  | -        [cancelAction],
 | 
| 78 |  | -        options
 | 
| 79 |  | -      );
 | 
| 80 |  | -    },
 | 
| 81 |  | -
 | 
| 82 |  | -    _onPromptShowing(aWarningMessage) {
 | 
| 83 |  | -      let xulDoc = this._browser.ownerDocument;
 | 
| 84 |  | -      let descElem = xulDoc.getElementById("tor-clientauth-notification-desc");
 | 
| 85 |  | -      if (descElem) {
 | 
| 86 |  | -        // Handle replacement of the onion name within the localized
 | 
| 87 |  | -        // string ourselves so we can show the onion name as bold text.
 | 
| 88 |  | -        // We do this by splitting the localized string and creating
 | 
| 89 |  | -        // several HTML <span> elements.
 | 
| 90 |  | -        const fmtString = lazy.TorStrings.onionServices.authPrompt.description;
 | 
| 91 |  | -        const [prefix, suffix] = fmtString.split("%S");
 | 
| 92 |  | -
 | 
| 93 |  | -        const domainEl = xulDoc.createElement("span");
 | 
| 94 |  | -        domainEl.id = "tor-clientauth-notification-onionname";
 | 
| 95 |  | -        domainEl.textContent = TorUIUtils.shortenOnionAddress(
 | 
| 96 |  | -          this._onionHostname
 | 
| 97 |  | -        );
 | 
| 98 |  | -
 | 
| 99 |  | -        descElem.replaceChildren(prefix, domainEl, suffix);
 | 
| 100 |  | -      }
 | 
| 101 |  | -
 | 
| 102 |  | -      // Set "Learn More" label and href.
 | 
| 103 |  | -      let learnMoreElem = xulDoc.getElementById(
 | 
| 104 |  | -        "tor-clientauth-notification-learnmore"
 | 
| 105 |  | -      );
 | 
| 106 |  | -      if (learnMoreElem) {
 | 
| 107 |  | -        learnMoreElem.setAttribute(
 | 
| 108 |  | -          "value",
 | 
| 109 |  | -          lazy.TorStrings.onionServices.learnMore
 | 
| 110 |  | -        );
 | 
| 111 |  | -        learnMoreElem.setAttribute(
 | 
| 112 |  | -          "href",
 | 
| 113 |  | -          "about:manual#onion-services_onion-service-authentication"
 | 
| 114 |  | -        );
 | 
| 115 |  | -        learnMoreElem.setAttribute("useoriginprincipal", "true");
 | 
| 116 |  | -      }
 | 
| 117 |  | -
 | 
| 118 |  | -      this._showWarning(aWarningMessage);
 | 
| 119 |  | -      let checkboxElem = this._getCheckboxElement();
 | 
| 120 |  | -      if (checkboxElem) {
 | 
| 121 |  | -        checkboxElem.checked = false;
 | 
| 122 |  | -      }
 | 
| 123 |  | -    },
 | 
| 124 |  | -
 | 
| 125 |  | -    _onPromptShown() {
 | 
| 126 |  | -      let keyElem = this._getKeyElement();
 | 
| 127 |  | -      if (keyElem) {
 | 
| 128 |  | -        keyElem.setAttribute(
 | 
| 129 |  | -          "placeholder",
 | 
| 130 |  | -          lazy.TorStrings.onionServices.authPrompt.keyPlaceholder
 | 
| 131 |  | -        );
 | 
| 132 |  | -        this._boundOnKeyFieldKeyPress = this._onKeyFieldKeyPress.bind(this);
 | 
| 133 |  | -        this._boundOnKeyFieldInput = this._onKeyFieldInput.bind(this);
 | 
| 134 |  | -        keyElem.addEventListener("keypress", this._boundOnKeyFieldKeyPress);
 | 
| 135 |  | -        keyElem.addEventListener("input", this._boundOnKeyFieldInput);
 | 
| 136 |  | -        keyElem.focus();
 | 
| 137 |  | -      }
 | 
| 138 |  | -    },
 | 
| 139 |  | -
 | 
| 140 |  | -    _onPromptRemoved() {
 | 
| 141 |  | -      if (this._boundOnKeyFieldKeyPress) {
 | 
| 142 |  | -        let keyElem = this._getKeyElement();
 | 
| 143 |  | -        if (keyElem) {
 | 
| 144 |  | -          keyElem.value = "";
 | 
| 145 |  | -          keyElem.removeEventListener(
 | 
| 146 |  | -            "keypress",
 | 
| 147 |  | -            this._boundOnKeyFieldKeyPress
 | 
| 148 |  | -          );
 | 
| 149 |  | -          this._boundOnKeyFieldKeyPress = undefined;
 | 
| 150 |  | -          keyElem.removeEventListener("input", this._boundOnKeyFieldInput);
 | 
| 151 |  | -          this._boundOnKeyFieldInput = undefined;
 | 
|  | 18 | +  },
 | 
|  | 19 | +
 | 
|  | 20 | +  /**
 | 
|  | 21 | +   * @typedef {object} PromptDetails
 | 
|  | 22 | +   *
 | 
|  | 23 | +   * @property {Browser} browser - The browser this prompt is for.
 | 
|  | 24 | +   * @property {string} cause - The notification that cause this prompt.
 | 
|  | 25 | +   * @property {string} onionHost - The onion host name.
 | 
|  | 26 | +   * @property {nsIURI} uri - The browser URI when the notification was
 | 
|  | 27 | +   *   triggered.
 | 
|  | 28 | +   * @property {string} onionServiceId - The onion service ID for this host.
 | 
|  | 29 | +   * @property {Notification} [notification] - The notification instance for
 | 
|  | 30 | +   *   this prompt.
 | 
|  | 31 | +   */
 | 
|  | 32 | +
 | 
|  | 33 | +  /**
 | 
|  | 34 | +   * The currently shown details in the prompt.
 | 
|  | 35 | +   */
 | 
|  | 36 | +  _shownDetails: null,
 | 
|  | 37 | +
 | 
|  | 38 | +  /**
 | 
|  | 39 | +   * Used for logging to represent PromptDetails.
 | 
|  | 40 | +   *
 | 
|  | 41 | +   * @param {PromptDetails} details - The details to represent.
 | 
|  | 42 | +   * @returns {string} - The representation of these details.
 | 
|  | 43 | +   */
 | 
|  | 44 | +  _detailsRepr(details) {
 | 
|  | 45 | +    if (!details) {
 | 
|  | 46 | +      return "none";
 | 
|  | 47 | +    }
 | 
|  | 48 | +    return `${details.browser.browserId}:${details.onionHost}`;
 | 
|  | 49 | +  },
 | 
|  | 50 | +
 | 
|  | 51 | +  /**
 | 
|  | 52 | +   * Show a new prompt, using the given details.
 | 
|  | 53 | +   *
 | 
|  | 54 | +   * @param {PromptDetails} details - The details to show.
 | 
|  | 55 | +   */
 | 
|  | 56 | +  show(details) {
 | 
|  | 57 | +    this._logger.debug(`New Notification: ${this._detailsRepr(details)}`);
 | 
|  | 58 | +
 | 
|  | 59 | +    let mainAction = {
 | 
|  | 60 | +      label: this.TorStrings.onionServices.authPrompt.done,
 | 
|  | 61 | +      accessKey: this.TorStrings.onionServices.authPrompt.doneAccessKey,
 | 
|  | 62 | +      leaveOpen: true, // Callback is responsible for closing the notification.
 | 
|  | 63 | +      callback: this._onDone.bind(this),
 | 
|  | 64 | +    };
 | 
|  | 65 | +
 | 
|  | 66 | +    let dialogBundle = Services.strings.createBundle(
 | 
|  | 67 | +      "chrome://global/locale/dialog.properties"
 | 
|  | 68 | +    );
 | 
|  | 69 | +
 | 
|  | 70 | +    let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
 | 
|  | 71 | +    if (!cancelAccessKey) {
 | 
|  | 72 | +      cancelAccessKey = "c";
 | 
|  | 73 | +    } // required by PopupNotifications.show()
 | 
|  | 74 | +
 | 
|  | 75 | +    // The first secondarybuttoncommand (cancelAction) should be triggered when
 | 
|  | 76 | +    // the user presses "Escape".
 | 
|  | 77 | +    let cancelAction = {
 | 
|  | 78 | +      label: dialogBundle.GetStringFromName("button-cancel"),
 | 
|  | 79 | +      accessKey: cancelAccessKey,
 | 
|  | 80 | +      callback: this._onCancel.bind(this),
 | 
|  | 81 | +    };
 | 
|  | 82 | +
 | 
|  | 83 | +    let options = {
 | 
|  | 84 | +      autofocus: true,
 | 
|  | 85 | +      hideClose: true,
 | 
|  | 86 | +      persistent: true,
 | 
|  | 87 | +      removeOnDismissal: false,
 | 
|  | 88 | +      eventCallback: topic => {
 | 
|  | 89 | +        if (topic === "showing") {
 | 
|  | 90 | +          this._onPromptShowing(details);
 | 
|  | 91 | +        } else if (topic === "shown") {
 | 
|  | 92 | +          this._onPromptShown();
 | 
|  | 93 | +        } else if (topic === "removed") {
 | 
|  | 94 | +          this._onPromptRemoved(details);
 | 
| 152 | 95 |          }
 | 
|  | 96 | +      },
 | 
|  | 97 | +    };
 | 
|  | 98 | +
 | 
|  | 99 | +    details.notification = PopupNotifications.show(
 | 
|  | 100 | +      details.browser,
 | 
|  | 101 | +      "tor-clientauth",
 | 
|  | 102 | +      "",
 | 
|  | 103 | +      "tor-clientauth-notification-icon",
 | 
|  | 104 | +      mainAction,
 | 
|  | 105 | +      [cancelAction],
 | 
|  | 106 | +      options
 | 
|  | 107 | +    );
 | 
|  | 108 | +  },
 | 
|  | 109 | +
 | 
|  | 110 | +  /**
 | 
|  | 111 | +   * Callback when the prompt is about to be shown.
 | 
|  | 112 | +   *
 | 
|  | 113 | +   * @param {PromptDetails?} details - The details to show, or null to shown
 | 
|  | 114 | +   *   none.
 | 
|  | 115 | +   */
 | 
|  | 116 | +  _onPromptShowing(details) {
 | 
|  | 117 | +    if (details === this._shownDetails) {
 | 
|  | 118 | +      // The last shown details match this one exactly.
 | 
|  | 119 | +      // This happens when we switch tabs to a page that has no prompt and then
 | 
|  | 120 | +      // switch back.
 | 
|  | 121 | +      // We don't want to reset the current state in this case.
 | 
|  | 122 | +      // In particular, we keep the current _keyInput value and _persistCheckbox
 | 
|  | 123 | +      // the same.
 | 
|  | 124 | +      this._logger.debug(`Already showing: ${this._detailsRepr(details)}`);
 | 
|  | 125 | +      return;
 | 
|  | 126 | +    }
 | 
|  | 127 | +
 | 
|  | 128 | +    this._logger.debug(`Now showing: ${this._detailsRepr(details)}`);
 | 
|  | 129 | +
 | 
|  | 130 | +    this._shownDetails = details;
 | 
|  | 131 | +
 | 
|  | 132 | +    // Clear the key input.
 | 
|  | 133 | +    // In particular, clear the input when switching tabs.
 | 
|  | 134 | +    this._keyInput.value = "";
 | 
|  | 135 | +    this._persistCheckbox.checked = false;
 | 
|  | 136 | +
 | 
|  | 137 | +    // Handle replacement of the onion name within the localized
 | 
|  | 138 | +    // string ourselves so we can show the onion name as bold text.
 | 
|  | 139 | +    // We do this by splitting the localized string and creating
 | 
|  | 140 | +    // several HTML <span> elements.
 | 
|  | 141 | +    const fmtString = this.TorStrings.onionServices.authPrompt.description;
 | 
|  | 142 | +    const [prefix, suffix] = fmtString.split("%S");
 | 
|  | 143 | +
 | 
|  | 144 | +    const domainEl = document.createElement("span");
 | 
|  | 145 | +    domainEl.id = "tor-clientauth-notification-onionname";
 | 
|  | 146 | +    domainEl.textContent = TorUIUtils.shortenOnionAddress(
 | 
|  | 147 | +      this._shownDetails?.onionHost ?? ""
 | 
|  | 148 | +    );
 | 
|  | 149 | +
 | 
|  | 150 | +    this._descriptionEl.replaceChildren(prefix, domainEl, suffix);
 | 
|  | 151 | +
 | 
|  | 152 | +    this._showWarning(undefined);
 | 
|  | 153 | +  },
 | 
|  | 154 | +
 | 
|  | 155 | +  /**
 | 
|  | 156 | +   * Callback after the prompt is shown.
 | 
|  | 157 | +   */
 | 
|  | 158 | +  _onPromptShown() {
 | 
|  | 159 | +    this._keyInput.focus();
 | 
|  | 160 | +  },
 | 
|  | 161 | +
 | 
|  | 162 | +  /**
 | 
|  | 163 | +   * Callback when a Notification is removed.
 | 
|  | 164 | +   *
 | 
|  | 165 | +   * @param {PromptDetails} details - The details for the removed notification.
 | 
|  | 166 | +   */
 | 
|  | 167 | +  _onPromptRemoved(details) {
 | 
|  | 168 | +    if (details !== this._shownDetails) {
 | 
|  | 169 | +      // Removing the notification for some other page.
 | 
|  | 170 | +      // For example, closing another tab that also requires authentication.
 | 
|  | 171 | +      this._logger.debug(`Removed not shown: ${this._detailsRepr(details)}`);
 | 
|  | 172 | +      return;
 | 
|  | 173 | +    }
 | 
|  | 174 | +    this._logger.debug(`Removed shown: ${this._detailsRepr(details)}`);
 | 
|  | 175 | +    // Reset the prompt as a precaution.
 | 
|  | 176 | +    // In particular, we want to clear the input so that the entered key does
 | 
|  | 177 | +    // not persist.
 | 
|  | 178 | +    this._onPromptShowing(null);
 | 
|  | 179 | +  },
 | 
|  | 180 | +
 | 
|  | 181 | +  /**
 | 
|  | 182 | +   * Callback when the user submits the key.
 | 
|  | 183 | +   */
 | 
|  | 184 | +  async _onDone() {
 | 
|  | 185 | +    this._logger.debug(
 | 
|  | 186 | +      `Sumbitting key: ${this._detailsRepr(this._shownDetails)}`
 | 
|  | 187 | +    );
 | 
|  | 188 | +
 | 
|  | 189 | +    // Grab the details before they might change as we await.
 | 
|  | 190 | +    const { browser, onionServiceId, notification } = this._shownDetails;
 | 
|  | 191 | +    const isPermanent = this._persistCheckbox.checked;
 | 
|  | 192 | +
 | 
|  | 193 | +    const base64key = this._keyToBase64(this._keyInput.value);
 | 
|  | 194 | +    if (!base64key) {
 | 
|  | 195 | +      this._showWarning(this.TorStrings.onionServices.authPrompt.invalidKey);
 | 
|  | 196 | +      return;
 | 
|  | 197 | +    }
 | 
|  | 198 | +
 | 
|  | 199 | +    try {
 | 
|  | 200 | +      const provider = await this._lazy.TorProviderBuilder.build();
 | 
|  | 201 | +      await provider.onionAuthAdd(onionServiceId, base64key, isPermanent);
 | 
|  | 202 | +    } catch (e) {
 | 
|  | 203 | +      if (e.torMessage) {
 | 
|  | 204 | +        this._showWarning(e.torMessage);
 | 
|  | 205 | +      } else {
 | 
|  | 206 | +        this._logger.error(`Failed to set key for ${onionServiceId}`, e);
 | 
|  | 207 | +        this._showWarning(
 | 
|  | 208 | +          this.TorStrings.onionServices.authPrompt.failedToSetKey
 | 
|  | 209 | +        );
 | 
| 153 | 210 |        }
 | 
| 154 |  | -    },
 | 
| 155 |  | -
 | 
| 156 |  | -    _onKeyFieldKeyPress(aEvent) {
 | 
| 157 |  | -      if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
 | 
| 158 |  | -        this._onDone();
 | 
| 159 |  | -      } else if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
 | 
| 160 |  | -        this._prompt.remove();
 | 
| 161 |  | -        this._onCancel();
 | 
| 162 |  | -      }
 | 
| 163 |  | -    },
 | 
| 164 |  | -
 | 
| 165 |  | -    _onKeyFieldInput(aEvent) {
 | 
| 166 |  | -      this._showWarning(undefined); // Remove the warning.
 | 
| 167 |  | -    },
 | 
| 168 |  | -
 | 
| 169 |  | -    async _onDone() {
 | 
| 170 |  | -      const keyElem = this._getKeyElement();
 | 
| 171 |  | -      if (!keyElem) {
 | 
| 172 |  | -        return;
 | 
| 173 |  | -      }
 | 
| 174 |  | -
 | 
| 175 |  | -      const base64key = this._keyToBase64(keyElem.value);
 | 
| 176 |  | -      if (!base64key) {
 | 
| 177 |  | -        this._showWarning(lazy.TorStrings.onionServices.authPrompt.invalidKey);
 | 
| 178 |  | -        return;
 | 
| 179 |  | -      }
 | 
| 180 |  | -
 | 
| 181 |  | -      this._prompt.remove();
 | 
| 182 |  | -
 | 
| 183 |  | -      const controllerFailureMsg =
 | 
| 184 |  | -        lazy.TorStrings.onionServices.authPrompt.failedToSetKey;
 | 
|  | 211 | +      return;
 | 
|  | 212 | +    }
 | 
|  | 213 | +
 | 
|  | 214 | +    notification.remove();
 | 
|  | 215 | +    // Success! Reload the page.
 | 
|  | 216 | +    browser.sendMessageToActor("Browser:Reload", {}, "BrowserTab");
 | 
|  | 217 | +  },
 | 
|  | 218 | +
 | 
|  | 219 | +  /**
 | 
|  | 220 | +   * Callback when the user dismisses the prompt.
 | 
|  | 221 | +   */
 | 
|  | 222 | +  _onCancel() {
 | 
|  | 223 | +    // Arrange for an error page to be displayed:
 | 
|  | 224 | +    // we build a short script calling docShell.displayError()
 | 
|  | 225 | +    // and we pass it as a data: URI to loadFrameScript(),
 | 
|  | 226 | +    // which runs it in the content frame which triggered
 | 
|  | 227 | +    // this authentication prompt.
 | 
|  | 228 | +    this._logger.debug(`Cancelling: ${this._detailsRepr(this._shownDetails)}`);
 | 
|  | 229 | +
 | 
|  | 230 | +    const { browser, cause, uri } = this._shownDetails;
 | 
|  | 231 | +    const errorCode =
 | 
|  | 232 | +      cause === this._topics.clientAuthMissing
 | 
|  | 233 | +        ? Cr.NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH
 | 
|  | 234 | +        : Cr.NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH;
 | 
|  | 235 | +    const io =
 | 
|  | 236 | +      'ChromeUtils.import("resource://gre/modules/Services.jsm").Services.io';
 | 
|  | 237 | +
 | 
|  | 238 | +    browser.messageManager.loadFrameScript(
 | 
|  | 239 | +      `data:application/_javascript_,${encodeURIComponent(
 | 
|  | 240 | +        `docShell.displayLoadError(${errorCode}, ${io}.newURI(${JSON.stringify(
 | 
|  | 241 | +          uri.spec
 | 
|  | 242 | +        )}), undefined, undefined);`
 | 
|  | 243 | +      )}`,
 | 
|  | 244 | +      false
 | 
|  | 245 | +    );
 | 
|  | 246 | +  },
 | 
|  | 247 | +
 | 
|  | 248 | +  /**
 | 
|  | 249 | +   * Show a warning message to the user or clear the warning.
 | 
|  | 250 | +   *
 | 
|  | 251 | +   * @param {string?} warningMessage - The message to show, or undefined to
 | 
|  | 252 | +   *   clear the current message.
 | 
|  | 253 | +   */
 | 
|  | 254 | +  _showWarning(warningMessage) {
 | 
|  | 255 | +    this._logger.debug(`Showing warning: ${warningMessage}`);
 | 
|  | 256 | +    if (warningMessage) {
 | 
|  | 257 | +      this._warningEl.textContent = warningMessage;
 | 
|  | 258 | +      this._warningEl.removeAttribute("hidden");
 | 
|  | 259 | +      this._keyInput.classList.add("invalid");
 | 
|  | 260 | +    } else {
 | 
|  | 261 | +      this._warningEl.setAttribute("hidden", "true");
 | 
|  | 262 | +      this._keyInput.classList.remove("invalid");
 | 
|  | 263 | +    }
 | 
|  | 264 | +  },
 | 
|  | 265 | +
 | 
|  | 266 | +  /**
 | 
|  | 267 | +   * Convert the user-entered key into base64.
 | 
|  | 268 | +   *
 | 
|  | 269 | +   * @param {string} keyString - The key to convert.
 | 
|  | 270 | +   * @returns {string?} - The base64 representation, or undefined if the given
 | 
|  | 271 | +   *   key was not the correct format.
 | 
|  | 272 | +   */
 | 
|  | 273 | +  _keyToBase64(keyString) {
 | 
|  | 274 | +    if (!keyString) {
 | 
|  | 275 | +      return undefined;
 | 
|  | 276 | +    }
 | 
|  | 277 | +
 | 
|  | 278 | +    let base64key;
 | 
|  | 279 | +    if (keyString.length === 52) {
 | 
|  | 280 | +      // The key is probably base32-encoded. Attempt to decode.
 | 
|  | 281 | +      // Although base32 specifies uppercase letters, we accept lowercase
 | 
|  | 282 | +      // as well because users may type in lowercase or copy a key out of
 | 
|  | 283 | +      // a tor onion-auth file (which uses lowercase).
 | 
|  | 284 | +      let rawKey;
 | 
| 185 | 285 |        try {
 | 
| 186 |  | -        // ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
 | 
| 187 |  | -        const onionServiceIdRegExp =
 | 
| 188 |  | -          /^(.*\.)*(?<onionServiceId>[a-z2-7]{56})\.onion$/i;
 | 
| 189 |  | -        // match() will return null on bad match, causing throw
 | 
| 190 |  | -        const onionServiceId = this._onionHostname
 | 
| 191 |  | -          .match(onionServiceIdRegExp)
 | 
| 192 |  | -          .groups.onionServiceId.toLowerCase();
 | 
| 193 |  | -
 | 
| 194 |  | -        const checkboxElem = this._getCheckboxElement();
 | 
| 195 |  | -        const isPermanent = checkboxElem && checkboxElem.checked;
 | 
| 196 |  | -        const provider = await lazy.TorProviderBuilder.build();
 | 
| 197 |  | -        await provider.onionAuthAdd(onionServiceId, base64key, isPermanent);
 | 
| 198 |  | -        // Success! Reload the page.
 | 
| 199 |  | -        this._browser.sendMessageToActor("Browser:Reload", {}, "BrowserTab");
 | 
| 200 |  | -      } catch (e) {
 | 
| 201 |  | -        if (e.torMessage) {
 | 
| 202 |  | -          this.show(e.torMessage);
 | 
| 203 |  | -        } else {
 | 
| 204 |  | -          console.error(controllerFailureMsg, e);
 | 
| 205 |  | -          this.show(controllerFailureMsg);
 | 
| 206 |  | -        }
 | 
| 207 |  | -      }
 | 
| 208 |  | -    },
 | 
| 209 |  | -
 | 
| 210 |  | -    _onCancel() {
 | 
| 211 |  | -      // Arrange for an error page to be displayed:
 | 
| 212 |  | -      // we build a short script calling docShell.displayError()
 | 
| 213 |  | -      // and we pass it as a data: URI to loadFrameScript(),
 | 
| 214 |  | -      // which runs it in the content frame which triggered
 | 
| 215 |  | -      // this authentication prompt.
 | 
| 216 |  | -      const failedURI = this._failedURI.spec;
 | 
| 217 |  | -      const errorCode =
 | 
| 218 |  | -        this._reasonForPrompt === topics.clientAuthMissing
 | 
| 219 |  | -          ? Cr.NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH
 | 
| 220 |  | -          : Cr.NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH;
 | 
| 221 |  | -      const io =
 | 
| 222 |  | -        'ChromeUtils.import("resource://gre/modules/Services.jsm").Services.io';
 | 
| 223 |  | -
 | 
| 224 |  | -      this._browser.messageManager.loadFrameScript(
 | 
| 225 |  | -        `data:application/_javascript_,${encodeURIComponent(
 | 
| 226 |  | -          `docShell.displayLoadError(${errorCode}, ${io}.newURI(${JSON.stringify(
 | 
| 227 |  | -            failedURI
 | 
| 228 |  | -          )}), undefined, undefined);`
 | 
| 229 |  | -        )}`,
 | 
| 230 |  | -        false
 | 
| 231 |  | -      );
 | 
| 232 |  | -    },
 | 
| 233 |  | -
 | 
| 234 |  | -    _getKeyElement() {
 | 
| 235 |  | -      let xulDoc = this._browser.ownerDocument;
 | 
| 236 |  | -      return xulDoc.getElementById("tor-clientauth-notification-key");
 | 
| 237 |  | -    },
 | 
| 238 |  | -
 | 
| 239 |  | -    _getCheckboxElement() {
 | 
| 240 |  | -      let xulDoc = this._browser.ownerDocument;
 | 
| 241 |  | -      return xulDoc.getElementById("tor-clientauth-persistkey-checkbox");
 | 
| 242 |  | -    },
 | 
| 243 |  | -
 | 
| 244 |  | -    _showWarning(aWarningMessage) {
 | 
| 245 |  | -      let xulDoc = this._browser.ownerDocument;
 | 
| 246 |  | -      let warningElem = xulDoc.getElementById("tor-clientauth-warning");
 | 
| 247 |  | -      let keyElem = this._getKeyElement();
 | 
| 248 |  | -      if (warningElem) {
 | 
| 249 |  | -        if (aWarningMessage) {
 | 
| 250 |  | -          warningElem.textContent = aWarningMessage;
 | 
| 251 |  | -          warningElem.removeAttribute("hidden");
 | 
| 252 |  | -          if (keyElem) {
 | 
| 253 |  | -            keyElem.className = "invalid";
 | 
| 254 |  | -          }
 | 
| 255 |  | -        } else {
 | 
| 256 |  | -          warningElem.setAttribute("hidden", "true");
 | 
| 257 |  | -          if (keyElem) {
 | 
| 258 |  | -            keyElem.className = "";
 | 
| 259 |  | -          }
 | 
| 260 |  | -        }
 | 
| 261 |  | -      }
 | 
| 262 |  | -    },
 | 
| 263 |  | -
 | 
| 264 |  | -    // Returns undefined if the key is the wrong length or format.
 | 
| 265 |  | -    _keyToBase64(aKeyString) {
 | 
| 266 |  | -      if (!aKeyString) {
 | 
| 267 |  | -        return undefined;
 | 
| 268 |  | -      }
 | 
|  | 286 | +        rawKey = this._lazy.CommonUtils.decodeBase32(keyString.toUpperCase());
 | 
|  | 287 | +      } catch (e) {}
 | 
| 269 | 288 |  
 | 
| 270 |  | -      let base64key;
 | 
| 271 |  | -      if (aKeyString.length == 52) {
 | 
| 272 |  | -        // The key is probably base32-encoded. Attempt to decode.
 | 
| 273 |  | -        // Although base32 specifies uppercase letters, we accept lowercase
 | 
| 274 |  | -        // as well because users may type in lowercase or copy a key out of
 | 
| 275 |  | -        // a tor onion-auth file (which uses lowercase).
 | 
| 276 |  | -        let rawKey;
 | 
|  | 289 | +      if (rawKey) {
 | 
| 277 | 290 |          try {
 | 
| 278 |  | -          rawKey = lazy.CommonUtils.decodeBase32(aKeyString.toUpperCase());
 | 
|  | 291 | +          base64key = btoa(rawKey);
 | 
| 279 | 292 |          } catch (e) {}
 | 
| 280 |  | -
 | 
| 281 |  | -        if (rawKey) {
 | 
| 282 |  | -          try {
 | 
| 283 |  | -            base64key = btoa(rawKey);
 | 
| 284 |  | -          } catch (e) {}
 | 
| 285 |  | -        }
 | 
| 286 |  | -      } else if (
 | 
| 287 |  | -        aKeyString.length == 44 &&
 | 
| 288 |  | -        /^[a-zA-Z0-9+/]*=*$/.test(aKeyString)
 | 
| 289 |  | -      ) {
 | 
| 290 |  | -        // The key appears to be a correctly formatted base64 value. If not,
 | 
| 291 |  | -        // tor will return an error when we try to add the key via the
 | 
| 292 |  | -        // control port.
 | 
| 293 |  | -        base64key = aKeyString;
 | 
| 294 | 293 |        }
 | 
| 295 |  | -
 | 
| 296 |  | -      return base64key;
 | 
| 297 |  | -    },
 | 
| 298 |  | -  };
 | 
| 299 |  | -
 | 
| 300 |  | -  let retval = {
 | 
| 301 |  | -    init() {
 | 
| 302 |  | -      Services.obs.addObserver(this, topics.clientAuthMissing);
 | 
| 303 |  | -      Services.obs.addObserver(this, topics.clientAuthIncorrect);
 | 
| 304 |  | -    },
 | 
| 305 |  | -
 | 
| 306 |  | -    uninit() {
 | 
| 307 |  | -      Services.obs.removeObserver(this, topics.clientAuthMissing);
 | 
| 308 |  | -      Services.obs.removeObserver(this, topics.clientAuthIncorrect);
 | 
| 309 |  | -    },
 | 
| 310 |  | -
 | 
| 311 |  | -    // aSubject is the DOM Window or browser where the prompt should be shown.
 | 
| 312 |  | -    // aData contains the .onion name.
 | 
| 313 |  | -    observe(aSubject, aTopic, aData) {
 | 
| 314 |  | -      if (
 | 
| 315 |  | -        aTopic != topics.clientAuthMissing &&
 | 
| 316 |  | -        aTopic != topics.clientAuthIncorrect
 | 
| 317 |  | -      ) {
 | 
| 318 |  | -        return;
 | 
| 319 |  | -      }
 | 
| 320 |  | -
 | 
| 321 |  | -      let browser;
 | 
| 322 |  | -      if (aSubject instanceof Ci.nsIDOMWindow) {
 | 
| 323 |  | -        let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
 | 
| 324 |  | -        browser = contentWindow.docShell.chromeEventHandler;
 | 
| 325 |  | -      } else {
 | 
| 326 |  | -        browser = aSubject.QueryInterface(Ci.nsIBrowser);
 | 
| 327 |  | -      }
 | 
| 328 |  | -
 | 
| 329 |  | -      if (!gBrowser.browsers.some(aBrowser => aBrowser == browser)) {
 | 
| 330 |  | -        return; // This window does not contain the subject browser; ignore.
 | 
|  | 294 | +    } else if (
 | 
|  | 295 | +      keyString.length === 44 &&
 | 
|  | 296 | +      /^[a-zA-Z0-9+/]*=*$/.test(keyString)
 | 
|  | 297 | +    ) {
 | 
|  | 298 | +      // The key appears to be a correctly formatted base64 value. If not,
 | 
|  | 299 | +      // tor will return an error when we try to add the key via the
 | 
|  | 300 | +      // control port.
 | 
|  | 301 | +      base64key = keyString;
 | 
|  | 302 | +    }
 | 
|  | 303 | +
 | 
|  | 304 | +    return base64key;
 | 
|  | 305 | +  },
 | 
|  | 306 | +
 | 
|  | 307 | +  /**
 | 
|  | 308 | +   * Initialize the authentication prompt.
 | 
|  | 309 | +   */
 | 
|  | 310 | +  init() {
 | 
|  | 311 | +    this._logger = console.createInstance({
 | 
|  | 312 | +      prefix: "OnionAuthPrompt",
 | 
|  | 313 | +      maxLogLevel: "Warn",
 | 
|  | 314 | +      maxLogLevelPref: "browser.onionAuthPrompt.loglevel",
 | 
|  | 315 | +    });
 | 
|  | 316 | +
 | 
|  | 317 | +    const { TorStrings } = ChromeUtils.importESModule(
 | 
|  | 318 | +      "resource://gre/modules/TorStrings.sys.mjs"
 | 
|  | 319 | +    );
 | 
|  | 320 | +    this.TorStrings = TorStrings;
 | 
|  | 321 | +    ChromeUtils.defineESModuleGetters(this._lazy, {
 | 
|  | 322 | +      TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
 | 
|  | 323 | +      CommonUtils: "resource://services-common/utils.sys.mjs",
 | 
|  | 324 | +    });
 | 
|  | 325 | +
 | 
|  | 326 | +    this._keyInput = document.getElementById("tor-clientauth-notification-key");
 | 
|  | 327 | +    this._persistCheckbox = document.getElementById(
 | 
|  | 328 | +      "tor-clientauth-persistkey-checkbox"
 | 
|  | 329 | +    );
 | 
|  | 330 | +    this._warningEl = document.getElementById("tor-clientauth-warning");
 | 
|  | 331 | +    this._descriptionEl = document.getElementById(
 | 
|  | 332 | +      "tor-clientauth-notification-desc"
 | 
|  | 333 | +    );
 | 
|  | 334 | +
 | 
|  | 335 | +    // Set "Learn More" label and href.
 | 
|  | 336 | +    const learnMoreElem = document.getElementById(
 | 
|  | 337 | +      "tor-clientauth-notification-learnmore"
 | 
|  | 338 | +    );
 | 
|  | 339 | +    learnMoreElem.setAttribute(
 | 
|  | 340 | +      "value",
 | 
|  | 341 | +      this.TorStrings.onionServices.learnMore
 | 
|  | 342 | +    );
 | 
|  | 343 | +
 | 
|  | 344 | +    this._keyInput.setAttribute(
 | 
|  | 345 | +      "placeholder",
 | 
|  | 346 | +      this.TorStrings.onionServices.authPrompt.keyPlaceholder
 | 
|  | 347 | +    );
 | 
|  | 348 | +    this._keyInput.addEventListener("keydown", event => {
 | 
|  | 349 | +      if (event.key === "Enter") {
 | 
|  | 350 | +        event.preventDefault();
 | 
|  | 351 | +        this._onDone();
 | 
| 331 | 352 |        }
 | 
| 332 |  | -
 | 
| 333 |  | -      let failedURI = browser.currentURI;
 | 
| 334 |  | -      let authPrompt = new OnionServicesAuthPrompt(
 | 
| 335 |  | -        browser,
 | 
| 336 |  | -        failedURI,
 | 
| 337 |  | -        aTopic,
 | 
| 338 |  | -        aData
 | 
|  | 353 | +    });
 | 
|  | 354 | +    this._keyInput.addEventListener("input", event => {
 | 
|  | 355 | +      // Remove the warning.
 | 
|  | 356 | +      this._showWarning(undefined);
 | 
|  | 357 | +    });
 | 
|  | 358 | +
 | 
|  | 359 | +    Services.obs.addObserver(this, this._topics.clientAuthMissing);
 | 
|  | 360 | +    Services.obs.addObserver(this, this._topics.clientAuthIncorrect);
 | 
|  | 361 | +  },
 | 
|  | 362 | +
 | 
|  | 363 | +  /**
 | 
|  | 364 | +   * Un-initialize the authentication prompt.
 | 
|  | 365 | +   */
 | 
|  | 366 | +  uninit() {
 | 
|  | 367 | +    Services.obs.removeObserver(this, this._topics.clientAuthMissing);
 | 
|  | 368 | +    Services.obs.removeObserver(this, this._topics.clientAuthIncorrect);
 | 
|  | 369 | +  },
 | 
|  | 370 | +
 | 
|  | 371 | +  observe(subject, topic, data) {
 | 
|  | 372 | +    if (
 | 
|  | 373 | +      topic !== this._topics.clientAuthMissing &&
 | 
|  | 374 | +      topic !== this._topics.clientAuthIncorrect
 | 
|  | 375 | +    ) {
 | 
|  | 376 | +      return;
 | 
|  | 377 | +    }
 | 
|  | 378 | +
 | 
|  | 379 | +    // "subject" is the DOM window or browser where the prompt should be shown.
 | 
|  | 380 | +    let browser;
 | 
|  | 381 | +    if (subject instanceof Ci.nsIDOMWindow) {
 | 
|  | 382 | +      let contentWindow = subject.QueryInterface(Ci.nsIDOMWindow);
 | 
|  | 383 | +      browser = contentWindow.docShell.chromeEventHandler;
 | 
|  | 384 | +    } else {
 | 
|  | 385 | +      browser = subject.QueryInterface(Ci.nsIBrowser);
 | 
|  | 386 | +    }
 | 
|  | 387 | +
 | 
|  | 388 | +    if (!gBrowser.browsers.includes(browser)) {
 | 
|  | 389 | +      // This window does not contain the subject browser.
 | 
|  | 390 | +      this._logger.debug(
 | 
|  | 391 | +        `Window ${window.docShell.outerWindowID}: Ignoring ${topic}`
 | 
| 339 | 392 |        );
 | 
| 340 |  | -      authPrompt.show(undefined);
 | 
| 341 |  | -    },
 | 
| 342 |  | -  };
 | 
| 343 |  | -
 | 
| 344 |  | -  return retval;
 | 
| 345 |  | -})(); /* OnionAuthPrompt */
 | 
| 346 |  | -
 | 
| 347 |  | -Object.defineProperty(this, "OnionAuthPrompt", {
 | 
| 348 |  | -  value: OnionAuthPrompt,
 | 
| 349 |  | -  enumerable: true,
 | 
| 350 |  | -  writable: false,
 | 
| 351 |  | -}); | 
|  | 393 | +      return;
 | 
|  | 394 | +    }
 | 
|  | 395 | +    this._logger.debug(
 | 
|  | 396 | +      `Window ${window.docShell.outerWindowID}: Handling ${topic}`
 | 
|  | 397 | +    );
 | 
|  | 398 | +
 | 
|  | 399 | +    const onionHost = data;
 | 
|  | 400 | +    // ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
 | 
|  | 401 | +    const onionServiceId = onionHost
 | 
|  | 402 | +      .match(/^(.*\.)?(?<onionServiceId>[a-z2-7]{56})\.onion$/i)
 | 
|  | 403 | +      ?.groups.onionServiceId.toLowerCase();
 | 
|  | 404 | +    if (!onionServiceId) {
 | 
|  | 405 | +      this._logger.error(`Malformed onion address: ${onionHost}`);
 | 
|  | 406 | +      return;
 | 
|  | 407 | +    }
 | 
|  | 408 | +
 | 
|  | 409 | +    const details = {
 | 
|  | 410 | +      browser,
 | 
|  | 411 | +      cause: topic,
 | 
|  | 412 | +      onionHost,
 | 
|  | 413 | +      uri: browser.currentURI,
 | 
|  | 414 | +      onionServiceId,
 | 
|  | 415 | +    };
 | 
|  | 416 | +    this.show(details);
 | 
|  | 417 | +  },
 | 
|  | 418 | +}; |