Skip to content
Snippets Groups Projects
Verified Commit 14058280 authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame :jack_o_lantern:
Browse files

fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain.

Refactors to the old JS code.
parent 0f9ea290
No related branches found
No related tags found
1 merge request!632Bug 41741: Refactor the domain isolator and new circuit
// # domain-isolator.js
// A component for Tor Browser that puts requests from different
// first party domains on separate tor circuits.
// first party domains on separate Tor circuits.
// This file is written in call stack order (later functions
// call earlier functions). The code file can be processed
// with docco.js to provide clear documentation.
var EXPORTED_SYMBOLS = ["DomainIsolator"];
var EXPORTED_SYMBOLS = ["DomainIsolator", "TorDomainIsolator"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
});
Cu.importGlobalProperties(["crypto"]);
// Make the logger available.
let logger = Cc["@torproject.org/torbutton-logger;1"].getService(Ci.nsISupports)
.wrappedJSObject;
XPCOMUtils.defineLazyServiceGetters(this, {
ProtocolProxyService: [
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService",
],
});
// Import crypto object (FF 37+).
Cu.importGlobalProperties(["crypto"]);
const logger = new ConsoleAPI({
prefix: "TorDomainIsolator",
maxLogLevel: "warn",
maxLogLevelPref: "browser.tordomainisolator.loglevel",
});
// ## mozilla namespace.
// Useful functionality for interacting with Mozilla services.
let mozilla = {};
// __mozilla.protocolProxyService__.
// Mozilla's protocol proxy service, useful for managing proxy connections made
// by the browser.
mozilla.protocolProxyService = Cc[
"@mozilla.org/network/protocol-proxy-service;1"
].getService(Ci.nsIProtocolProxyService);
// __mozilla.registerProxyChannelFilter(filterFunction, positionIndex)__.
// Registers a proxy channel filter with the Mozilla Protocol Proxy Service,
// which will help to decide the proxy to be used for a given channel.
// The filterFunction should expect two arguments, (aChannel, aProxy),
// where aProxy is the proxy or list of proxies that would be used by default
// for the given channel, and should return a new Proxy or list of Proxies.
mozilla.registerProxyChannelFilter = function(filterFunction, positionIndex) {
let proxyFilter = {
applyFilter(aChannel, aProxy, aCallback) {
aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy));
},
};
mozilla.protocolProxyService.registerChannelFilter(
proxyFilter,
positionIndex
);
};
// The string to use instead of the domain when it is not known.
const CATCHALL_DOMAIN = "--unknown--";
// ## tor functionality.
let tor = {};
// The preference to observe, to know whether isolation should be enabled or
// disabled.
const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy";
// __tor.noncesForDomains__.
class TorDomainIsolatorImpl {
// A mutable map that records what nonce we are using for each domain.
tor.noncesForDomains = new Map();
#noncesForDomains = new Map();
// __tor.noncesForUserContextId__.
// A mutable map that records what nonce we are using for each tab container.
tor.noncesForUserContextId = new Map();
#noncesForUserContextId = new Map();
// __tor.isolationEabled__.
// A bool that controls if we use SOCKS auth for isolation or not.
tor.isolationEnabled = true;
#isolationEnabled = true;
// __tor.unknownDirtySince__.
// Specifies when the current catch-all circuit was first used
tor.unknownDirtySince = Date.now();
tor.passwordForDomainAndUserContextId = function(domain, userContextId) {
// Check if we already have a nonce. If not, create
// one for this domain and userContextId.
if (!tor.noncesForDomains.has(domain)) {
tor.noncesForDomains.set(domain, tor.nonce());
#catchallDirtySince = Date.now();
/**
* Initialize the domain isolator.
* This function will setup the proxy filter that injects the credentials and
* register some observers.
*/
init() {
logger.info("Setup circuit isolation by domain and user context");
if (Services.prefs.getBoolPref(NON_TOR_PROXY_PREF)) {
this.#isolationEnabled = false;
}
if (!tor.noncesForUserContextId.has(userContextId)) {
tor.noncesForUserContextId.set(userContextId, tor.nonce());
this.#setupProxyFilter();
Services.prefs.addObserver(NON_TOR_PROXY_PREF, this);
}
return (
tor.noncesForDomains.get(domain) +
tor.noncesForUserContextId.get(userContextId)
);
};
// __tor.socksProxyCredentials(originalProxy, domain, userContextId)__.
// Takes a proxyInfo object (originalProxy) and returns a new proxyInfo
// object with the same properties, except the username is set to the
// the domain and userContextId, and the password is a nonce.
tor.socksProxyCredentials = function(originalProxy, domain, userContextId) {
let proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo);
let proxyPassword = tor.passwordForDomainAndUserContextId(
domain,
userContextId
);
return mozilla.protocolProxyService.newProxyInfoWithAuth(
"socks",
proxy.host,
proxy.port,
`${domain}:${userContextId}`, // username
proxyPassword,
"", // aProxyAuthorizationHeader
"", // aConnectionIsolationKey
proxy.flags,
proxy.failoverTimeout,
proxy.failoverProxy
);
};
/**
* Removes the observers added in the initialization.
*/
uninit() {
Services.prefs.removeObserver(NON_TOR_PROXY_PREF, this);
}
tor.nonce = function() {
// Generate a new 128 bit random tag. Strictly speaking both using a
// cryptographic entropy source and using 128 bits of entropy for the
// tag are likely overkill, as correct behavior only depends on how
// unlikely it is for there to be a collision.
let tag = new Uint8Array(16);
crypto.getRandomValues(tag);
enable() {
logger.trace("Domain isolation enabled");
this.#isolationEnabled = true;
}
// Convert the tag to a hex string.
let tagStr = "";
for (let i = 0; i < tag.length; i++) {
tagStr += (tag[i] >>> 4).toString(16);
tagStr += (tag[i] & 0x0f).toString(16);
disable() {
logger.trace("Domain isolation disabled");
this.#isolationEnabled = false;
}
return tagStr;
/**
* Return the credentials to use as username and password for the SOCKS proxy,
* given a certain domain and userContextId. Optionally, create them.
*
* @param firstPartyDomain The first party domain associated to the requests
* @param userContextId The context ID associated to the request
* @param create Whether to create the nonce, if it is not available
* @return Either the credential, or null if we do not have them and create is
* false.
*/
getSocksProxyCredentials(firstPartyDomain, userContextId, create = false) {
if (!this.#noncesForDomains.has(firstPartyDomain)) {
if (!create) {
return null;
}
const nonce = this.#nonce();
logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`);
this.#noncesForDomains.set(firstPartyDomain, nonce);
}
if (!this.#noncesForUserContextId.has(userContextId)) {
if (!create) {
return null;
}
const nonce = this.#nonce();
logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`);
this.#noncesForUserContextId.set(userContextId, nonce);
}
return {
username: `${firstPartyDomain}:${userContextId}`,
password:
this.#noncesForDomains.get(firstPartyDomain) +
this.#noncesForUserContextId.get(userContextId),
};
}
tor.newCircuitForDomain = function(domain) {
// Re-generate the nonce for the domain.
if (domain === "") {
domain = "--unknown--";
// Re-generate the nonce for a certain domain.
newCircuitForDomain(domain) {
if (!domain) {
domain = CATCHALL_DOMAIN;
}
tor.noncesForDomains.set(domain, tor.nonce());
logger.eclog(
3,
`New domain isolation for ${domain}: ${tor.noncesForDomains.get(domain)}`
this.#noncesForDomains.set(domain, this.#nonce());
logger.info(
`New domain isolation for ${domain}: ${this.#noncesForDomains.get(
domain
)}`
);
};
}
tor.newCircuitForUserContextId = function(userContextId) {
// Re-generate the nonce for the context.
tor.noncesForUserContextId.set(userContextId, tor.nonce());
logger.eclog(
3,
`New container isolation for ${userContextId}: ${tor.noncesForUserContextId.get(
// Re-generate the nonce for a userContextId.
newCircuitForUserContextId(userContextId) {
this.#noncesForUserContextId.set(userContextId, this.#nonce());
logger.info(
`New container isolation for ${userContextId}: ${this.#noncesForUserContextId.get(
userContextId
)}`
);
};
}
// __tor.clearIsolation()_.
// Clear the isolation state cache, forcing new circuits to be used for all
// subsequent requests.
tor.clearIsolation = function() {
// Per-domain and per contextId nonces are stored in maps, so simply clear them.
tor.noncesForDomains.clear();
tor.noncesForUserContextId.clear();
// Force a rotation on the next catch-all circuit use by setting the creation
// time to the epoch.
tor.unknownDirtySince = 0;
};
/**
* Clear the isolation state cache, forcing new circuits to be used for all
* subsequent requests.
*/
clearIsolation() {
logger.trace("Clearing isolation nonces.");
// Per-domain and per contextId nonces are stored in maps, so simply clear
// them.
this.#noncesForDomains.clear();
this.#noncesForUserContextId.clear();
// __tor.isolateCircuitsByDomain()__.
// For every HTTPChannel, replaces the default SOCKS proxy with one that authenticates
// to the SOCKS server (the tor client process) with a username (the first party domain
// and userContextId) and a nonce password. Tor provides a separate circuit for each
// username+password combination.
tor.isolateCircuitsByDomain = function() {
mozilla.registerProxyChannelFilter(function(aChannel, aProxy) {
if (!tor.isolationEnabled) {
// Force a rotation on the next catch-all circuit use by setting the
// creation time to the epoch.
this.#catchallDirtySince = 0;
}
observe(subject, topic, data) {
if (topic === "nsPref:changed" && data === NON_TOR_PROXY_PREF) {
if (Services.prefs.getBoolPref(NON_TOR_PROXY_PREF)) {
this.disable();
} else {
this.enable();
}
}
}
/**
* Setup a filter that for every HTTPChannel, replaces the default SOCKS proxy
* with one that authenticates to the SOCKS server (the tor client process)
* with a username (the first party domain and userContextId) and a nonce
* password.
* Tor provides a separate circuit for each username+password combination.
*/
#setupProxyFilter() {
const filterFunction = (aChannel, aProxy) => {
if (!this.#isolationEnabled) {
return aProxy;
}
try {
let channel = aChannel.QueryInterface(Ci.nsIChannel),
firstPartyDomain = channel.loadInfo.originAttributes.firstPartyDomain,
userContextId = channel.loadInfo.originAttributes.userContextId;
const channel = aChannel.QueryInterface(Ci.nsIChannel);
let firstPartyDomain =
channel.loadInfo.originAttributes.firstPartyDomain;
const userContextId = channel.loadInfo.originAttributes.userContextId;
if (firstPartyDomain === "") {
firstPartyDomain = "--unknown--";
if (Date.now() - tor.unknownDirtySince > 1000 * 10 * 60) {
logger.eclog(
3,
firstPartyDomain = CATCHALL_DOMAIN;
if (Date.now() - this.#catchallDirtySince > 1000 * 10 * 60) {
logger.info(
"tor catchall circuit has been dirty for over 10 minutes. Rotating."
);
tor.newCircuitForDomain("--unknown--");
tor.unknownDirtySince = Date.now();
this.newCircuitForDomain(CATCHALL_DOMAIN);
this.#catchallDirtySince = Date.now();
}
}
let replacementProxy = tor.socksProxyCredentials(
const replacementProxy = this.#applySocksProxyCredentials(
aProxy,
firstPartyDomain,
userContextId
);
logger.eclog(
3,
`tor SOCKS: ${channel.URI.spec} via
${replacementProxy.username}:${replacementProxy.password}`
logger.debug(
`Requested ${channel.URI.spec} via ${replacementProxy.username}:${replacementProxy.password}`
);
return replacementProxy;
} catch (e) {
logger.eclog(4, `tor domain isolator error: ${e.message}`);
logger.error("Error while setting a new proxy", e);
return null;
}
}, 0);
};
// ## XPCOM component construction.
// Module specific constants
const kMODULE_NAME = "TorBrowser Domain Isolator";
const kMODULE_CONTRACTID = "@torproject.org/domain-isolator;1";
const kMODULE_CID = Components.ID("e33fd6d4-270f-475f-a96f-ff3140279f68");
// DomainIsolator object.
function DomainIsolator() {
this.wrappedJSObject = this;
ProtocolProxyService.registerChannelFilter(
{
applyFilter(aChannel, aProxy, aCallback) {
aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy));
},
},
0
);
}
// Firefox component requirements
DomainIsolator.prototype = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
classDescription: kMODULE_NAME,
classID: kMODULE_CID,
contractID: kMODULE_CONTRACTID,
observe(subject, topic, data) {
if (topic === "profile-after-change") {
logger.eclog(3, "domain isolator: set up isolating circuits by domain");
/**
* Takes a proxyInfo object (originalProxy) and returns a new proxyInfo
* object with the same properties, except the username is set to the
* the domain and userContextId, and the password is a nonce.
*/
#applySocksProxyCredentials(originalProxy, domain, userContextId) {
const proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo);
const { username, password } = this.getSocksProxyCredentials(
domain,
userContextId,
true
);
return ProtocolProxyService.newProxyInfoWithAuth(
"socks",
proxy.host,
proxy.port,
username,
password,
"", // aProxyAuthorizationHeader
"", // aConnectionIsolationKey
proxy.flags,
proxy.failoverTimeout,
proxy.failoverProxy
);
}
if (Services.prefs.getBoolPref("extensions.torbutton.use_nontor_proxy")) {
tor.isolationEnabled = false;
/**
* Generate a new 128 bit random tag.
*
* Strictly speaking both using a cryptographic entropy source and using 128
* bits of entropy for the tag are likely overkill, as correct behavior only
* depends on how unlikely it is for there to be a collision.
*/
#nonce() {
return Array.from(crypto.getRandomValues(new Uint8Array(16)), byte =>
byte.toString(16).padStart(2, "0")
).join("");
}
tor.isolateCircuitsByDomain();
}
},
const TorDomainIsolator = new TorDomainIsolatorImpl();
// The DomainIsolator object, used only to access this feature through XPCOM.
// TODO: Remove this, and directly use the module, instead.
class DomainIsolator {
constructor() {
this.wrappedJSObject = this;
}
newCircuitForDomain(domain) {
tor.newCircuitForDomain(domain);
},
TorDomainIsolator.newCircuitForDomain(domain);
}
newCircuitForUserContextId(userContextId) {
tor.newCircuitForUserContextId(userContextId);
},
TorDomainIsolator.newCircuitForUserContextId(userContextId);
}
enableIsolation() {
tor.isolationEnabled = true;
},
TorDomainIsolator.enable();
}
disableIsolation() {
tor.isolationEnabled = false;
},
TorDomainIsolator.disable();
}
clearIsolation() {
tor.clearIsolation();
},
wrappedJSObject: null,
};
TorDomainIsolator.clearIsolation();
}
}
......@@ -33,6 +33,12 @@ ChromeUtils.defineModuleGetter(
"resource:///modules/TorSettings.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TorDomainIsolator",
"resource://gre/modules/TorDomainIsolator.jsm"
);
/* Browser observer topis */
const BrowserTopics = Object.freeze({
ProfileAfterChange: "profile-after-change",
......@@ -67,12 +73,16 @@ class TorStartupService {
TorSettings.init();
TorConnect.init();
TorDomainIsolator.init();
gInited = true;
}
_uninit() {
Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
TorDomainIsolator.uninit();
// Close any helper connection first...
TorProtocolService.uninit();
// ... and only then closes the event monitor connection, which will cause
......
......
category profile-after-change TorStartupService @torproject.org/tor-startup-service;1
# TODO: Remove and make TorStartupService initialize also the domain isolator, instead
category profile-after-change DomainIsolator @torproject.org/domain-isolator;1
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment