Commit cb132169 authored by Alex Catarineu's avatar Alex Catarineu Committed by Matthew Finkel
Browse files

Bug 28005: Implement .onion alias urlbar rewrites

A custom HTTPS Everywhere update channel is installed,
which provides rules for locally redirecting some memorable
.tor.onion URLs to non-memorable .onion URLs.

When these redirects occur, we also rewrite the URL in the urlbar
to display the human-memorable hostname instead of the actual
.onion.

Bug 34196: Update site info URL with the onion name
parent 19021b06
......@@ -138,6 +138,26 @@ class ClickHandlerChild extends JSWindowActorChild {
json.originStoragePrincipal = ownerDoc.effectiveStoragePrincipal;
json.triggeringPrincipal = ownerDoc.nodePrincipal;
// Check if the link needs to be opened with .tor.onion urlbar rewrites
// allowed. Only when the owner doc has onionUrlbarRewritesAllowed = true
// and the same origin we should allow this.
json.onionUrlbarRewritesAllowed = false;
if (this.docShell.onionUrlbarRewritesAllowed) {
const sm = Services.scriptSecurityManager;
try {
let targetURI = Services.io.newURI(href);
let isPrivateWin =
ownerDoc.nodePrincipal.originAttributes.privateBrowsingId > 0;
sm.checkSameOriginURI(
docshell.currentDocumentChannel.URI,
targetURI,
false,
isPrivateWin
);
json.onionUrlbarRewritesAllowed = true;
} catch (e) {}
}
// If a link element is clicked with middle button, user wants to open
// the link somewhere rather than pasting clipboard content. Therefore,
// when it's clicked with middle button, we should prevent multiple
......
......@@ -102,6 +102,7 @@ class ClickHandlerParent extends JSWindowActorParent {
charset: browser.characterSet,
referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
allowMixedContent: data.allowMixedContent,
onionUrlbarRewritesAllowed: data.onionUrlbarRewritesAllowed,
isContentWindowPrivate: data.isContentWindowPrivate,
originPrincipal: data.originPrincipal,
originStoragePrincipal: data.originStoragePrincipal,
......
......@@ -575,6 +575,9 @@ class ContextMenuChild extends JSWindowActorChild {
// The same-origin check will be done in nsContextMenu.openLinkInTab.
let parentAllowsMixedContent = !!this.docShell.mixedContentChannel;
let parentAllowsOnionUrlbarRewrites = this.docShell
.onionUrlbarRewritesAllowed;
let disableSetDesktopBackground = null;
// Media related cache info parent needs for saving
......@@ -687,6 +690,7 @@ class ContextMenuChild extends JSWindowActorChild {
frameBrowsingContextID,
disableSetDesktopBackground,
parentAllowsMixedContent,
parentAllowsOnionUrlbarRewrites,
};
if (context.inFrame && !context.inSrcdocFrame) {
......
......@@ -452,7 +452,8 @@ var PlacesCommandHook = {
*/
async bookmarkPage() {
let browser = gBrowser.selectedBrowser;
let url = new URL(browser.currentURI.spec);
const uri = browser.currentOnionAliasURI || browser.currentURI;
let url = new URL(uri.spec);
let info = await PlacesUtils.bookmarks.fetch({ url });
let isNewBookmark = !info;
let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks;
......@@ -556,7 +557,7 @@ var PlacesCommandHook = {
tabs.forEach(tab => {
let browser = tab.linkedBrowser;
let uri = browser.currentURI;
let uri = browser.currentOnionAliasURI || browser.currentURI;
let title = browser.contentTitle || tab.label;
let spec = uri.spec;
if (!(spec in uniquePages)) {
......@@ -1655,14 +1656,17 @@ var BookmarkingUI = {
},
onLocationChange: function BUI_onLocationChange() {
if (this._uri && gBrowser.currentURI.equals(this._uri)) {
const uri =
gBrowser.selectedBrowser.currentOnionAliasURI || gBrowser.currentURI;
if (this._uri && uri.equals(this._uri)) {
return;
}
this.updateStarState();
},
updateStarState: function BUI_updateStarState() {
this._uri = gBrowser.currentURI;
this._uri =
gBrowser.selectedBrowser.currentOnionAliasURI || gBrowser.currentURI;
this._itemGuids.clear();
let guids = new Set();
......
......@@ -474,13 +474,13 @@ var gIdentityHandler = {
* nsIURI for which the identity UI should be displayed, already
* processed by createExposableURI.
*/
updateIdentity(state, uri) {
updateIdentity(state, uri, onionAliasURI) {
let shouldHidePopup = this._uri && this._uri.spec != uri.spec;
this._state = state;
// Firstly, populate the state properties required to display the UI. See
// the documentation of the individual properties for details.
this.setURI(uri);
this.setURI(uri, onionAliasURI);
this._secInfo = gBrowser.securityUI.secInfo;
this._isSecureContext = gBrowser.securityUI.isSecureContext;
......@@ -566,17 +566,18 @@ var gIdentityHandler = {
* Attempt to provide proper IDN treatment for host names
*/
getEffectiveHost() {
let uri = this._onionAliasURI || this._uri;
if (!this._IDNService) {
this._IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(
Ci.nsIIDNService
);
}
try {
return this._IDNService.convertToDisplayIDN(this._uri.host, {});
return this._IDNService.convertToDisplayIDN(uri.host, {});
} catch (e) {
// If something goes wrong (e.g. host is an IP address) just fail back
// to the full domain.
return this._uri.host;
return uri.host;
}
},
......@@ -1010,8 +1011,9 @@ var gIdentityHandler = {
this.updateSitePermissions();
},
setURI(uri) {
setURI(uri, onionAliasURI) {
this._uri = uri;
this._onionAliasURI = onionAliasURI;
try {
// Account for file: urls and catch when "" is the value
......
......@@ -77,6 +77,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
Translation: "resource:///modules/translation/TranslationParent.jsm",
OnionAliasStore: "resource:///modules/OnionAliasStore.jsm",
UITour: "resource:///modules/UITour.jsm",
UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
UrlbarInput: "resource:///modules/UrlbarInput.jsm",
......@@ -2325,6 +2326,7 @@ var gBrowserInit = {
// [9]: allowInheritPrincipal (bool)
// [10]: csp (nsIContentSecurityPolicy)
// [11]: nsOpenWindowInfo
// [12]: onionUrlbarRewritesAllowed (bool)
let userContextId =
window.arguments[5] != undefined
? window.arguments[5]
......@@ -2344,7 +2346,8 @@ var gBrowserInit = {
// TODO fix allowInheritPrincipal to default to false.
// Default to true unless explicitly set to false because of bug 1475201.
window.arguments[9] !== false,
window.arguments[10]
window.arguments[10],
window.arguments[12]
);
window.focus();
} else {
......@@ -3230,7 +3233,8 @@ function loadURI(
forceAboutBlankViewerInCurrent,
triggeringPrincipal,
allowInheritPrincipal = false,
csp = null
csp = null,
onionUrlbarRewritesAllowed = false
) {
if (!triggeringPrincipal) {
throw new Error("Must load with a triggering Principal");
......@@ -3248,6 +3252,7 @@ function loadURI(
csp,
forceAboutBlankViewerInCurrent,
allowInheritPrincipal,
onionUrlbarRewritesAllowed,
});
} catch (e) {
Cu.reportError(e);
......@@ -5359,11 +5364,24 @@ var XULBrowserWindow = {
this.reloadCommand.removeAttribute("disabled");
}
// The onion memorable alias needs to be used in gURLBar.setURI, but also in
// other parts of the code (like the bookmarks UI), so we save it.
if (gBrowser.selectedBrowser.onionUrlbarRewritesAllowed) {
gBrowser.selectedBrowser.currentOnionAliasURI = OnionAliasStore.getShortURI(
aLocationURI
);
} else {
gBrowser.selectedBrowser.currentOnionAliasURI = null;
}
// We want to update the popup visibility if we received this notification
// via simulated locationchange events such as switching between tabs, however
// if this is a document navigation then PopupNotifications will be updated
// via TabsProgressListener.onLocationChange and we do not want it called twice
gURLBar.setURI(aLocationURI, aIsSimulated);
gURLBar.setURI(
gBrowser.selectedBrowser.currentOnionAliasURI || aLocationURI,
aIsSimulated
);
BookmarkingUI.onLocationChange();
......@@ -5534,6 +5552,7 @@ var XULBrowserWindow = {
// Don't need to do anything if the data we use to update the UI hasn't
// changed
let uri = gBrowser.currentURI;
let onionAliasURI = gBrowser.selectedBrowser.currentOnionAliasURI;
let spec = uri.spec;
if (this._state == aState && this._lastLocation == spec) {
// Switching to a tab of the same URL doesn't change most security
......@@ -5551,7 +5570,7 @@ var XULBrowserWindow = {
try {
uri = Services.io.createExposableURI(uri);
} catch (e) {}
gIdentityHandler.updateIdentity(this._state, uri);
gIdentityHandler.updateIdentity(this._state, uri, onionAliasURI);
},
// simulate all change notifications after switching tabs
......@@ -7011,6 +7030,21 @@ function handleLinkClick(event, href, linkNode) {
} catch (e) {}
}
// Check if the link needs to be opened with .tor.onion urlbar rewrites
// allowed. Only when the owner doc has onionUrlbarRewritesAllowed = true
// and the same origin we should allow this.
let persistOnionUrlbarRewritesAllowedInChildTab = false;
if (where == "tab" && gBrowser.docShell.onionUrlbarRewritesAllowed) {
const sm = Services.scriptSecurityManager;
try {
let tURI = makeURI(href);
let isPrivateWin =
doc.nodePrincipal.originAttributes.privateBrowsingId > 0;
sm.checkSameOriginURI(doc.documentURIObject, tURI, false, isPrivateWin);
persistOnionUrlbarRewritesAllowedInChildTab = true;
} catch (e) {}
}
let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
urlSecurityCheck(href, doc.nodePrincipal);
......@@ -7023,6 +7057,7 @@ function handleLinkClick(event, href, linkNode) {
triggeringPrincipal: doc.nodePrincipal,
csp: doc.csp,
frameOuterWindowID,
onionUrlbarRewritesAllowed: persistOnionUrlbarRewritesAllowedInChildTab,
};
// The new tab/window must use the same userContextId
......
......@@ -57,6 +57,7 @@ function openContextMenu(aMessage, aBrowser, aActor) {
disableSetDesktopBackground: data.disableSetDesktopBackground,
loginFillInfo: data.loginFillInfo,
parentAllowsMixedContent: data.parentAllowsMixedContent,
parentAllowsOnionUrlbarRewrites: data.parentAllowsOnionUrlbarRewrites,
userContextId: data.userContextId,
webExtContextData: data.webExtContextData,
};
......@@ -1059,6 +1060,7 @@ class nsContextMenu {
triggeringPrincipal: this.principal,
csp: this.csp,
frameOuterWindowID: this.contentData.frameOuterWindowID,
onionUrlbarRewritesAllowed: false,
};
for (let p in extra) {
params[p] = extra[p];
......@@ -1082,6 +1084,22 @@ class nsContextMenu {
}
params.referrerInfo = referrerInfo;
// Check if the link needs to be opened with .tor.onion urlbar rewrites
// allowed. Only when parent has onionUrlbarRewritesAllowed = true
// and the same origin we should allow this.
if (this.contentData.parentAllowsOnionUrlbarRewrites) {
let referrerURI = this.contentData.documentURIObject;
const sm = Services.scriptSecurityManager;
try {
let targetURI = this.linkURI;
let isPrivateWin =
this.browser.contentPrincipal.originAttributes.privateBrowsingId > 0;
sm.checkSameOriginURI(referrerURI, targetURI, false, isPrivateWin);
params.onionUrlbarRewritesAllowed = true;
} catch (e) {}
}
return params;
}
......
......@@ -390,7 +390,7 @@ async function onNonMediaPageInfoLoad(browser, pageInfoData, imageInfo) {
);
}
onLoadPermission(uri, principal);
securityOnLoad(uri, windowInfo);
securityOnLoad(uri, windowInfo, browser.currentOnionAliasURI);
}
function resetPageInfo(args) {
......
......@@ -312,6 +312,16 @@
<input id="security-identity-domain-value" readonly="readonly"/>
</td>
</tr>
<!-- Onion Alias -->
<tr id="security-view-identity-onionalias-row">
<th>
<xul:label id="security-view-identity-onionalias"
control="security-view-identity-onionalias-value"/>
</th>
<td>
<input id="security-view-identity-onionalias-value" readonly="true"/>
</td>
</tr>
<!-- Owner -->
<tr>
<th>
......
......@@ -250,7 +250,7 @@ var security = {
},
};
async function securityOnLoad(uri, windowInfo) {
async function securityOnLoad(uri, windowInfo, onionAliasURI) {
await security.init(uri, windowInfo);
let info = security.securityInfo;
......@@ -263,6 +263,21 @@ async function securityOnLoad(uri, windowInfo) {
}
document.getElementById("securityTab").hidden = false;
if (onionAliasURI) {
setText(
"security-view-identity-onionalias",
gTorButtonBundle.GetStringFromName("pageInfo_OnionName")
);
setText("security-view-identity-onionalias-value", onionAliasURI.host);
document.getElementById(
"security-view-identity-onionalias-row"
).hidden = false;
} else {
document.getElementById(
"security-view-identity-onionalias-row"
).hidden = true;
}
/* Set Identity section text */
setText("security-identity-domain-value", windowInfo.hostName);
......
......@@ -1547,6 +1547,7 @@
var aRelatedToCurrent;
var aAllowInheritPrincipal;
var aAllowMixedContent;
var aOnionUrlbarRewritesAllowed;
var aSkipAnimation;
var aForceNotRemote;
var aPreferredRemoteType;
......@@ -1577,6 +1578,7 @@
aRelatedToCurrent = params.relatedToCurrent;
aAllowInheritPrincipal = !!params.allowInheritPrincipal;
aAllowMixedContent = params.allowMixedContent;
aOnionUrlbarRewritesAllowed = params.onionUrlbarRewritesAllowed;
aSkipAnimation = params.skipAnimation;
aForceNotRemote = params.forceNotRemote;
aPreferredRemoteType = params.preferredRemoteType;
......@@ -1618,6 +1620,7 @@
relatedToCurrent: aRelatedToCurrent,
skipAnimation: aSkipAnimation,
allowMixedContent: aAllowMixedContent,
onionUrlbarRewritesAllowed: aOnionUrlbarRewritesAllowed,
forceNotRemote: aForceNotRemote,
createLazyBrowser: aCreateLazyBrowser,
preferredRemoteType: aPreferredRemoteType,
......@@ -2504,6 +2507,7 @@
{
allowInheritPrincipal,
allowMixedContent,
onionUrlbarRewritesAllowed,
allowThirdPartyFixup,
bulkOrderedOpen,
charset,
......@@ -2833,6 +2837,9 @@
if (allowMixedContent) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
}
if (onionUrlbarRewritesAllowed) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES;
}
if (!allowInheritPrincipal) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
}
......
......@@ -367,6 +367,7 @@ function openLinkIn(url, where, params) {
var aRelatedToCurrent = params.relatedToCurrent;
var aAllowInheritPrincipal = !!params.allowInheritPrincipal;
var aAllowMixedContent = params.allowMixedContent;
var aOnionUrlbarRewritesAllowed = params.onionUrlbarRewritesAllowed;
var aForceAllowDataURI = params.forceAllowDataURI;
var aInBackground = params.inBackground;
var aInitiatingDoc = params.initiatingDoc;
......@@ -482,6 +483,11 @@ function openLinkIn(url, where, params) {
].createInstance(Ci.nsISupportsPRBool);
allowThirdPartyFixupSupports.data = aAllowThirdPartyFixup;
var onionUrlbarRewritesAllowed = Cc[
"@mozilla.org/supports-PRBool;1"
].createInstance(Ci.nsISupportsPRBool);
onionUrlbarRewritesAllowed.data = aOnionUrlbarRewritesAllowed;
var userContextIdSupports = Cc[
"@mozilla.org/supports-PRUint32;1"
].createInstance(Ci.nsISupportsPRUint32);
......@@ -498,6 +504,8 @@ function openLinkIn(url, where, params) {
sa.appendElement(aTriggeringPrincipal);
sa.appendElement(null); // allowInheritPrincipal
sa.appendElement(aCsp);
sa.appendElement(null); // nsOpenWindowInfo
sa.appendElement(onionUrlbarRewritesAllowed);
const sourceWindow = w || window;
let win;
......@@ -614,6 +622,9 @@ function openLinkIn(url, where, params) {
if (aForceAllowDataURI) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
}
if (aOnionUrlbarRewritesAllowed) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES;
}
let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
if (
......@@ -661,6 +672,7 @@ function openLinkIn(url, where, params) {
relatedToCurrent: aRelatedToCurrent,
skipAnimation: aSkipTabAnimation,
allowMixedContent: aAllowMixedContent,
onionUrlbarRewritesAllowed: aOnionUrlbarRewritesAllowed,
userContextId: aUserContextId,
originPrincipal: aPrincipal,
originStoragePrincipal: aStoragePrincipal,
......
......@@ -703,6 +703,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
TabUnloader: "resource:///modules/TabUnloader.jsm",
TRRRacer: "resource:///modules/TRRPerformance.jsm",
OnionAliasStore: "resource:///modules/OnionAliasStore.jsm",
UIState: "resource://services-sync/UIState.jsm",
WebChannel: "resource://gre/modules/WebChannel.jsm",
WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
......@@ -2046,6 +2047,7 @@ BrowserGlue.prototype = {
Normandy.uninit();
RFPHelper.uninit();
OnionAliasStore.uninit();
},
// Set up a listener to enable/disable the screenshots extension
......@@ -2412,6 +2414,12 @@ BrowserGlue.prototype = {
},
},
{
task: () => {
OnionAliasStore.init();
},
},
{
task: () => {
Blocklist.loadBlocklistAsync();
......
// Copyright (c) 2020, The Tor Project, Inc.
"use strict";
const EXPORTED_SYMBOLS = ["ExtensionMessaging"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm"
);
const { MessageChannel } = ChromeUtils.import(
"resource://gre/modules/MessageChannel.jsm"
);
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
class ExtensionMessaging {
constructor() {
this._callback = null;
this._handlers = new Map();
this._messageManager = Services.cpmm;
}
async sendMessage(msg, extensionId) {
this._init();
const addon = await AddonManager.getAddonByID(extensionId);
if (!addon) {
throw new Error(`extension '${extensionId} does not exist`);
}
await addon.startupPromise;
const channelId = ExtensionUtils.getUniqueId();
return new Promise((resolve, reject) => {
this._handlers.set(channelId, { resolve, reject });
this._messageManager.sendAsyncMessage("MessageChannel:Messages", [
{
messageName: "Extension:Message",
sender: {
id: extensionId,
extensionId,
},
recipient: { extensionId },
data: new StructuredCloneHolder(msg),
channelId,
responseType: MessageChannel.RESPONSE_FIRST,
},
]);
});
}
unload() {
if (this._callback) {
this._handlers.clear();
this._messageManager.removeMessageListener(
"MessageChannel:Response",
this._callback
);
this._callback = null;
}
}
_onMessage({ data }) {
const channelId = data.messageName;
if (this._handlers.has(channelId)) {
const { resolve, reject } = this._handlers.get(channelId);
this._handlers.delete(channelId);
if (data.error) {
reject(new Error(data.error.message));
} else {
resolve(data.value);
}
}
}
_init() {
if (this._callback === null) {
this._callback = this._onMessage.bind(this);
this._messageManager.addMessageListener(
"MessageChannel:Response",
this._callback
);
}
}
}
// Copyright (c) 2020, The Tor Project, Inc.
"use strict";
const EXPORTED_SYMBOLS = ["HttpsEverywhereControl"];
const { ExtensionMessaging } = ChromeUtils.import(
"resource:///modules/ExtensionMessaging.jsm"
);
const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
const EXTENSION_ID = "https-everywhere-eff@eff.org";
const SECUREDROP_TOR_ONION_CHANNEL = {
name: "SecureDropTorOnion",
jwk: {
kty: "RSA",
e: "AQAB",
n:
"p10BbUVc5Xj2S_-MH3bACNBaISo_r9e3PVPyTTjsGsdg2qSXvqUO42fBtpFAy0zUzIGS83v4JjiRdvKJaZTIvbC8AcpymzdsTqujMm8RPTSy3hO_8mXzGa4DEsIB1uNLnUWRBKXvSGCmT9kFyxhTpkYqokNBzafVihTU34tN2Md1xFHnmZGqfYtPtbJLWAa5Z1M11EyR4lIyUxIiPTV9t1XstDbWr3iS83REJrGEFmjG1-BAgx8_lDUTa41799N2yYEhgZud7bL0M3ei8s5OERjiion5uANkUV3-s2QqUZjiVA-XR_HizXjciaUWNd683KqekpNOZ_0STh_UGwpcwU-KwG07QyiCrLrRpz8S_vH8CqGrrcWY3GSzYe9dp34jJdO65oA-G8tK6fMXtvTCFDZI6oNNaXJH71F5J0YbqO2ZqwKYc2WSi0gKVl2wd9roOVjaBmkJqvocntYuNM7t38fDEWHn5KUkmrTbiG68Cy56tDUfpKl3D9Uj4LaMvxJ1tKGvzQ4k_60odT7gIxu6DqYjXUHZpwPsSGBq3njaD7boe4CUXF2K7ViOc87BsKxRNCzDD8OklRjjXzOTOBH3PqFJ93CJ-4ECE5t9STU20aZ8E-2zKB8vjKyCySE4-kcIvBBsnkwVaJTPy9Ft1qYybo-soXEWVEZATANNWklBt8k",
},
update_path_prefix: "https://securedrop.org/https-everywhere/",
scope:
"^https?:\\/\\/[a-z0-9-]+(?:\\.[a-z0-9-]+)*\\.securedrop\\.tor\\.onion\\/",
replaces_default_rulesets: false,
};
class HttpsEverywhereControl {
constructor() {
this._extensionMessaging = null;
}
async _sendMessage(type, object) {
return this._extensionMessaging.sendMessage(
{
type,
object,
},
EXTENSION_ID
);
}
static async wait(seconds = 1) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}
/**
* Installs the .tor.onion update channel in https-everywhere
*/
async installTorOnionUpdateChannel(retries = 5) {
this._init();
// TODO: https-everywhere store is initialized asynchronously, so sending a message
// immediately results in a `store.get is undefined` error.
// For now, let's wait a bit and retry a few times if there is an error, but perhaps
// we could suggest https-everywhere to send a message when that happens and listen