Compare commits

...

2 Commits

Author SHA1 Message Date
Nico Alt
fcd52261fe Temporarily add self-built Windows tor binary
Until it's officially published by the Briar Project.
2022-02-09 00:00:01 +00:00
Nico Alt
fa6f7e62ed Make use of Tor on Windows 2022-02-09 00:00:00 +00:00
9 changed files with 304 additions and 43 deletions

View File

@@ -105,43 +105,46 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@ParametersNotNullByDefault @ParametersNotNullByDefault
abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private static final Logger LOG = getLogger(TorPlugin.class.getName()); static final Logger LOG = getLogger(TorPlugin.class.getName());
private static final String[] EVENTS = { static final String[] EVENTS = {
"CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR" "CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR"
}; };
private static final String OWNER = "__OwningControllerProcess"; static final String OWNER = "__OwningControllerProcess";
private static final int COOKIE_TIMEOUT_MS = 3000; static final int COOKIE_TIMEOUT_MS = 3000;
private static final int COOKIE_POLLING_INTERVAL_MS = 200; static final int COOKIE_POLLING_INTERVAL_MS = 200;
private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}"); private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}");
private final Executor ioExecutor, wakefulIoExecutor; private final Executor ioExecutor, wakefulIoExecutor;
private final Executor connectionStatusExecutor; private final Executor connectionStatusExecutor;
private final NetworkManager networkManager; final NetworkManager networkManager;
private final LocationUtils locationUtils; private final LocationUtils locationUtils;
private final SocketFactory torSocketFactory; private final SocketFactory torSocketFactory;
private final Clock clock; final Clock clock;
private final BatteryManager batteryManager; final BatteryManager batteryManager;
private final Backoff backoff; private final Backoff backoff;
private final TorRendezvousCrypto torRendezvousCrypto; private final TorRendezvousCrypto torRendezvousCrypto;
private final PluginCallback callback; final PluginCallback callback;
private final String architecture; private final String architecture;
private final CircumventionProvider circumventionProvider; private final CircumventionProvider circumventionProvider;
private final ResourceProvider resourceProvider; private final ResourceProvider resourceProvider;
private final long maxLatency; private final long maxLatency;
private final int maxIdleTime; private final int maxIdleTime;
private final int socketTimeout; private final int socketTimeout;
private final File torDirectory, geoIpFile, configFile; final File torDirectory;
private final int torSocksPort; final File geoIpFile;
private final int torControlPort; final File configFile;
private final File doneFile, cookieFile; final int torSocksPort;
private final AtomicBoolean used = new AtomicBoolean(false); final int torControlPort;
private final File doneFile;
final File cookieFile;
final AtomicBoolean used = new AtomicBoolean(false);
protected final PluginState state = new PluginState(); protected final PluginState state = new PluginState();
private volatile Socket controlSocket = null; volatile Socket controlSocket = null;
private volatile TorControlConnection controlConnection = null; volatile TorControlConnection controlConnection = null;
private volatile Settings settings = null; volatile Settings settings = null;
protected abstract int getProcessId(); protected abstract int getProcessId();
@@ -242,6 +245,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
Process torProcess; Process torProcess;
ProcessBuilder pb = ProcessBuilder pb =
new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); new ProcessBuilder(torPath, "-f", configPath, OWNER, pid);
// TODO: pb.redirectErrorStream on Linux, too?
Map<String, String> env = pb.environment(); Map<String, String> env = pb.environment();
env.put("HOME", torDirectory.getAbsolutePath()); env.put("HOME", torDirectory.getAbsolutePath());
pb.directory(torDirectory); pb.directory(torDirectory);
@@ -318,8 +322,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
bind(); bind();
} }
// TODO: Remove after a reasonable migration period (added 2020-06-25) // TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateSettings(Settings settings) { Settings migrateSettings(Settings settings) {
int network = settings.getInt(PREF_TOR_NETWORK, int network = settings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK); DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) { if (network == PREF_TOR_NETWORK_NEVER) {
@@ -330,11 +335,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
return settings; return settings;
} }
private boolean assetsAreUpToDate() { boolean assetsAreUpToDate() {
return doneFile.lastModified() > getLastUpdateTime(); return doneFile.lastModified() > getLastUpdateTime();
} }
private void installAssets() throws PluginException { void installAssets() throws PluginException {
try { try {
// The done file may already exist from a previous installation // The done file may already exist from a previous installation
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
@@ -389,20 +394,20 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private InputStream getObfs4InputStream() throws IOException { private InputStream getObfs4InputStream() throws IOException {
InputStream in = resourceProvider InputStream in = resourceProvider
.getResourceInputStream("obfs4proxy_" + architecture, ".zip"); .getResourceInputStream("obfs4proxy_" + "linux-x86_64", ".zip");
ZipInputStream zin = new ZipInputStream(in); ZipInputStream zin = new ZipInputStream(in);
if (zin.getNextEntry() == null) throw new IOException(); if (zin.getNextEntry() == null) throw new IOException();
return zin; return zin;
} }
private static void append(StringBuilder strb, String name, int value) { protected static void append(StringBuilder strb, String name, int value) {
strb.append(name); strb.append(name);
strb.append(" "); strb.append(" ");
strb.append(value); strb.append(value);
strb.append("\n"); strb.append("\n");
} }
private InputStream getConfigInputStream() { protected InputStream getConfigInputStream() {
StringBuilder strb = new StringBuilder(); StringBuilder strb = new StringBuilder();
append(strb, "ControlPort", torControlPort); append(strb, "ControlPort", torControlPort);
append(strb, "CookieAuthentication", 1); append(strb, "CookieAuthentication", 1);
@@ -415,7 +420,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
strb.toString().getBytes(Charset.forName("UTF-8"))); strb.toString().getBytes(Charset.forName("UTF-8")));
} }
private void listFiles(File f) { void listFiles(File f) {
if (f.isDirectory()) { if (f.isDirectory()) {
File[] children = f.listFiles(); File[] children = f.listFiles();
if (children != null) for (File child : children) listFiles(child); if (children != null) for (File child : children) listFiles(child);
@@ -424,7 +429,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
} }
private byte[] read(File f) throws IOException { byte[] read(File f) throws IOException {
byte[] b = new byte[(int) f.length()]; byte[] b = new byte[(int) f.length()];
FileInputStream in = new FileInputStream(f); FileInputStream in = new FileInputStream(f);
try { try {
@@ -440,7 +445,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
} }
private void bind() { void bind() {
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
// If there's already a port number stored in config, reuse it // If there's already a port number stored in config, reuse it
String portString = settings.get(PREF_TOR_PORT); String portString = settings.get(PREF_TOR_PORT);
@@ -814,8 +819,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}); });
} }
private void updateConnectionStatus(NetworkStatus status, void updateConnectionStatus(NetworkStatus status,
boolean charging) { boolean charging) {
connectionStatusExecutor.execute(() -> { connectionStatusExecutor.execute(() -> {
if (!state.isTorRunning()) return; if (!state.isTorRunning()) return;
boolean online = status.isConnected(); boolean online = status.isConnected();

View File

@@ -0,0 +1,12 @@
package org.briarproject.bramble.system;
import javax.annotation.Nullable;
import java.security.Provider;
public class WindowsSecureRandomProvider extends AbstractSecureRandomProvider {
@Nullable
@Override
public Provider getProvider() {
return null;
}
}

View File

@@ -17,7 +17,7 @@ dependencies {
def jna_version = '4.5.2' def jna_version = '4.5.2'
implementation "net.java.dev.jna:jna:$jna_version" implementation "net.java.dev.jna:jna:$jna_version"
implementation "net.java.dev.jna:jna-platform:$jna_version" implementation "net.java.dev.jna:jna-platform:$jna_version"
tor "org.briarproject:tor:$tor_version" tor fileTree(dir: 'libs', include: '*.zip')
tor "org.briarproject:obfs4proxy:$obfs4proxy_version@zip" tor "org.briarproject:obfs4proxy:$obfs4proxy_version@zip"
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version" annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"

Binary file not shown.

View File

@@ -32,13 +32,14 @@ import javax.net.SocketFactory;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.OsUtils.isLinux; import static org.briarproject.bramble.util.OsUtils.isLinux;
import static org.briarproject.bramble.util.OsUtils.isWindows;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class UnixTorPluginFactory implements DuplexPluginFactory { public class DesktopTorPluginFactory implements DuplexPluginFactory {
private static final Logger LOG = private static final Logger LOG =
getLogger(UnixTorPluginFactory.class.getName()); getLogger(DesktopTorPluginFactory.class.getName());
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
@@ -62,7 +63,7 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
private final CryptoComponent crypto; private final CryptoComponent crypto;
@Inject @Inject
UnixTorPluginFactory(@IoExecutor Executor ioExecutor, DesktopTorPluginFactory(@IoExecutor Executor ioExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor, @WakefulIoExecutor Executor wakefulIoExecutor,
NetworkManager networkManager, NetworkManager networkManager,
LocationUtils locationUtils, LocationUtils locationUtils,
@@ -107,25 +108,33 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
@Override @Override
public DuplexPlugin createPlugin(PluginCallback callback) { public DuplexPlugin createPlugin(PluginCallback callback) {
// Check that we have a Tor binary for this architecture // Check that we have a Tor binary for this architecture
String architecture = null;
if (isLinux()) { if (isLinux()) {
String arch = System.getProperty("os.arch"); String arch = System.getProperty("os.arch");
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("System's os.arch is " + arch); LOG.info("System's os.arch is " + arch);
} }
if (arch.equals("amd64")) { if (arch.equals("amd64")) {
architecture = "linux-x86_64"; return createUnixPlugin(callback, "linux-x86_64");
} else if (arch.equals("aarch64")) { } else if (arch.equals("aarch64")) {
architecture = "linux-aarch64"; return createUnixPlugin(callback, "linux-aarch64");
} else if (arch.equals("arm")) { } else if (arch.equals("arm")) {
architecture = "linux-armhf"; return createUnixPlugin(callback, "linux-armhf");
} }
} }
if (architecture == null) { if (isWindows()) {
LOG.info("Tor is not supported on this architecture"); String arch = System.getProperty("os.arch");
return null; if (LOG.isLoggable(INFO)) {
LOG.info("System's os.arch is " + arch);
}
if (arch.equals("amd64")) {
return createWindowsPlugin(callback, "windows-x86_64");
}
} }
LOG.info("Tor is not supported on this architecture");
return null;
}
private DuplexPlugin createUnixPlugin(PluginCallback callback, String architecture) {
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("The selected architecture for Tor is " + architecture); LOG.info("The selected architecture for Tor is " + architecture);
} }
@@ -143,4 +152,21 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
eventBus.addListener(plugin); eventBus.addListener(plugin);
return plugin; return plugin;
} }
private DuplexPlugin createWindowsPlugin(PluginCallback callback, String architecture) {
if (LOG.isLoggable(INFO)) {
LOG.info("The selected architecture for Tor is " + architecture);
}
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl();
WindowsTorPlugin plugin = new WindowsTorPlugin(ioExecutor, wakefulIoExecutor,
networkManager, locationUtils, torSocketFactory, clock,
resourceProvider, circumventionProvider, batteryManager,
backoff, torRendezvousCrypto, callback, architecture,
MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, torControlPort);
eventBus.addListener(plugin);
return plugin;
}
} }

View File

@@ -0,0 +1,217 @@
package org.briarproject.bramble.plugin.tor;
import com.sun.jna.Library;
import com.sun.jna.Native;
import net.freehaven.tor.control.TorControlConnection;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import java.io.*;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.net.SocketFactory;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@NotNullByDefault
class WindowsTorPlugin extends JavaTorPlugin {
WindowsTorPlugin(Executor ioExecutor,
Executor wakefulIoExecutor,
NetworkManager networkManager,
LocationUtils locationUtils,
SocketFactory torSocketFactory,
Clock clock,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider,
BatteryManager batteryManager,
Backoff backoff,
TorRendezvousCrypto torRendezvousCrypto,
PluginCallback callback,
String architecture,
long maxLatency,
int maxIdleTime,
File torDirectory,
int torSocksPort,
int torControlPort) {
super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils,
torSocketFactory, clock, resourceProvider,
circumventionProvider, batteryManager, backoff,
torRendezvousCrypto, callback, architecture,
maxLatency, maxIdleTime, torDirectory, torSocksPort, torControlPort);
}
protected File getTorExecutableFile() {
return new File(torDirectory, "tor.exe");
}
protected InputStream getConfigInputStream() {
StringBuilder strb = new StringBuilder();
append(strb, "ControlPort", torControlPort);
append(strb, "CookieAuthentication", 1);
append(strb, "DisableNetwork", 1);
append(strb, "RunAsDaemon", 1);
append(strb, "SafeSocks", 1);
append(strb, "SocksPort", torSocksPort);
InputStream inputStream = new ByteArrayInputStream(
strb.toString().getBytes(Charset.forName("UTF-8")));
InputStream windowsPaths = new ByteArrayInputStream(getTorrcPaths());
inputStream = new SequenceInputStream(inputStream, windowsPaths);
return inputStream;
}
private byte[] getTorrcPaths() {
StringBuilder sb = new StringBuilder();
sb.append("\n");
sb.append("GeoIPFile ");
sb.append(geoIpFile.getAbsolutePath());
sb.append("\n");
sb.append("GeoIPv6File ");
sb.append(geoIpFile.getAbsolutePath());
sb.append("6");
sb.append("\n");
sb.append("DataDirectory ");
sb.append(torDirectory);
sb.append("\\.tor");
return sb.toString().getBytes(StandardCharsets.UTF_8);
}
@Override
public void start() throws PluginException {
/*
TODO:
- properly handle and throw PluginExceptions etc.
- absolute paths in Windows torrc (Linux too?)
- don't do 10 seconds sleep in main thread
*/
if (used.getAndSet(true)) throw new IllegalStateException();
if (!torDirectory.exists()) {
if (!torDirectory.mkdirs()) {
LOG.warning("Could not create Tor directory.");
throw new PluginException();
}
}
// Load the settings
settings = migrateSettings(callback.getSettings());
// Install or update the assets if necessary
if (!assetsAreUpToDate()) installAssets();
if (cookieFile.exists() && !cookieFile.delete())
LOG.warning("Old auth cookie not deleted");
// Start a new Tor process
LOG.info("Starting Tor");
File torFile = getTorExecutableFile();
String torPath = torFile.getAbsolutePath();
String configPath = configFile.getAbsolutePath();
String pid = String.valueOf(getProcessId());
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
Process torProcess;
ProcessBuilder pb =
new ProcessBuilder(torPath, "-f", configPath, OWNER, pid);
pb.redirectErrorStream(true); // logged only first line on Windows otherwise
Map<String, String> env = pb.environment();
env.put("HOME", torDirectory.getAbsolutePath());
pb.directory(torDirectory);
try {
torProcess = pb.start();
// Log the process's standard output until it detaches
if (LOG.isLoggable(INFO)) {
Scanner stdout = new Scanner(torProcess.getInputStream());
while (stdout.hasNextLine()) {
if (stdout.hasNextLine()) {
LOG.info(stdout.nextLine());
}
}
stdout.close();
}
try {
// Wait for the process to detach or exit
int exit = torProcess.waitFor();
if (exit != 0) {
if (LOG.isLoggable(WARNING))
LOG.warning("Tor exited with value " + exit);
}
// Wait for the auth cookie file to be created/updated
long start = clock.currentTimeMillis();
while (cookieFile.length() < 32) {
if (clock.currentTimeMillis() - start > COOKIE_TIMEOUT_MS) {
LOG.warning("Auth cookie not created");
if (LOG.isLoggable(INFO)) listFiles(torDirectory);
}
Thread.sleep(COOKIE_POLLING_INTERVAL_MS);
}
LOG.info("Auth cookie created");
} catch (InterruptedException e) {
LOG.warning("Interrupted while starting Tor");
Thread.currentThread().interrupt();
e.printStackTrace();
}
} catch (SecurityException | IOException e) {
e.printStackTrace();
}
}
});
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// Open a control connection and authenticate using the cookie file
controlSocket = new Socket("127.0.0.1", torControlPort);
controlConnection = new TorControlConnection(controlSocket);
controlConnection.authenticate(read(cookieFile));
// Tell Tor to exit when the control connection is closed
controlConnection.takeOwnership();
controlConnection.resetConf(singletonList(OWNER));
// Register to receive events from the Tor process
controlConnection.setEventHandler(this);
controlConnection.setEvents(asList(EVENTS));
// Check whether Tor has already bootstrapped
String phase = controlConnection.getInfo("status/bootstrap-phase");
if (phase != null && phase.contains("PROGRESS=100")) {
LOG.info("Tor has already bootstrapped");
state.setBootstrapped();
}
} catch (IOException e) {
throw new PluginException(e);
}
state.setStarted();
// Check whether we're online
updateConnectionStatus(networkManager.getNetworkStatus(),
batteryManager.isCharging());
// Bind a server socket to receive incoming hidden service connections
bind();
}
@Override
protected int getProcessId() {
return CLibrary.INSTANCE._getpid();
}
private interface CLibrary extends Library {
CLibrary INSTANCE = Native.loadLibrary("msvcrt", CLibrary.class);
int _getpid();
}
}

View File

@@ -9,6 +9,7 @@ import dagger.Provides;
import static org.briarproject.bramble.util.OsUtils.isLinux; import static org.briarproject.bramble.util.OsUtils.isLinux;
import static org.briarproject.bramble.util.OsUtils.isMac; import static org.briarproject.bramble.util.OsUtils.isMac;
import static org.briarproject.bramble.util.OsUtils.isWindows;
@Module @Module
public class DesktopSecureRandomModule { public class DesktopSecureRandomModule {
@@ -18,7 +19,8 @@ public class DesktopSecureRandomModule {
SecureRandomProvider provideSecureRandomProvider() { SecureRandomProvider provideSecureRandomProvider() {
if (isLinux() || isMac()) if (isLinux() || isMac())
return new UnixSecureRandomProvider(); return new UnixSecureRandomProvider();
// TODO: Create a secure random provider for Windows if (isWindows())
return new WindowsSecureRandomProvider();
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
} }

View File

@@ -107,7 +107,7 @@ public class BridgeTest extends BrambleTestCase {
private final File torDir = getTestDirectory(); private final File torDir = getTestDirectory();
private final Params params; private final Params params;
private UnixTorPluginFactory factory; private DesktopTorPluginFactory factory;
public BridgeTest(Params params) { public BridgeTest(Params params) {
this.params = params; this.params = params;
@@ -152,7 +152,7 @@ public class BridgeTest extends BrambleTestCase {
return singletonList(params.bridge); return singletonList(params.bridge);
} }
}; };
factory = new UnixTorPluginFactory(ioExecutor, wakefulIoExecutor, factory = new DesktopTorPluginFactory(ioExecutor, wakefulIoExecutor,
networkManager, locationUtils, eventBus, torSocketFactory, networkManager, locationUtils, eventBus, torSocketFactory,
backoffFactory, resourceProvider, bridgeProvider, backoffFactory, resourceProvider, bridgeProvider,
batteryManager, clock, torDir, DEFAULT_SOCKS_PORT, batteryManager, clock, torDir, DEFAULT_SOCKS_PORT,

View File

@@ -25,7 +25,6 @@ dependencyVerification {
'net.ltgt.gradle.incap:incap:0.2:incap-0.2.jar:b625b9806b0f1e4bc7a2e3457119488de3cd57ea20feedd513db070a573a4ffd', 'net.ltgt.gradle.incap:incap:0.2:incap-0.2.jar:b625b9806b0f1e4bc7a2e3457119488de3cd57ea20feedd513db070a573a4ffd',
'org.apache-extras.beanshell:bsh:2.0b6:bsh-2.0b6.jar:a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047', 'org.apache-extras.beanshell:bsh:2.0b6:bsh-2.0b6.jar:a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047',
'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766', 'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766',
'org.briarproject:tor:0.3.5.17:tor-0.3.5.17.jar:ce0e1f4d8f14878e61b23a35a452bc0f2a8e3117ced5a74773cd78475fa7af39',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d', 'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a', 'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53', 'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',