diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index 9a93f96d2..164c22c46 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -107,7 +107,7 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @ParametersNotNullByDefault abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { - private static final Logger LOG = getLogger(TorPlugin.class.getName()); + protected static final Logger LOG = getLogger(TorPlugin.class.getName()); private static final String[] EVENTS = { "CIRC", @@ -124,7 +124,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private static final int COOKIE_POLLING_INTERVAL_MS = 200; private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}"); - private final Executor ioExecutor, wakefulIoExecutor; + protected final Executor ioExecutor; + private final Executor wakefulIoExecutor; private final Executor connectionStatusExecutor; private final NetworkManager networkManager; private final LocationUtils locationUtils; @@ -254,34 +255,15 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { Map env = pb.environment(); env.put("HOME", torDirectory.getAbsolutePath()); pb.directory(torDirectory); + pb.redirectErrorStream(true); try { torProcess = pb.start(); } catch (SecurityException | IOException e) { throw new PluginException(e); } - // Log the process's standard output until it detaches - if (LOG.isLoggable(INFO)) { - Scanner stdout = new Scanner(torProcess.getInputStream()); - Scanner stderr = new Scanner(torProcess.getErrorStream()); - while (stdout.hasNextLine() || stderr.hasNextLine()) { - if (stdout.hasNextLine()) { - LOG.info(stdout.nextLine()); - } - if (stderr.hasNextLine()) { - LOG.info(stderr.nextLine()); - } - } - stdout.close(); - stderr.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); - throw new PluginException(); - } + // Wait for the Tor process to start + waitForTorToStart(torProcess); // Wait for the auth cookie file to be created/updated long start = clock.currentTimeMillis(); while (cookieFile.length() < 32) { @@ -397,7 +379,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { return zin; } - private static void append(StringBuilder strb, String name, int value) { + private static void append(StringBuilder strb, String name, Object value) { strb.append(name); strb.append(" "); strb.append(value); @@ -405,13 +387,17 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private InputStream getConfigInputStream() { + File dataDirectory = new File(torDirectory, ".tor"); StringBuilder strb = new StringBuilder(); append(strb, "ControlPort", torControlPort); append(strb, "CookieAuthentication", 1); + append(strb, "DataDirectory", dataDirectory.getAbsolutePath()); append(strb, "DisableNetwork", 1); append(strb, "RunAsDaemon", 1); append(strb, "SafeSocks", 1); append(strb, "SocksPort", torSocksPort); + strb.append("GeoIPFile\n"); + strb.append("GeoIPv6File\n"); //noinspection CharsetObjectCanBeUsed return new ByteArrayInputStream( strb.toString().getBytes(Charset.forName("UTF-8"))); @@ -442,6 +428,23 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } } + protected void waitForTorToStart(Process torProcess) + throws InterruptedException, PluginException { + Scanner stdout = new Scanner(torProcess.getInputStream()); + // Log the first line of stdout (contains Tor and library versions) + if (stdout.hasNextLine()) LOG.info(stdout.nextLine()); + // Read the process's stdout (and redirected stderr) until it detaches + while (stdout.hasNextLine()) stdout.nextLine(); + stdout.close(); + // 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); + throw new PluginException(); + } + } + private void bind() { ioExecutor.execute(() -> { // If there's already a port number stored in config, reuse it diff --git a/bramble-java/build.gradle b/bramble-java/build.gradle index 65682ac50..fb3414d7d 100644 --- a/bramble-java/build.gradle +++ b/bramble-java/build.gradle @@ -18,7 +18,9 @@ dependencies { implementation "net.java.dev.jna:jna:$jna_version" implementation "net.java.dev.jna:jna-platform:$jna_version" tor "org.briarproject:tor-linux:$tor_version" + tor "org.briarproject:tor-windows:$tor_version" tor "org.briarproject:obfs4proxy-linux:$obfs4proxy_version" + tor "org.briarproject:obfs4proxy-windows:$obfs4proxy_version" annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version" diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java new file mode 100644 index 000000000..9f49416fe --- /dev/null +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java @@ -0,0 +1,97 @@ +package org.briarproject.bramble.plugin.tor; + +import com.sun.jna.platform.win32.Kernel32; + +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.File; +import java.util.Scanner; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; + +import javax.net.SocketFactory; + +import static java.util.logging.Level.INFO; + +@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); + } + + @Override + protected int getProcessId() { + return Kernel32.INSTANCE.GetCurrentProcessId(); + } + + @Override + protected void waitForTorToStart(Process torProcess) + throws InterruptedException, PluginException { + // On Windows the RunAsDaemon option has no effect, so Tor won't detach. + // Wait for the control port to be opened, then continue to read its + // stdout and stderr in a background thread until it exits. + BlockingQueue success = new ArrayBlockingQueue<>(1); + ioExecutor.execute(() -> { + boolean started = false; + // Read the process's stdout (and redirected stderr) + Scanner stdout = new Scanner(torProcess.getInputStream()); + // Log the first line of stdout (contains Tor and library versions) + if (stdout.hasNextLine()) LOG.info(stdout.nextLine()); + // Startup has succeeded when the control port is open + while (stdout.hasNextLine()) { + String line = stdout.nextLine(); + if (!started && line.contains("Opened Control listener")) { + success.add(true); + started = true; + } + } + stdout.close(); + // If the control port wasn't opened, startup has failed + if (!started) success.add(false); + // Wait for the process to exit + try { + int exit = torProcess.waitFor(); + if (LOG.isLoggable(INFO)) + LOG.info("Tor exited with value " + exit); + } catch (InterruptedException e1) { + LOG.warning("Interrupted while waiting for Tor to exit"); + Thread.currentThread().interrupt(); + } + }); + // Wait for the startup result + if (!success.take()) throw new PluginException(); + } + +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPluginFactory.java new file mode 100644 index 000000000..9496a9689 --- /dev/null +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPluginFactory.java @@ -0,0 +1,80 @@ +package org.briarproject.bramble.plugin.tor; + +import org.briarproject.bramble.api.battery.BatteryManager; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +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.BackoffFactory; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.plugin.TorControlPort; +import org.briarproject.bramble.api.plugin.TorDirectory; +import org.briarproject.bramble.api.plugin.TorSocksPort; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.system.LocationUtils; +import org.briarproject.bramble.api.system.ResourceProvider; +import org.briarproject.bramble.api.system.WakefulIoExecutor; + +import java.io.File; +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; +import javax.net.SocketFactory; + +import static java.util.logging.Level.INFO; +import static org.briarproject.bramble.util.OsUtils.isWindows; + +@Immutable +@NotNullByDefault +public class WindowsTorPluginFactory extends TorPluginFactory { + + @Inject + WindowsTorPluginFactory(@IoExecutor Executor ioExecutor, + @WakefulIoExecutor Executor wakefulIoExecutor, + NetworkManager networkManager, + LocationUtils locationUtils, + EventBus eventBus, + SocketFactory torSocketFactory, + BackoffFactory backoffFactory, + ResourceProvider resourceProvider, + CircumventionProvider circumventionProvider, + BatteryManager batteryManager, + Clock clock, + CryptoComponent crypto, + @TorDirectory File torDirectory, + @TorSocksPort int torSocksPort, + @TorControlPort int torControlPort) { + super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, + eventBus, torSocketFactory, backoffFactory, resourceProvider, + circumventionProvider, batteryManager, clock, crypto, + torDirectory, torSocksPort, torControlPort); + } + + @Nullable + @Override + String getArchitectureForTorBinary() { + if (!isWindows()) return null; + String arch = System.getProperty("os.arch"); + if (LOG.isLoggable(INFO)) { + LOG.info("System's os.arch is " + arch); + } + if (arch.equals("amd64")) return "windows-x86_64"; + return null; + } + + @Override + TorPlugin createPluginInstance(Backoff backoff, + TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, + String architecture) { + return new WindowsTorPlugin(ioExecutor, wakefulIoExecutor, + networkManager, locationUtils, torSocketFactory, clock, + resourceProvider, circumventionProvider, batteryManager, + backoff, torRendezvousCrypto, callback, architecture, + MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, + torControlPort); + } +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java b/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java index 47dfe5b0a..92eb3376f 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java @@ -16,9 +16,7 @@ public class DesktopSecureRandomModule { @Provides @Singleton SecureRandomProvider provideSecureRandomProvider() { - if (isLinux() || isMac()) - return new UnixSecureRandomProvider(); - // TODO: Create a secure random provider for Windows - throw new UnsupportedOperationException(); + if (isLinux() || isMac()) return new UnixSecureRandomProvider(); + return () -> null; // Use system default } } diff --git a/bramble-java/witness.gradle b/bramble-java/witness.gradle index b5a29d22b..0cee027a8 100644 --- a/bramble-java/witness.gradle +++ b/bramble-java/witness.gradle @@ -25,7 +25,9 @@ dependencyVerification { '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.briarproject:obfs4proxy-linux:0.0.12:obfs4proxy-linux-0.0.12.jar:3dd83aff25fe1cb3e4eab78a02c76ac921f552be6877b3af83a472438525df2a', + 'org.briarproject:obfs4proxy-windows:0.0.12:obfs4proxy-windows-0.0.12.jar:392aa4b9d9c6fef0c659c4068d019d6c6471991bbb62ff00553884ec36018c7b', 'org.briarproject:tor-linux:0.4.5.12-2:tor-linux-0.4.5.12-2.jar:d275f323faf5e70b33d2c8a1bdab1bb3ab5a0d8f4e23c4a6dda03d86f4e95838', + 'org.briarproject:tor-windows:0.4.5.12-2:tor-windows-0.4.5.12-2.jar:46599a15d099ed35a360113293f66acc119571c24ec2e37e85e4fb54b4722e07', '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.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53', diff --git a/briar-headless/build.gradle b/briar-headless/build.gradle index e905914c5..501b67872 100644 --- a/briar-headless/build.gradle +++ b/briar-headless/build.gradle @@ -60,7 +60,12 @@ void jarFactory(Jar jarTask, jarArchitecture) { } { it.duplicatesStrategy(DuplicatesStrategy.EXCLUDE) - String[] architectures = ["linux-aarch64", "linux-armhf", "linux-x86_64"] + String[] architectures = [ + "linux-aarch64", + "linux-armhf", + "linux-x86_64", + "windows-x86_64" + ] for (String arch : architectures) { if (arch != jarArchitecture) { exclude "obfs4proxy_" + arch + ".zip" @@ -113,6 +118,10 @@ task x86LinuxJar(type: Jar) { jarFactory(it, 'linux-x86_64') } +task windowsJar(type: Jar) { + jarFactory(it, 'windows-x86_64') +} + task linuxJars { dependsOn(aarch64LinuxJar, armhfLinuxJar, x86LinuxJar) } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt index e78774011..018045a0a 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt @@ -21,6 +21,7 @@ import org.briarproject.bramble.event.DefaultEventExecutorModule import org.briarproject.bramble.network.JavaNetworkModule import org.briarproject.bramble.plugin.tor.CircumventionModule import org.briarproject.bramble.plugin.tor.UnixTorPluginFactory +import org.briarproject.bramble.plugin.tor.WindowsTorPluginFactory import org.briarproject.bramble.socks.SocksModule import org.briarproject.bramble.system.ClockModule import org.briarproject.bramble.system.DefaultTaskSchedulerModule @@ -29,6 +30,7 @@ import org.briarproject.bramble.system.DesktopSecureRandomModule import org.briarproject.bramble.system.JavaSystemModule import org.briarproject.bramble.util.OsUtils.isLinux import org.briarproject.bramble.util.OsUtils.isMac +import org.briarproject.bramble.util.OsUtils.isWindows import org.briarproject.briar.headless.blogs.HeadlessBlogModule import org.briarproject.briar.headless.contact.HeadlessContactModule import org.briarproject.briar.headless.event.HeadlessEventModule @@ -94,9 +96,15 @@ internal class HeadlessModule(private val appDir: File) { @Provides @Singleton - internal fun providePluginConfig(tor: UnixTorPluginFactory): PluginConfig { - val duplex: List = - if (isLinux() || isMac()) listOf(tor) else emptyList() + internal fun providePluginConfig( + unixTor: UnixTorPluginFactory, + winTor: WindowsTorPluginFactory + ): PluginConfig { + val duplex: List = when { + isLinux() || isMac() -> listOf(unixTor) + isWindows() -> listOf(winTor) + else -> emptyList() + } return object : PluginConfig { override fun getDuplexFactories(): Collection = duplex override fun getSimplexFactories(): Collection = emptyList() diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/Main.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/Main.kt index 6c091f71d..410781441 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/Main.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/Main.kt @@ -8,6 +8,8 @@ import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.int import org.bouncycastle.util.encoders.Base64.toBase64String import org.briarproject.bramble.BrambleCoreEagerSingletons +import org.briarproject.bramble.util.OsUtils.isLinux +import org.briarproject.bramble.util.OsUtils.isMac import org.briarproject.briar.BriarCoreEagerSingletons import org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY import java.io.File @@ -90,11 +92,13 @@ private class Main : CliktCommand( } else if (!file.isDirectory) { throw IOException("Data dir is not a directory: ${file.absolutePath}") } - val perms = HashSet() - perms.add(OWNER_READ) - perms.add(OWNER_WRITE) - perms.add(OWNER_EXECUTE) - setPosixFilePermissions(file.toPath(), perms) + if (isLinux() || isMac()) { + val perms = HashSet() + perms.add(OWNER_READ) + perms.add(OWNER_WRITE) + perms.add(OWNER_EXECUTE) + setPosixFilePermissions(file.toPath(), perms) + } return file }