richard pushed to branch tor-browser-115.10.0esr-13.5-1 at The Tor Project / Applications / Tor Browser

Commits:

2 changed files:

Changes:

  • browser/components/onionservices/content/authPopup.inc.xhtml
    ... ... @@ -5,7 +5,9 @@
    5 5
         <description id="tor-clientauth-notification-desc"/>
    
    6 6
         <label id="tor-clientauth-notification-learnmore"
    
    7 7
                class="text-link popup-notification-learnmore-link"
    
    8
    -           is="text-link"/>
    
    8
    +           is="text-link"
    
    9
    +           href="about:manual#onion-services_onion-service-authentication"
    
    10
    +           useoriginprincipal="true"/>
    
    9 11
         <html:div>
    
    10 12
           <html:input id="tor-clientauth-notification-key" type="password"/>
    
    11 13
           <html:div id="tor-clientauth-warning"/>
    

  • browser/components/onionservices/content/authPrompt.js
    ... ... @@ -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
    +};