Commit 71ba90c4 authored by Kathleen Brade's avatar Kathleen Brade Committed by Matthew Finkel
Browse files

Bug 30237: Add v3 onion services client authentication prompt

When Tor informs the browser that client authentication is needed,
temporarily load about:blank instead of about:neterror and prompt
for the user's key.

If a correctly formatted key is entered, use Tor's ONION_CLIENT_AUTH_ADD
control port command to add the key (via Torbutton's control port
module) and reload the page.

If the user cancels the prompt, display the standard about:neterror
"Unable to connect" page. This requires a small change to
browser/actors/NetErrorChild.jsm to account for the fact that the
docShell no longer has the failedChannel information. The failedChannel
is used to extract TLS-related error info, which is not applicable
in the case of a canceled .onion authentication prompt.

Add a leaveOpen option to PopupNotifications.show so we can display
error messages within the popup notification doorhanger without
closing the prompt.

Add support for onion services strings to the TorStrings module.

Add support for Tor extended SOCKS errors (Tor proposal 304) to the
socket transport and SOCKS layers. Improved display of all of these
errors will be implemented as part of bug 30025.

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.

Also fixes bug 19251: use enhanced error pages for onion service errors.
parent cd096f94
......@@ -13,6 +13,8 @@ const { RemotePageChild } = ChromeUtils.import(
"resource://gre/actors/RemotePageChild.jsm"
);
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
XPCOMUtils.defineLazyServiceGetter(
this,
"gSerializationHelper",
......@@ -33,6 +35,7 @@ class NetErrorChild extends RemotePageChild {
"RPMAddToHistogram",
"RPMRecordTelemetryEvent",
"RPMGetHttpResponseHeader",
"RPMGetTorStrings",
];
this.exportFunctions(exportableFunctions);
}
......@@ -115,4 +118,8 @@ class NetErrorChild extends RemotePageChild {
return "";
}
RPMGetTorStrings() {
return Cu.cloneInto(TorStrings.onionServices, this.contentWindow);
}
}
......@@ -3,6 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env mozilla/frame-script */
/* import-globals-from ../../components/onionservices/content/netError/onionNetError.js */
const formatter = new Intl.DateTimeFormat("default");
......@@ -282,7 +283,10 @@ function initPage() {
errDesc = document.getElementById("ed_generic");
}
setErrorPageStrings(err);
const isOnionError = err.startsWith("onionServices.");
if (!isOnionError) {
setErrorPageStrings(err);
}
var sd = document.getElementById("errorShortDescText");
if (sd) {
......@@ -435,6 +439,10 @@ function initPage() {
span.textContent = HOST_NAME;
}
}
if (isOnionError) {
OnionServicesAboutNetError.initPage(document);
}
}
function setupErrorUI() {
......
......@@ -215,6 +215,7 @@
</div>
</div>
</body>
<script src="chrome://browser/content/onionservices/netError/onionNetError.js"/>
<script src="chrome://browser/content/aboutNetErrorCodes.js"/>
<script src="chrome://browser/content/aboutNetError.js"/>
</html>
......@@ -222,6 +222,11 @@ XPCOMUtils.defineLazyScriptGetter(
["SecurityLevelButton"],
"chrome://browser/content/securitylevel/securityLevel.js"
);
XPCOMUtils.defineLazyScriptGetter(
this,
["OnionAuthPrompt"],
"chrome://browser/content/onionservices/authPrompt.js"
);
XPCOMUtils.defineLazyScriptGetter(
this,
"gEditItemOverlay",
......@@ -1811,6 +1816,9 @@ var gBrowserInit = {
// Init the SecuritySettingsButton
SecurityLevelButton.init();
// Init the OnionAuthPrompt
OnionAuthPrompt.init();
// Certain kinds of automigration rely on this notification to complete
// their tasks BEFORE the browser window is shown. SessionStore uses it to
// restore tabs into windows AFTER important parts like gMultiProcessBrowser
......@@ -2498,6 +2506,8 @@ var gBrowserInit = {
SecurityLevelButton.uninit();
OnionAuthPrompt.uninit();
gAccessibilityServiceIndicator.uninit();
if (gToolbarKeyNavEnabled) {
......
......@@ -33,6 +33,7 @@
<?xml-stylesheet href="chrome://browser/skin/places/editBookmark.css" type="text/css"?>
<?xml-stylesheet href="chrome://torbutton/skin/tor-circuit-display.css" type="text/css"?>
<?xml-stylesheet href="chrome://torbutton/skin/torbutton.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/onionservices/onionservices.css" type="text/css"?>
# All DTD information is stored in a separate file so that it can be shared by
# hiddenWindowMac.xhtml.
......@@ -647,6 +648,7 @@
#include ../../components/downloads/content/downloadsPanel.inc.xhtml
#include ../../../devtools/startup/enableDevToolsPopup.inc.xhtml
#include ../../components/securitylevel/content/securityLevelPanel.inc.xhtml
#include ../../components/onionservices/content/authPopup.inc.xhtml
#include browser-allTabsMenu.inc.xhtml
<hbox id="downloads-animation-container">
......@@ -1835,6 +1837,7 @@
data-l10n-id="urlbar-indexed-db-notification-anchor"/>
<image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button"
data-l10n-id="urlbar-password-notification-anchor"/>
#include ../../components/onionservices/content/authNotificationIcon.inc.xhtml
<stack id="plugins-notification-icon" class="notification-anchor-icon" role="button" align="center" data-l10n-id="urlbar-plugins-notification-anchor">
<image class="plugin-icon" />
<image id="plugin-icon-badge" />
......
......@@ -14,6 +14,9 @@ ChromeUtils.defineModuleGetter(
"BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm"
);
var { OnionAuthUtil } = ChromeUtils.import(
"chrome://browser/content/onionservices/authUtil.jsm"
);
// BrowserChildGlobal
var global = this;
......@@ -54,5 +57,7 @@ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
Services.obs.notifyObservers(this, "tab-content-frameloader-created");
OnionAuthUtil.addCancelMessageListener(this, docShell);
// This is a temporary hack to prevent regressions (bug 1471327).
void content;
......@@ -40,6 +40,7 @@ DIRS += [
"fxmonitor",
"migration",
"newtab",
"onionservices",
"originattributes",
"ion",
"places",
......
# Copyright (c) 2020, The Tor Project, Inc.
<image id="tor-clientauth-notification-icon"
class="notification-anchor-icon tor-clientauth-icon"
role="button"
tooltiptext="&torbutton.onionServices.authPrompt.tooltip;"/>
# Copyright (c) 2020, The Tor Project, Inc.
<popupnotification id="tor-clientauth-notification" hidden="true">
<popupnotificationcontent orient="vertical">
<description id="tor-clientauth-notification-desc"/>
<label id="tor-clientauth-notification-learnmore"
class="text-link popup-notification-learnmore-link"
is="text-link"/>
<html:div>
<html:input id="tor-clientauth-notification-key" type="password"/>
<html:div id="tor-clientauth-warning"/>
<checkbox id="tor-clientauth-persistkey-checkbox"
label="&torbutton.onionServices.authPrompt.persistCheckboxLabel;"/>
</html:div>
</popupnotificationcontent>
</popupnotification>
/* 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;
}
# 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"/>
</vbox>
</hbox>
</groupbox>
// 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
);
elem.addEventListener("command", () =>
OnionServicesAuthPreferences.onViewSavedKeys()
);
},
onViewSavedKeys() {
gSubDialog.open(
"chrome://browser/content/onionservices/savedKeysDialog.xhtml"
);
},
}; // OnionServicesAuthPreferences
Object.defineProperty(this, "OnionServicesAuthPreferences", {
value: OnionServicesAuthPreferences,
enumerable: true,
writable: false,
});
// Copyright (c) 2020, The Tor Project, Inc.
"use strict";
XPCOMUtils.defineLazyModuleGetters(this, {
OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm",
CommonUtils: "resource://services-common/utils.js",
TorStrings: "resource:///modules/TorStrings.jsm",
});
const OnionAuthPrompt = (function() {
// OnionServicesAuthPrompt objects run within the main/chrome process.
// aReason is the topic passed within the observer notification that is
// causing this auth prompt to be displayed.
function OnionServicesAuthPrompt(aBrowser, aFailedURI, aReason, aOnionName) {
this._browser = aBrowser;
this._failedURI = aFailedURI;
this._reasonForPrompt = aReason;
this._onionName = aOnionName;
}
OnionServicesAuthPrompt.prototype = {
show(aWarningMessage) {
let mainAction = {
label: TorStrings.onionServices.authPrompt.done,
accessKey: TorStrings.onionServices.authPrompt.doneAccessKey,
leaveOpen: true, // Callback is responsible for closing the notification.
callback: this._onDone.bind(this),
};
let dialogBundle = Services.strings.createBundle(
"chrome://global/locale/dialog.properties");
let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
if (!cancelAccessKey)
cancelAccessKey = "c"; // required by PopupNotifications.show()
let cancelAction = {
label: dialogBundle.GetStringFromName("button-cancel"),
accessKey: cancelAccessKey,
callback: this._onCancel.bind(this),
};
let _this = this;
let options = {
autofocus: true,
hideClose: true,
persistent: true,
removeOnDismissal: false,
eventCallback(aTopic) {
if (aTopic === "showing") {
_this._onPromptShowing(aWarningMessage);
} else if (aTopic === "shown") {
_this._onPromptShown();
} else if (aTopic === "removed") {
_this._onPromptRemoved();
}
}
};
this._prompt = PopupNotifications.show(this._browser,
OnionAuthUtil.domid.notification, "",
OnionAuthUtil.domid.anchor,
mainAction, [cancelAction], options);
},
_onPromptShowing(aWarningMessage) {
let xulDoc = this._browser.ownerDocument;
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.
// We do this by splitting the localized string and creating
// several HTML <span> elements.
while (descElem.firstChild)
descElem.removeChild(descElem.firstChild);
let fmtString = TorStrings.onionServices.authPrompt.description;
let prefix = "";
let suffix = "";
const kToReplace = "%S";
let idx = fmtString.indexOf(kToReplace);
if (idx < 0) {
prefix = fmtString;
} else {
prefix = fmtString.substring(0, idx);
suffix = fmtString.substring(idx + kToReplace.length);
}
const kHTMLNS = "http://www.w3.org/1999/xhtml";
let span = xulDoc.createElementNS(kHTMLNS, "span");
span.textContent = prefix;
descElem.appendChild(span);
span = xulDoc.createElementNS(kHTMLNS, "span");
span.id = OnionAuthUtil.domid.onionNameSpan;
span.textContent = this._onionName;
descElem.appendChild(span);
span = xulDoc.createElementNS(kHTMLNS, "span");
span.textContent = suffix;
descElem.appendChild(span);
}
// Set "Learn More" label and href.
let learnMoreElem = xulDoc.getElementById(OnionAuthUtil.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() {
let keyElem = this._getKeyElement();
if (keyElem) {
keyElem.setAttribute("placeholder",
TorStrings.onionServices.authPrompt.keyPlaceholder);
this._boundOnKeyFieldKeyPress = this._onKeyFieldKeyPress.bind(this);
this._boundOnKeyFieldInput = this._onKeyFieldInput.bind(this);
keyElem.addEventListener("keypress", this._boundOnKeyFieldKeyPress);
keyElem.addEventListener("input", this._boundOnKeyFieldInput);
keyElem.focus();
}
},
_onPromptRemoved() {
if (this._boundOnKeyFieldKeyPress) {
let keyElem = this._getKeyElement();
if (keyElem) {
keyElem.value = "";
keyElem.removeEventListener("keypress",
this._boundOnKeyFieldKeyPress);
this._boundOnKeyFieldKeyPress = undefined;
keyElem.removeEventListener("input", this._boundOnKeyFieldInput);
this._boundOnKeyFieldInput = undefined;
}
}
},
_onKeyFieldKeyPress(aEvent) {
if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
this._onDone();
} else if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
this._prompt.remove();
this._onCancel();
}
},
_onKeyFieldInput(aEvent) {
this._showWarning(undefined); // Remove the warning.
},
_onDone() {
let keyElem = this._getKeyElement();
if (!keyElem)
return;
let base64key = this._keyToBase64(keyElem.value);
if (!base64key) {
this._showWarning(TorStrings.onionServices.authPrompt.invalidKey);
return;
}
this._prompt.remove();
// Use Torbutton's controller module to add the private key to Tor.
let controllerFailureMsg =
TorStrings.onionServices.authPrompt.failedToSetKey;
try {
let { controller } =
Cu.import("resource://torbutton/modules/tor-control-port.js", {});
let torController = controller(aError => {
this.show(controllerFailureMsg);
});
let onionAddr = this._onionName.toLowerCase().replace(/\.onion$/, "");
let checkboxElem = this._getCheckboxElement();
let isPermanent = (checkboxElem && checkboxElem.checked);
torController.onionAuthAdd(onionAddr, base64key, isPermanent)
.then(aResponse => {
// Success! Reload the page.
this._browser.sendMessageToActor(
"Browser:Reload",
{},
"BrowserTab"
);
})
.catch(aError => {
if (aError.torMessage)
this.show(aError.torMessage);
else
this.show(controllerFailureMsg);
});
} catch (e) {
this.show(controllerFailureMsg);
}
},
_onCancel() {
// Arrange for an error page to be displayed.
this._browser.messageManager.sendAsyncMessage(
OnionAuthUtil.message.authPromptCanceled,
{failedURI: this._failedURI.spec,
reasonForPrompt: this._reasonForPrompt});
},
_getKeyElement() {
let xulDoc = this._browser.ownerDocument;
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.domid.warningElement);
let keyElem = this._getKeyElement();
if (warningElem) {
if (aWarningMessage) {
warningElem.textContent = aWarningMessage;
warningElem.removeAttribute("hidden");
if (keyElem)
keyElem.className = "invalid";
} else {
warningElem.setAttribute("hidden", "true");
if (keyElem)
keyElem.className = "";
}
}
},
// Returns undefined if the key is the wrong length or format.
_keyToBase64(aKeyString) {
if (!aKeyString)
return undefined;
let base64key;
if (aKeyString.length == 52) {
// The key is probably base32-encoded. Attempt to decode.
// 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.toUpperCase());
} catch (e) {}
if (rawKey) try {
base64key = btoa(rawKey);
} catch (e) {}
} else if ((aKeyString.length == 44) &&
/^[a-zA-Z0-9+/]*=*$/.test(aKeyString)) {
// The key appears to be a correctly formatted base64 value. If not,
// tor will return an error when we try to add the key via the
// control port.
base64key = aKeyString;
}
return base64key;
},
};
let retval = {
init() {
Services.obs.addObserver(this, OnionAuthUtil.topic.clientAuthMissing);
Services.obs.addObserver(this, OnionAuthUtil.topic.clientAuthIncorrect);
},
uninit() {
Services.obs.removeObserver(this, OnionAuthUtil.topic.clientAuthMissing);
Services.obs.removeObserver(this, OnionAuthUtil.topic.clientAuthIncorrect);
},
// 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.topic.clientAuthMissing) &&
(aTopic != OnionAuthUtil.topic.clientAuthIncorrect)) {
return;
}
let browser;
if (aSubject instanceof Ci.nsIDOMWindow) {
let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
browser = contentWindow.docShell.chromeEventHandler;
} else {
browser = aSubject.QueryInterface(Ci.nsIBrowser);
}
if (!gBrowser.browsers.some(aBrowser => aBrowser == browser)) {
return; // This window does not contain the subject browser; ignore.
}
let failedURI = browser.currentURI;
let authPrompt = new OnionServicesAuthPrompt(browser, failedURI,
aTopic, aData);
authPrompt.show(undefined);
}
};
return retval;
})(); /* OnionAuthPrompt */