diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index f6631329795d70f75db972aac4a1f206dfdaf754..69c6325f0522458a57a0e6c7e55cb3d4a1130f87 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -6,6 +6,7 @@ </option> <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> + <option name="ALLOW_TRAILING_COMMA" value="true" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> <codeStyleSettings language="XML"> diff --git a/build.gradle b/build.gradle index 5dbb31c8e66191bebfac5b0d44c3718d24a7a242..20a8ec64acc71ca227ade83c21a2017294b49595 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ buildscript { ext.kotlin_version = '1.5.21' ext.hilt_version = '2.38.1' + ext.tor_version = '0.3.5.15' + ext.obfs4_version = '0.0.12-dev-40245c4a' ext.junit_version = '5.7.2' ext.mockk_version = '1.10.4' repositories { @@ -17,6 +19,7 @@ buildscript { allprojects { repositories { google() + jcenter() // for dependencies that haven't moved, yet mavenCentral() } } diff --git a/mailbox-android/.gitignore b/mailbox-android/.gitignore index 42afabfd2abebf31384ca7797186a27a4b7dbee8..baba0a48b70154e90f4e94917ef00b07632d174f 100644 --- a/mailbox-android/.gitignore +++ b/mailbox-android/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +/src/main/res/raw/geoip.zip +/src/main/jniLibs/*/lib*.so diff --git a/mailbox-android/build.gradle b/mailbox-android/build.gradle index ca37ef77ee00e0df0f576a3c135870bbacec7c88..3825d95d14ac73864a93d231a0628eae1880ce70 100644 --- a/mailbox-android/build.gradle +++ b/mailbox-android/build.gradle @@ -1,3 +1,5 @@ +import com.android.build.gradle.tasks.MergeResources + plugins { id 'com.android.application' id 'kotlin-android' @@ -43,6 +45,10 @@ android { } } +configurations { + tor +} + dependencies { implementation project(path: ':mailbox-core', configuration: 'default') @@ -66,10 +72,71 @@ dependencies { def multidex_version = "2.0.1" implementation "androidx.multidex:multidex:$multidex_version" + tor "org.briarproject:tor-android:$tor_version" + tor "org.briarproject:obfs4proxy-android:$obfs4_version@zip" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } +def torBinariesDir = 'src/main/res/raw' +def torLibsDir = 'src/main/jniLibs' + +task cleanTorBinaries { + doLast { + delete fileTree(torBinariesDir) { include '*.zip' } + delete fileTree(torLibsDir) { include '**/*.so' } + } +} + +clean.dependsOn cleanTorBinaries + +task unpackTorBinaries { + doLast { + copy { + from configurations.tor.collect { zipTree(it) } + into torBinariesDir + include 'geoip.zip' + } + configurations.tor.each { outer -> + zipTree(outer).each { inner -> + if (inner.name.endsWith('_arm_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'armeabi-v7a/lib$1.so' + } + } else if (inner.name.endsWith('_arm64_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'arm64-v8a/lib$1.so' + } + } else if (inner.name.endsWith('_x86_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'x86/lib$1.so' + } + } else if (inner.name.endsWith('_x86_64_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'x86_64/lib$1.so' + } + } + } + } + } + dependsOn cleanTorBinaries +} + +tasks.withType(MergeResources) { + inputs.dir torBinariesDir + inputs.dir torLibsDir + dependsOn unpackTorBinaries +} + apply from: "${rootProject.rootDir}/gradle/ktlint.gradle" diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt index b8a6bd90022d3a2f2df6cbc383c9eb3cfe9d6b5f..68239a115ae4568bbdf781cf29e6be7174051d97 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt @@ -4,10 +4,12 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.briarproject.mailbox.core.CoreModule +import org.briarproject.mailbox.core.tor.AndroidTorModule @Module( includes = [ CoreModule::class, + AndroidTorModule::class, ] ) @InstallIn(SingletonComponent::class) diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt index 939a5acf167312898f38f00e5f61d7f5d4522eeb..a8788fa9413f743ae53caff854effe8d6f0c5735 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt @@ -2,6 +2,7 @@ package org.briarproject.mailbox.android import androidx.multidex.MultiDexApplication import dagger.hilt.android.HiltAndroidApp +import org.briarproject.mailbox.core.AndroidEagerSingletons import org.briarproject.mailbox.core.CoreEagerSingletons import javax.inject.Inject @@ -9,7 +10,10 @@ import javax.inject.Inject class MailboxApplication : MultiDexApplication() { @Inject - lateinit var coreEagerSingletons: CoreEagerSingletons + internal lateinit var coreEagerSingletons: CoreEagerSingletons + + @Inject + internal lateinit var androidEagerSingletons: AndroidEagerSingletons override fun onCreate() { super.onCreate() diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/AndroidEagerSingletons.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/core/AndroidEagerSingletons.kt new file mode 100644 index 0000000000000000000000000000000000000000..baae3905f77ffafb0ce3273466c1149b7614b6d1 --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/AndroidEagerSingletons.kt @@ -0,0 +1,9 @@ +package org.briarproject.mailbox.core + +import org.briarproject.mailbox.core.tor.AndroidTorPlugin +import javax.inject.Inject + +@Suppress("unused") +internal class AndroidEagerSingletons @Inject constructor( + val androidTorPlugin: AndroidTorPlugin, +) diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..d1eb0cd88b383bce439f0f99c1b786976b9c75e7 --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt @@ -0,0 +1,76 @@ +package org.briarproject.mailbox.core.tor + +import android.content.Context +import android.content.res.Resources +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.android.api.system.AndroidWakeLockManager +import org.briarproject.mailbox.core.lifecycle.IoExecutor +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import org.briarproject.mailbox.core.system.Clock +import org.briarproject.mailbox.core.system.LocationUtils +import org.briarproject.mailbox.core.system.ResourceProvider +import java.util.concurrent.Executor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class AndroidTorModule { + + @Provides + @Singleton + fun provideResourceProvider(@ApplicationContext ctx: Context): ResourceProvider { + return ResourceProvider { name, _ -> + val res: Resources = ctx.resources + // extension is ignored on Android, resources are retrieved without it + val resId = res.getIdentifier(name, "raw", ctx.packageName) + res.openRawResource(resId) + } + } + + @Provides + @Singleton + fun provideAndroidTorPlugin( + @ApplicationContext app: Context, + @IoExecutor ioExecutor: Executor, + networkManager: NetworkManager, + locationUtils: LocationUtils, + clock: Clock, + resourceProvider: ResourceProvider, + circumventionProvider: CircumventionProvider, + androidWakeLockManager: AndroidWakeLockManager, + backoff: Backoff, + lifecycleManager: LifecycleManager, + ) = AndroidTorPlugin( + ioExecutor, + app, + networkManager, + locationUtils, + clock, + resourceProvider, + circumventionProvider, + androidWakeLockManager, + backoff, + architecture, + app.getDir("tor", Context.MODE_PRIVATE), + ).also { lifecycleManager.registerService(it) } + + private val architecture: String + get() { + for (abi in AndroidTorPlugin.getSupportedArchitectures()) { + return when { + abi.startsWith("x86_64") -> "x86_64_pie" + abi.startsWith("x86") -> "x86_pie" + abi.startsWith("arm64") -> "arm64_pie" + abi.startsWith("armeabi") -> "arm_pie" + else -> continue + } + } +// LOG.info("Tor is not supported on this architecture") + return "" // TODO + } + +} diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..d25ee11357e787d1e0108329863e51b9a8a7859e --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java @@ -0,0 +1,213 @@ +package org.briarproject.mailbox.core.tor; + +import static android.os.Build.VERSION.SDK_INT; +import static java.util.Arrays.asList; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; + +import org.briarproject.mailbox.android.api.system.AndroidWakeLock; +import org.briarproject.mailbox.android.api.system.AndroidWakeLockManager; +import org.briarproject.mailbox.core.system.Clock; +import org.briarproject.mailbox.core.system.LocationUtils; +import org.briarproject.mailbox.core.system.ResourceProvider; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class AndroidTorPlugin extends TorPlugin { + + private static final List<String> LIBRARY_ARCHITECTURES = + asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64"); + + private static final String TOR_LIB_NAME = "libtor.so"; + private static final String OBFS4_LIB_NAME = "libobfs4proxy.so"; + + private static final Logger LOG = + getLogger(AndroidTorPlugin.class.getName()); + + private final Context ctx; + private final AndroidWakeLock wakeLock; + private final File torLib, obfs4Lib; + + AndroidTorPlugin(Executor ioExecutor, + Context ctx, + NetworkManager networkManager, + LocationUtils locationUtils, + Clock clock, + ResourceProvider resourceProvider, + CircumventionProvider circumventionProvider, + AndroidWakeLockManager wakeLockManager, + Backoff backoff, + String architecture, + File torDirectory) { + super(ioExecutor, networkManager, locationUtils, clock, resourceProvider, circumventionProvider, backoff, architecture, torDirectory); + this.ctx = ctx; + wakeLock = wakeLockManager.createWakeLock("TorPlugin"); + String nativeLibDir = ctx.getApplicationInfo().nativeLibraryDir; + torLib = new File(nativeLibDir, TOR_LIB_NAME); + obfs4Lib = new File(nativeLibDir, OBFS4_LIB_NAME); + } + + @Override + protected int getProcessId() { + return android.os.Process.myPid(); + } + + @Override + protected long getLastUpdateTime() { + try { + PackageManager pm = ctx.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), 0); + return pi.lastUpdateTime; + } catch (NameNotFoundException e) { + throw new AssertionError(e); + } + } + + @Override + protected void enableNetwork(boolean enable) throws IOException { + if (enable) wakeLock.acquire(); + super.enableNetwork(enable); + if (!enable) wakeLock.release(); + } + + @Override + public void stopService() { + super.stopService(); + wakeLock.release(); + } + + @Override + protected File getTorExecutableFile() { + return torLib.exists() ? torLib : super.getTorExecutableFile(); + } + + @Override + protected File getObfs4ExecutableFile() { + return obfs4Lib.exists() ? obfs4Lib : super.getObfs4ExecutableFile(); + } + + @Override + protected void installTorExecutable() throws IOException { + File extracted = super.getTorExecutableFile(); + if (torLib.exists()) { + // If an older version left behind a Tor binary, delete it + if (extracted.exists()) { + if (extracted.delete()) LOG.info("Deleted Tor binary"); + else LOG.info("Failed to delete Tor binary"); + } + } else if (SDK_INT < 29) { + // The binary wasn't extracted at install time. Try to extract it + extractLibraryFromApk(TOR_LIB_NAME, extracted); + } else { + // No point extracting the binary, we won't be allowed to execute it + throw new FileNotFoundException(torLib.getAbsolutePath()); + } + } + + @Override + protected void installObfs4Executable() throws IOException { + File extracted = super.getObfs4ExecutableFile(); + if (obfs4Lib.exists()) { + // If an older version left behind an obfs4 binary, delete it + if (extracted.exists()) { + if (extracted.delete()) LOG.info("Deleted obfs4 binary"); + else LOG.info("Failed to delete obfs4 binary"); + } + } else if (SDK_INT < 29) { + // The binary wasn't extracted at install time. Try to extract it + extractLibraryFromApk(OBFS4_LIB_NAME, extracted); + } else { + // No point extracting the binary, we won't be allowed to execute it + throw new FileNotFoundException(obfs4Lib.getAbsolutePath()); + } + } + + private void extractLibraryFromApk(String libName, File dest) + throws IOException { + File sourceDir = new File(ctx.getApplicationInfo().sourceDir); + if (sourceDir.isFile()) { + // Look for other APK files in the same directory, if we're allowed + File parent = sourceDir.getParentFile(); + if (parent != null) sourceDir = parent; + } + List<String> libPaths = getSupportedLibraryPaths(libName); + for (File apk : findApkFiles(sourceDir)) { + ZipInputStream zin = new ZipInputStream(new FileInputStream(apk)); + for (ZipEntry e = zin.getNextEntry(); e != null; + e = zin.getNextEntry()) { + if (libPaths.contains(e.getName())) { + if (LOG.isLoggable(INFO)) { + LOG.info("Extracting " + e.getName() + + " from " + apk.getAbsolutePath()); + } + extract(zin, dest); // Zip input stream will be closed + return; + } + } + zin.close(); + } + throw new FileNotFoundException(libName); + } + + /** + * Returns all files with the extension .apk or .APK under the given root. + */ + private List<File> findApkFiles(File root) { + List<File> files = new ArrayList<>(); + findApkFiles(root, files); + return files; + } + + private void findApkFiles(File f, List<File> files) { + if (f.isFile() && f.getName().toLowerCase().endsWith(".apk")) { + files.add(f); + } else if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children != null) { + for (File child : children) findApkFiles(child, files); + } + } + } + + /** + * Returns the paths at which libraries with the given name would be found + * inside an APK file, for all architectures supported by the device, in + * order of preference. + */ + private List<String> getSupportedLibraryPaths(String libName) { + List<String> architectures = new ArrayList<>(); + for (String abi : getSupportedArchitectures()) { + if (LIBRARY_ARCHITECTURES.contains(abi)) { + architectures.add("lib/" + abi + "/" + libName); + } + } + return architectures; + } + + static Collection<String> getSupportedArchitectures() { + List<String> abis = new ArrayList<>(); + if (SDK_INT >= 21) { + abis.addAll(asList(Build.SUPPORTED_ABIS)); + } else { + abis.add(Build.CPU_ABI); + if (Build.CPU_ABI2 != null) abis.add(Build.CPU_ABI2); + } + return abis; + } +} diff --git a/mailbox-cli/.gitignore b/mailbox-cli/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8376b62300be1314da3229f59f10027007198768 --- /dev/null +++ b/mailbox-cli/.gitignore @@ -0,0 +1 @@ +/src/main/resources/*.zip diff --git a/mailbox-cli/build.gradle b/mailbox-cli/build.gradle index 6cd010ccc834b4a78db83cdd56aa04bf12d6b2e3..e0c9c7d51f2858d7f9709579e9a58a236085214c 100644 --- a/mailbox-cli/build.gradle +++ b/mailbox-cli/build.gradle @@ -8,12 +8,25 @@ plugins { sourceCompatibility = 1.8 targetCompatibility = 1.8 +configurations { + tor +} + dependencies { implementation project(path: ':mailbox-core', configuration: 'default') implementation 'org.slf4j:slf4j-simple:1.7.30' implementation 'com.github.ajalt:clikt:2.2.0' + def jna_version = '5.8.0' + implementation "net.java.dev.jna:jna:$jna_version" + implementation "net.java.dev.jna:jna-platform:$jna_version" + tor "org.briarproject:tor:$tor_version" + tor "org.briarproject:obfs4proxy:$obfs4_version@zip" + + implementation "com.google.dagger:hilt-core:$hilt_version" + kapt "com.google.dagger:hilt-compiler:$hilt_version" + testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version" testImplementation "org.junit.jupiter:junit-jupiter-params:$junit_version" testImplementation "org.junit.jupiter:junit-jupiter-engine:$junit_version" @@ -30,3 +43,42 @@ test { events "passed", "skipped", "failed" } } + +// At the moment for non-Android projects we need to explicitly mark the code generated by kapt +// as 'generated source code' for correct highlighting and resolve in IDE. +idea { + module { + sourceDirs += file('build/generated/source/kapt/main') + testSourceDirs += file('build/generated/source/kapt/test') + generatedSourceDirs += file('build/generated/source/kapt/main') + } +} + +def torBinariesDir = 'src/main/resources' + +task cleanTorBinaries { + doLast { + delete fileTree(torBinariesDir) { include '*.zip' } + } +} + +clean.dependsOn cleanTorBinaries + +task unpackTorBinaries { + doLast { + copy { + from configurations.tor.collect { zipTree(it) } + into torBinariesDir + } + } + dependsOn cleanTorBinaries +} + +processResources { + inputs.dir torBinariesDir + dependsOn unpackTorBinaries +} + +tasks.withType(Test) { + systemProperty 'java.library.path', 'libs' +} diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliComponent.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..f35b13191373058110df60b79e1fb9561d51a2be --- /dev/null +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliComponent.kt @@ -0,0 +1,14 @@ +package org.briarproject.mailbox.cli + +import dagger.Component +import javax.inject.Singleton + +@Singleton +@Component( + modules = [ + JavaCliModule::class, + ] +) +interface JavaCliComponent { + fun inject(main: Main) +} diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ee6c354a3c2cf83efe9b2bfed34d1beef61d547c --- /dev/null +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt @@ -0,0 +1,16 @@ +package org.briarproject.mailbox.cli + +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.CoreModule +import org.briarproject.mailbox.core.tor.JavaTorModule + +@Module( + includes = [ + CoreModule::class, + JavaTorModule::class, + ] +) +@InstallIn(SingletonComponent::class) +internal class JavaCliModule diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt index d8eec8e63a3d539b4824ec46ada496e94acd274f..22f7f445ba12c2c1659a721a5d3a9b8dbff308fe 100644 --- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt @@ -4,19 +4,23 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.counted import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option +import org.briarproject.mailbox.core.CoreEagerSingletons +import org.briarproject.mailbox.core.JavaCliEagerSingletons +import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY import java.lang.System.setProperty import java.util.logging.Level.ALL import java.util.logging.Level.INFO import java.util.logging.Level.WARNING import java.util.logging.LogManager +import javax.inject.Inject -private class Main : CliktCommand( +class Main : CliktCommand( name = "briar-mailbox", help = "Command line interface for the Briar Mailbox" ) { private val debug by option("--debug", "-d", help = "Enable printing of debug messages").flag( - default = false + default = true//false ) private val verbosity by option( "--verbose", @@ -24,6 +28,15 @@ private class Main : CliktCommand( help = "Print verbose log messages" ).counted() + @Inject + internal lateinit var coreEagerSingletons: CoreEagerSingletons + + @Inject + internal lateinit var javaCliEagerSingletons: JavaCliEagerSingletons + + @Inject + internal lateinit var lifecycleManager: LifecycleManager + override fun run() { // logging val levelSlf4j = if (debug) "DEBUG" else when (verbosity) { @@ -40,6 +53,17 @@ private class Main : CliktCommand( LogManager.getLogManager().getLogger("").level = level println("Hello Mailbox") + + val javaCliComponent = DaggerJavaCliComponent.builder().build() + javaCliComponent.inject(this) + + Runtime.getRuntime().addShutdownHook(Thread { + lifecycleManager.stopServices() + lifecycleManager.waitForShutdown() + }) + + lifecycleManager.startServices() + lifecycleManager.waitForStartup() } } diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/JavaCliEagerSingletons.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/JavaCliEagerSingletons.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff5328b0bfca3e9b1fef791e12212a8f704ec039 --- /dev/null +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/JavaCliEagerSingletons.kt @@ -0,0 +1,9 @@ +package org.briarproject.mailbox.core + +import org.briarproject.mailbox.core.tor.JavaTorPlugin +import javax.inject.Inject + +@Suppress("unused") +internal class JavaCliEagerSingletons @Inject constructor( + val javaTorPlugin: JavaTorPlugin, +) diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..53d14fc7da85d2293c5fe2642618d7ff0eff6275 --- /dev/null +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt @@ -0,0 +1,78 @@ +package org.briarproject.mailbox.core.tor + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.lifecycle.IoExecutor +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import org.briarproject.mailbox.core.system.Clock +import org.briarproject.mailbox.core.system.LocationUtils +import org.briarproject.mailbox.core.system.ResourceProvider +import org.briarproject.mailbox.core.util.OsUtils.isLinux +import java.io.File +import java.util.concurrent.Executor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class JavaTorModule { + + @Provides + @Singleton + fun provideResourceProvider() = ResourceProvider { name, extension -> + val cl = javaClass.classLoader + cl.getResourceAsStream(name + extension) + } + + @Provides + @Singleton + fun provideJavaTorPlugin( + @IoExecutor ioExecutor: Executor, + networkManager: NetworkManager, + locationUtils: LocationUtils, + clock: Clock, + resourceProvider: ResourceProvider, + circumventionProvider: CircumventionProvider, + backoff: Backoff, + lifecycleManager: LifecycleManager, + ): JavaTorPlugin { + val configDir = File(System.getProperty("user.home") + File.separator + ".config") + val mailboxDir = File(configDir, ".briar-mailbox") + val torDir = File(mailboxDir, "tor") + return JavaTorPlugin( + ioExecutor, + networkManager, + locationUtils, + clock, + resourceProvider, + circumventionProvider, + backoff, + architecture, + torDir, + ).also { lifecycleManager.registerService(it) } + } + + private val architecture: String + get() { + if (isLinux()) { +// if (LOG.isLoggable(Level.INFO)) { +// LOG.info("System's os.arch is $arch") +// } + when (System.getProperty("os.arch")) { + "amd64" -> { + return "linux-x86_64" + } + "aarch64" -> { + return "linux-aarch64" + } + "arm" -> { + return "linux-armhf" + } + } + } +// LOG.info("Tor is not supported on this architecture") + return "" // TODO + } + +} diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..0ecdd95f419b492736bec24651c92a96e28a6339 --- /dev/null +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java @@ -0,0 +1,56 @@ +package org.briarproject.mailbox.core.tor; + +import com.sun.jna.Library; +import com.sun.jna.Native; + +import org.briarproject.mailbox.core.system.Clock; +import org.briarproject.mailbox.core.system.LocationUtils; +import org.briarproject.mailbox.core.system.ResourceProvider; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.CodeSource; +import java.util.concurrent.Executor; + +public class JavaTorPlugin extends TorPlugin { + + JavaTorPlugin(Executor ioExecutor, + NetworkManager networkManager, + LocationUtils locationUtils, + Clock clock, + ResourceProvider resourceProvider, + CircumventionProvider circumventionProvider, + Backoff backoff, + String architecture, + File torDirectory) { + super(ioExecutor, networkManager, locationUtils, clock, resourceProvider, + circumventionProvider, backoff, architecture, torDirectory); + } + + @Override + protected long getLastUpdateTime() { + CodeSource codeSource = + getClass().getProtectionDomain().getCodeSource(); + if (codeSource == null) throw new AssertionError("CodeSource null"); + try { + URI path = codeSource.getLocation().toURI(); + File file = new File(path); + return file.lastModified(); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Override + protected int getProcessId() { + return CLibrary.INSTANCE.getpid(); + } + + private interface CLibrary extends Library { + + CLibrary INSTANCE = Native.load("c", CLibrary.class); + + int getpid(); + } +} diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle index 82bc45a5ca36a534ccd29e24542f97f306d71895..8dd8fb3806806261d5caa70aa58dfbbce997a6ab 100644 --- a/mailbox-core/build.gradle +++ b/mailbox-core/build.gradle @@ -16,6 +16,8 @@ dependencies { implementation "com.google.dagger:hilt-core:$hilt_version" kapt "com.google.dagger:dagger-compiler:$hilt_version" + implementation 'org.briarproject:jtorctl:0.3' + def ktorVersion = '1.6.2' implementation "io.ktor:ktor-server-core:$ktorVersion" implementation "io.ktor:ktor-server-netty:$ktorVersion" diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletons.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletons.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6e046bd0406496e3c36fce4a191fe41fb2d093b --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletons.kt @@ -0,0 +1,9 @@ +package org.briarproject.mailbox.core + +import org.briarproject.mailbox.core.server.WebServerManager +import javax.inject.Inject + +@Suppress("unused") +class CoreEagerSingletons @Inject constructor( + val webServerManager: WebServerManager, +) diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletonsModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletonsModule.kt deleted file mode 100644 index a1e7c297842170d682d509bd1d5b764f5bca4669..0000000000000000000000000000000000000000 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletonsModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.briarproject.mailbox.core - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import org.briarproject.mailbox.core.server.WebServerManager -import javax.inject.Inject -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal class CoreEagerSingletonsModule { - - @Provides - @Singleton - fun provideEagerSingletons(webServerManager: WebServerManager): CoreEagerSingletons { - return CoreEagerSingletons() - } - -} - -class CoreEagerSingletons { - - @Inject - internal lateinit var webServerManager: WebServerManager - -} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt index b14ecb17627129c73639980eb90ffce490c6d0c6..b0c37df01a5b6bfe334310af7385ac377b8b8d17 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt @@ -1,19 +1,27 @@ package org.briarproject.mailbox.core import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.briarproject.mailbox.core.db.DatabaseModule import org.briarproject.mailbox.core.lifecycle.LifecycleModule import org.briarproject.mailbox.core.server.WebServerModule +import org.briarproject.mailbox.core.system.Clock +import org.briarproject.mailbox.core.tor.TorModule +import javax.inject.Singleton @Module( includes = [ - CoreEagerSingletonsModule::class, LifecycleModule::class, DatabaseModule::class, WebServerModule::class, + TorModule::class, ] ) @InstallIn(SingletonComponent::class) -class CoreModule \ No newline at end of file +class CoreModule { + @Singleton + @Provides + fun provideClock() = Clock { System.currentTimeMillis() } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/PoliteExecutor.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/PoliteExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..671842ce53737f946533ce9d418d28c37260efe4 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/PoliteExecutor.java @@ -0,0 +1,76 @@ +package org.briarproject.mailbox.core; + +import static org.briarproject.mailbox.core.util.LogUtils.now; +import static java.util.logging.Level.FINE; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.concurrent.GuardedBy; + +/** + * An {@link Executor} that delegates its tasks to another {@link Executor} + * while limiting the number of tasks that are delegated concurrently. Tasks + * are delegated in the order they are submitted to this executor. + */ +public class PoliteExecutor implements Executor { + + private final Object lock = new Object(); + @GuardedBy("lock") + private final Queue<Runnable> queue = new LinkedList<>(); + private final Executor delegate; + private final int maxConcurrentTasks; + private final Logger log; + + @GuardedBy("lock") + private int concurrentTasks = 0; + + /** + * @param tag the tag to be used for logging + * @param delegate the executor to which tasks will be delegated + * @param maxConcurrentTasks the maximum number of tasks that will be + * delegated concurrently. If this is set to 1, tasks submitted to this + * executor will run in the order they are submitted and will not run + * concurrently + */ + public PoliteExecutor(String tag, Executor delegate, + int maxConcurrentTasks) { + this.delegate = delegate; + this.maxConcurrentTasks = maxConcurrentTasks; + log = Logger.getLogger(tag); + } + + @Override + public void execute(Runnable r) { + long submitted = now(); + Runnable wrapped = () -> { + if (log.isLoggable(FINE)) { + long queued = now() - submitted; + log.fine("Queue time " + queued + " ms"); + } + try { + r.run(); + } finally { + scheduleNext(); + } + }; + synchronized (lock) { + if (concurrentTasks < maxConcurrentTasks) { + concurrentTasks++; + delegate.execute(wrapped); + } else { + queue.add(wrapped); + } + } + } + + private void scheduleNext() { + synchronized (lock) { + Runnable next = queue.poll(); + if (next == null) concurrentTasks--; + else delegate.execute(next); + } + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/IoExecutor.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/IoExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..45e8c51f15bc79ca99d84cd20c1e10ef055718d7 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/IoExecutor.java @@ -0,0 +1,25 @@ +package org.briarproject.mailbox.core.lifecycle; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Annotation for injecting the executor for long-running IO tasks. Also used + * for annotating methods that should run on the IO executor. + * <p> + * The contract of this executor is that tasks may be run concurrently, and + * submitting a task will never block. Tasks may run indefinitely. Tasks + * submitted during shutdown are discarded. + */ +@Qualifier +@Target({FIELD, METHOD, PARAMETER}) +@Retention(RUNTIME) +public @interface IoExecutor { +} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Clock.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Clock.java new file mode 100644 index 0000000000000000000000000000000000000000..2ed21d9e891f0526ddc496e3514e37c767bc09d8 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Clock.java @@ -0,0 +1,5 @@ +package org.briarproject.mailbox.core.system; + +public interface Clock { + long currentTimeMillis(); +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/LocationUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/LocationUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..9ce3cd09eae570fbb00a79d0fb27329611284c5f --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/LocationUtils.java @@ -0,0 +1,13 @@ +package org.briarproject.mailbox.core.system; + +public interface LocationUtils { + + /** + * Get the country the device is currently located in, or "" if it cannot + * be determined. + * <p> + * The country codes are formatted upper-case and as per <a href=" + * https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha 2</a>. + */ + String getCurrentCountry(); +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/ResourceProvider.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/ResourceProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..70ec10cd411deeb6dad0c511e7b96cc834502f44 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/ResourceProvider.java @@ -0,0 +1,8 @@ +package org.briarproject.mailbox.core.system; + +import java.io.InputStream; + +public interface ResourceProvider { + + InputStream getResourceInputStream(String name, String extension); +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/Backoff.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/Backoff.java new file mode 100644 index 0000000000000000000000000000000000000000..33aa5f9ee079a4e843bdc15d76ea652995680553 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/Backoff.java @@ -0,0 +1,22 @@ +package org.briarproject.mailbox.core.tor; + +/** + * Calculates polling intervals for transport plugins that use backoff. + */ +public interface Backoff { + + /** + * Returns the current polling interval. + */ + int getPollingInterval(); + + /** + * Increments the backoff counter. + */ + void increment(); + + /** + * Resets the backoff counter. + */ + void reset(); +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/BackoffImpl.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/BackoffImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..60f420fc69ca6547351dcf799c321b71750bf3bb --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/BackoffImpl.java @@ -0,0 +1,38 @@ +package org.briarproject.mailbox.core.tor; + +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +class BackoffImpl implements Backoff { + + private final int minInterval, maxInterval; + private final double base; + private final AtomicInteger backoff; + + BackoffImpl(int minInterval, int maxInterval, double base) { + this.minInterval = minInterval; + this.maxInterval = maxInterval; + this.base = base; + backoff = new AtomicInteger(0); + } + + @Override + public int getPollingInterval() { + double multiplier = Math.pow(base, backoff.get()); + // Large or infinite values will be rounded to Integer.MAX_VALUE + int interval = (int) (minInterval * multiplier); + return Math.min(interval, maxInterval); + } + + @Override + public void increment() { + backoff.incrementAndGet(); + } + + @Override + public void reset() { + backoff.set(0); + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/CircumventionProvider.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/CircumventionProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..f1d33614cc082a19f409921d57e196130f74af68 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/CircumventionProvider.java @@ -0,0 +1,35 @@ +package org.briarproject.mailbox.core.tor; + +import java.util.List; + +public interface CircumventionProvider { + + /** + * Countries where Tor is blocked, i.e. vanilla Tor connection won't work. + * + * See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + * and https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki + */ + String[] BLOCKED = {"CN", "IR", "EG", "BY", "TR", "SY", "VE"}; + + /** + * Countries where obfs4 or meek bridge connections are likely to work. + * Should be a subset of {@link #BLOCKED}. + */ + String[] BRIDGES = { "CN", "IR", "EG", "BY", "TR", "SY", "VE" }; + + /** + * Countries where obfs4 bridges won't work and meek is needed. + * Should be a subset of {@link #BRIDGES}. + */ + String[] NEEDS_MEEK = {"CN", "IR"}; + + boolean isTorProbablyBlocked(String countryCode); + + boolean doBridgesWork(String countryCode); + + boolean needsMeek(String countryCode); + + List<String> getBridges(boolean meek); + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/NetworkManager.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/NetworkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..fb0428ae86f9bef0461b047eb44884aa9a022e73 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/NetworkManager.java @@ -0,0 +1,6 @@ +package org.briarproject.mailbox.core.tor; + +public interface NetworkManager { + + NetworkStatus getNetworkStatus(); +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/NetworkStatus.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/NetworkStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..8176351ac4b8591051b2a228c297c721c39c0406 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/NetworkStatus.java @@ -0,0 +1,27 @@ +package org.briarproject.mailbox.core.tor; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public class NetworkStatus { + + private final boolean connected, wifi, ipv6Only; + + public NetworkStatus(boolean connected, boolean wifi, boolean ipv6Only) { + this.connected = connected; + this.wifi = wifi; + this.ipv6Only = ipv6Only; + } + + public boolean isConnected() { + return connected; + } + + public boolean isWifi() { + return wifi; + } + + public boolean isIpv6Only() { + return ipv6Only; + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorConstants.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..f0a2b52b7d1a2ba9a6bebed445bc434ef19256fa --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorConstants.java @@ -0,0 +1,29 @@ +package org.briarproject.mailbox.core.tor; + +public interface TorConstants { + + // Transport properties + String PROP_ONION_V3 = "onion3"; + + int SOCKS_PORT = 59050; + int CONTROL_PORT = 59051; + + int CONNECT_TO_PROXY_TIMEOUT = 5000; // Milliseconds + int EXTRA_SOCKET_TIMEOUT = 30000; // Milliseconds + + // Local settings (not shared with contacts) + String HS_PRIVATE_KEY_V3 = "onionPrivKey3"; + String HS_V3_CREATED = "onionPrivKey3Created"; + + // Values for PREF_TOR_NETWORK + int PREF_TOR_NETWORK_AUTOMATIC = 0; + int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1; + int PREF_TOR_NETWORK_WITH_BRIDGES = 2; + + // Default values for local settings + boolean DEFAULT_PREF_PLUGIN_ENABLE = true; + int DEFAULT_PREF_TOR_NETWORK = PREF_TOR_NETWORK_AUTOMATIC; + boolean DEFAULT_PREF_TOR_MOBILE = true; + boolean DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING = false; + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..833f19012bbff51d4f09fcaab2ea1cb7d204dfcb --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorModule.kt @@ -0,0 +1,63 @@ +package org.briarproject.mailbox.core.tor + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.lifecycle.IoExecutor +import org.briarproject.mailbox.core.system.LocationUtils +import java.util.concurrent.Executor +import java.util.concurrent.SynchronousQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class TorModule { + + companion object { + private const val MIN_POLLING_INTERVAL = 60 * 1000 // 1 minute + private const val MAX_POLLING_INTERVAL = 10 * 60 * 1000 // 10 mins + private const val BACKOFF_BASE = 1.2 + } + + @Provides + @Singleton + fun provideNetworkManager() = NetworkManager { + NetworkStatus(true, true, false) + } + + @Provides + @Singleton + fun provideLocationUtils() = LocationUtils { "" } + + @Provides + @Singleton + fun provideCircumventionProvider(): CircumventionProvider = object : CircumventionProvider { + override fun isTorProbablyBlocked(countryCode: String?) = false + override fun doBridgesWork(countryCode: String?) = true + override fun needsMeek(countryCode: String?) = false + override fun getBridges(meek: Boolean): List<String> = emptyList() + } + + @Provides + @Singleton + fun provideBackoff(): Backoff { + return BackoffImpl(MIN_POLLING_INTERVAL, MAX_POLLING_INTERVAL, BACKOFF_BASE) + } + + @Provides + @Singleton + @IoExecutor + fun provideIoExecutor(): Executor = ThreadPoolExecutor( + 0, + Int.MAX_VALUE, + 60, + TimeUnit.SECONDS, + SynchronousQueue(), + DiscardPolicy() + ) + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..eff73f5e95cf65d410d7c847eaeb59f8281af93d --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java @@ -0,0 +1,647 @@ +package org.briarproject.mailbox.core.tor; + +import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS; +import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY; +import static org.briarproject.mailbox.core.tor.TorConstants.CONTROL_PORT; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.ACTIVE; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.DISABLED; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.ENABLING; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.INACTIVE; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.STARTING_STOPPING; +import static org.briarproject.mailbox.core.util.IoUtils.copyAndClose; +import static org.briarproject.mailbox.core.util.IoUtils.tryToClose; +import static org.briarproject.mailbox.core.util.LogUtils.logException; +import static org.briarproject.mailbox.core.util.PrivacyUtils.scrubOnion; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; + +import net.freehaven.tor.control.EventHandler; +import net.freehaven.tor.control.TorControlConnection; + +import org.briarproject.mailbox.core.PoliteExecutor; +import org.briarproject.mailbox.core.lifecycle.Service; +import org.briarproject.mailbox.core.lifecycle.ServiceException; +import org.briarproject.mailbox.core.system.Clock; +import org.briarproject.mailbox.core.system.LocationUtils; +import org.briarproject.mailbox.core.system.ResourceProvider; + +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; +import java.util.zip.ZipInputStream; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +abstract class TorPlugin implements Service, EventHandler { + + private static final Logger LOG = getLogger(TorPlugin.class.getName()); + + private static final String[] EVENTS = { + "CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR" + }; + private static final String OWNER = "__OwningControllerProcess"; + private static final int COOKIE_TIMEOUT_MS = 3000; + private static final int COOKIE_POLLING_INTERVAL_MS = 200; + + private final Executor ioExecutor; + private final Executor connectionStatusExecutor; + private final NetworkManager networkManager; + private final LocationUtils locationUtils; + private final Clock clock; + private final Backoff backoff; + private final String architecture; + private final CircumventionProvider circumventionProvider; + private final ResourceProvider resourceProvider; + private final File torDirectory, geoIpFile, configFile; + private final File doneFile, cookieFile; + private final AtomicBoolean used = new AtomicBoolean(false); + + protected final PluginState state = new PluginState(); + + private volatile Socket controlSocket = null; + private volatile TorControlConnection controlConnection = null; + + protected abstract int getProcessId(); + + protected abstract long getLastUpdateTime(); + + TorPlugin(Executor ioExecutor, + NetworkManager networkManager, + LocationUtils locationUtils, + Clock clock, + ResourceProvider resourceProvider, + CircumventionProvider circumventionProvider, + Backoff backoff, + String architecture, + File torDirectory) { + this.ioExecutor = ioExecutor; + this.networkManager = networkManager; + this.locationUtils = locationUtils; + this.clock = clock; + this.resourceProvider = resourceProvider; + this.circumventionProvider = circumventionProvider; + this.backoff = backoff; + this.architecture = architecture; + this.torDirectory = torDirectory; + geoIpFile = new File(torDirectory, "geoip"); + configFile = new File(torDirectory, "torrc"); + doneFile = new File(torDirectory, "done"); + cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); + // Don't execute more than one connection status check at a time + connectionStatusExecutor = + new PoliteExecutor("TorPlugin", ioExecutor, 1); + } + + protected File getTorExecutableFile() { + return new File(torDirectory, "tor"); + } + + protected File getObfs4ExecutableFile() { + return new File(torDirectory, "obfs4proxy"); + } + + @Override + public void startService() throws ServiceException { + if (used.getAndSet(true)) throw new IllegalStateException(); + if (!torDirectory.exists()) { + if (!torDirectory.mkdirs()) { + LOG.warning("Could not create Tor directory."); + throw new ServiceException(); + } + } + // 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()); + Process torProcess; + ProcessBuilder pb = + new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); + Map<String, String> env = pb.environment(); + env.put("HOME", torDirectory.getAbsolutePath()); + pb.directory(torDirectory); + try { + torProcess = pb.start(); + } catch (SecurityException | IOException e) { + throw new ServiceException(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 ServiceException(); + } + // 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); + throw new ServiceException(); + } + //noinspection BusyWait + Thread.sleep(COOKIE_POLLING_INTERVAL_MS); + } + LOG.info("Auth cookie created"); + } catch (InterruptedException e) { + LOG.warning("Interrupted while starting Tor"); + Thread.currentThread().interrupt(); + throw new ServiceException(); + } + try { + // Open a control connection and authenticate using the cookie file + controlSocket = new Socket("127.0.0.1", CONTROL_PORT); + 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 ServiceException(e); + } + state.setStarted(); + // Check whether we're online + updateConnectionStatus(networkManager.getNetworkStatus()); + // Create a hidden service if necessary + ioExecutor.execute(() -> publishHiddenService("8888")); + } + + private boolean assetsAreUpToDate() { + return doneFile.lastModified() > getLastUpdateTime(); + } + + private void installAssets() throws ServiceException { + try { + // The done file may already exist from a previous installation + //noinspection ResultOfMethodCallIgnored + doneFile.delete(); + installTorExecutable(); + installObfs4Executable(); + extract(getGeoIpInputStream(), geoIpFile); + extract(getConfigInputStream(), configFile); + if (!doneFile.createNewFile()) + LOG.warning("Failed to create done file"); + } catch (IOException e) { + throw new ServiceException(e); + } + } + + protected void extract(InputStream in, File dest) throws IOException { + OutputStream out = new FileOutputStream(dest); + copyAndClose(in, out); + } + + protected void installTorExecutable() throws IOException { + if (LOG.isLoggable(INFO)) + LOG.info("Installing Tor binary for " + architecture); + File torFile = getTorExecutableFile(); + extract(getTorInputStream(), torFile); + if (!torFile.setExecutable(true, true)) throw new IOException(); + } + + protected void installObfs4Executable() throws IOException { + if (LOG.isLoggable(INFO)) + LOG.info("Installing obfs4proxy binary for " + architecture); + File obfs4File = getObfs4ExecutableFile(); + extract(getObfs4InputStream(), obfs4File); + if (!obfs4File.setExecutable(true, true)) throw new IOException(); + } + + private InputStream getTorInputStream() throws IOException { + InputStream in = resourceProvider + .getResourceInputStream("tor_" + architecture, ".zip"); + ZipInputStream zin = new ZipInputStream(in); + if (zin.getNextEntry() == null) throw new IOException(); + return zin; + } + + private InputStream getGeoIpInputStream() throws IOException { + InputStream in = resourceProvider.getResourceInputStream("geoip", + ".zip"); + ZipInputStream zin = new ZipInputStream(in); + if (zin.getNextEntry() == null) throw new IOException(); + return zin; + } + + private InputStream getObfs4InputStream() throws IOException { + InputStream in = resourceProvider + .getResourceInputStream("obfs4proxy_" + architecture, ".zip"); + ZipInputStream zin = new ZipInputStream(in); + if (zin.getNextEntry() == null) throw new IOException(); + return zin; + } + + private InputStream getConfigInputStream() { + ClassLoader cl = getClass().getClassLoader(); + return requireNonNull(cl.getResourceAsStream("torrc")); + } + + private void listFiles(File f) { + if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children != null) for (File child : children) listFiles(child); + } else { + LOG.info(f.getAbsolutePath() + " " + f.length()); + } + } + + private byte[] read(File f) throws IOException { + byte[] b = new byte[(int) f.length()]; + FileInputStream in = new FileInputStream(f); + try { + int offset = 0; + while (offset < b.length) { + int read = in.read(b, offset, b.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + return b; + } finally { + tryToClose(in, LOG, WARNING); + } + } + + private void publishHiddenService(String port) { + if (!state.isTorRunning()) return; + // TODO get stored key + String privKey3 = null; + publishV3HiddenService(port, privKey3); + } + + private void publishV3HiddenService(String port, @Nullable String privKey) { + LOG.info("Creating v3 hidden service"); + Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port); + Map<String, String> response; + try { + // Use the control connection to set up the hidden service + if (privKey == null) { + response = controlConnection.addOnion("NEW:ED25519-V3", + portLines, null); + } else { + response = controlConnection.addOnion(privKey, portLines); + } + } catch (IOException e) { + logException(LOG, WARNING, e); + return; + } + if (!response.containsKey(HS_ADDRESS)) { + LOG.warning("Tor did not return a hidden service address"); + return; + } + if (privKey == null && !response.containsKey(HS_PRIVKEY)) { + LOG.warning("Tor did not return a private key"); + return; + } + String onion3 = response.get(HS_ADDRESS); + if (LOG.isLoggable(INFO)) { + LOG.info("V3 hidden service " + scrubOnion(onion3)); + } + // TODO remove + LOG.warning("V3 hidden service: http://" + onion3 + ".onion"); + if (privKey == null) { + // TODO Save the hidden service's onion hostname +// p.put(PROP_ONION_V3, onion3); + // TODO Save the hidden service's private key for next time +// s.put(HS_PRIVATE_KEY_V3, response.get(HS_PRIVKEY)); +// s.put(HS_V3_CREATED, String.valueOf(clock.currentTimeMillis())); + } + } + + protected void enableNetwork(boolean enable) throws IOException { + state.enableNetwork(enable); + controlConnection.setConf("DisableNetwork", enable ? "0" : "1"); + } + + private void enableBridges(boolean enable, boolean needsMeek) + throws IOException { + if (enable) { + Collection<String> conf = new ArrayList<>(); + conf.add("UseBridges 1"); + File obfs4File = getObfs4ExecutableFile(); + if (needsMeek) { + conf.add("ClientTransportPlugin meek_lite exec " + + obfs4File.getAbsolutePath()); + } else { + conf.add("ClientTransportPlugin obfs4 exec " + + obfs4File.getAbsolutePath()); + } + conf.addAll(circumventionProvider.getBridges(needsMeek)); + controlConnection.setConf(conf); + } else { + controlConnection.setConf("UseBridges", "0"); + } + } + + @Override + public void stopService() { + ServerSocket ss = state.setStopped(); + tryToClose(ss, LOG, WARNING); + if (controlSocket != null && controlConnection != null) { + try { + LOG.info("Stopping Tor"); + controlConnection.setConf("DisableNetwork", "1"); + controlConnection.shutdownTor("TERM"); + controlSocket.close(); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + } + } + + @Override + public void circuitStatus(String status, String id, String path) { + if (status.equals("BUILT") && + state.getAndSetCircuitBuilt()) { + LOG.info("First circuit built"); + backoff.reset(); + } + } + + @Override + public void streamStatus(String status, String id, String target) { + } + + @Override + public void orConnStatus(String status, String orName) { + if (LOG.isLoggable(INFO)) + LOG.info("OR connection " + status + " " + orName); + if (status.equals("CLOSED") || status.equals("FAILED")) { + // Check whether we've lost connectivity + updateConnectionStatus(networkManager.getNetworkStatus() + ); + } + } + + @Override + public void bandwidthUsed(long read, long written) { + } + + @Override + public void newDescriptors(List<String> orList) { + } + + @Override + public void message(String severity, String msg) { + if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg); + if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) { + state.setBootstrapped(); + backoff.reset(); + } + } + + @Override + public void unrecognized(String type, String msg) { + if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) { + LOG.info("V3 descriptor uploaded"); + } + } + + public void onNetworkStatusChanged() { + updateConnectionStatus(networkManager.getNetworkStatus()); + } + + private void disableNetwork() { + connectionStatusExecutor.execute(() -> { + try { + if (state.isTorRunning()) enableNetwork(false); + } catch (IOException ex) { + logException(LOG, WARNING, ex); + } + }); + } + + private void updateConnectionStatus(NetworkStatus status) { + connectionStatusExecutor.execute(() -> { + if (!state.isTorRunning()) return; + boolean online = status.isConnected(); + boolean wifi = status.isWifi(); + boolean ipv6Only = status.isIpv6Only(); + String country = locationUtils.getCurrentCountry(); + boolean blocked = + circumventionProvider.isTorProbablyBlocked(country); + boolean bridgesWork = circumventionProvider.doBridgesWork(country); + + if (LOG.isLoggable(INFO)) { + LOG.info("Online: " + online + ", wifi: " + wifi + + ", IPv6 only: " + ipv6Only); + if (country.isEmpty()) LOG.info("Country code unknown"); + else LOG.info("Country code: " + country); + } + + int reasonsDisabled = 0; + boolean enableNetwork = false; + boolean enableBridges = false; + boolean useMeek = false; + + if (!online) { + LOG.info("Disabling network, device is offline"); + } else { + LOG.info("Enabling network"); + enableNetwork = true; + if (blocked && bridgesWork) { + if (ipv6Only || circumventionProvider.needsMeek(country)) { + LOG.info("Using meek bridges"); + enableBridges = true; + useMeek = true; + } else { + LOG.info("Using obfs4 bridges"); + enableBridges = true; + } + } else { + LOG.info("Not using bridges"); + } + } + state.setReasonsDisabled(reasonsDisabled); + try { + if (enableNetwork) { + enableBridges(enableBridges, useMeek); + enableConnectionPadding(true); + useIpv6(ipv6Only); + } + enableNetwork(enableNetwork); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + }); + } + + private void enableConnectionPadding(boolean enable) throws IOException { + controlConnection.setConf("ConnectionPadding", enable ? "1" : "0"); + } + + private void useIpv6(boolean ipv6Only) throws IOException { + controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1"); + controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0"); + } + + @ThreadSafe + protected class PluginState { + + @GuardedBy("this") + private boolean started = false, + stopped = false, + networkInitialised = false, + networkEnabled = false, + bootstrapped = false, + circuitBuilt = false, + settingsChecked = false; + + @GuardedBy("this") + private int reasonsDisabled = 0; + + @GuardedBy("this") + @Nullable + private ServerSocket serverSocket = null; + + synchronized void setStarted() { + started = true; +// callback.pluginStateChanged(getState()); + } + + synchronized boolean isTorRunning() { + return started && !stopped; + } + + @Nullable + synchronized ServerSocket setStopped() { + stopped = true; + ServerSocket ss = serverSocket; + serverSocket = null; +// callback.pluginStateChanged(getState()); + return ss; + } + + synchronized void setBootstrapped() { + bootstrapped = true; +// callback.pluginStateChanged(getState()); + } + + synchronized boolean getAndSetCircuitBuilt() { + boolean firstCircuit = !circuitBuilt; + circuitBuilt = true; +// callback.pluginStateChanged(getState()); + return firstCircuit; + } + + synchronized void enableNetwork(boolean enable) { + networkInitialised = true; + networkEnabled = enable; + if (!enable) circuitBuilt = false; +// callback.pluginStateChanged(getState()); + } + + synchronized void setReasonsDisabled(int reasonsDisabled) { + settingsChecked = true; + this.reasonsDisabled = reasonsDisabled; +// callback.pluginStateChanged(getState()); + } + + // Doesn't affect getState() + synchronized boolean setServerSocket(ServerSocket ss) { + if (stopped || serverSocket != null) return false; + serverSocket = ss; + return true; + } + + // Doesn't affect getState() + synchronized void clearServerSocket(ServerSocket ss) { + if (serverSocket == ss) serverSocket = null; + } + + synchronized State getState() { + if (!started || stopped || !settingsChecked) { + return STARTING_STOPPING; + } + if (reasonsDisabled != 0) return DISABLED; + if (!networkInitialised) return ENABLING; + if (!networkEnabled) return INACTIVE; + return bootstrapped && circuitBuilt ? ACTIVE : ENABLING; + } + + synchronized int getReasonsDisabled() { + return getState() == DISABLED ? reasonsDisabled : 0; + } + + } + + enum State { + + /** + * The plugin has not finished starting or has been stopped. + */ + STARTING_STOPPING, + + /** + * The plugin is disabled by settings. + */ + DISABLED, + + /** + * The plugin is being enabled and can't yet make or receive + * connections. + */ + ENABLING, + + /** + * The plugin is enabled and can make or receive connections. + */ + ACTIVE, + + /** + * The plugin is enabled but can't make or receive connections + */ + INACTIVE + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..116363f427b252ecee8ba8796460627f9c92db5f --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java @@ -0,0 +1,123 @@ +package org.briarproject.mailbox.core.util; + +import static org.briarproject.mailbox.core.util.LogUtils.logException; +import static java.util.logging.Level.WARNING; + +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +public class IoUtils { + + private static final Logger LOG = Logger.getLogger(IoUtils.class.getName()); + + public static void deleteFileOrDir(File f) { + if (f.isFile()) { + delete(f); + } else if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children == null) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("Could not list files in " + + f.getAbsolutePath()); + } + } else { + for (File child : children) deleteFileOrDir(child); + } + delete(f); + } + } + + private static void delete(File f) { + if (!f.delete() && LOG.isLoggable(WARNING)) + LOG.warning("Could not delete " + f.getAbsolutePath()); + } + + public static void copyAndClose(InputStream in, OutputStream out) { + byte[] buf = new byte[4096]; + try { + while (true) { + int read = in.read(buf); + if (read == -1) break; + out.write(buf, 0, read); + } + in.close(); + out.flush(); + out.close(); + } catch (IOException e) { + tryToClose(in, LOG, WARNING); + tryToClose(out, LOG, WARNING); + } + } + + public static void tryToClose(@Nullable Closeable c, Logger logger, + Level level) { + try { + if (c != null) c.close(); + } catch (IOException e) { + logException(logger, level, e); + } + } + + public static void tryToClose(@Nullable Socket s, Logger logger, + Level level) { + try { + if (s != null) s.close(); + } catch (IOException e) { + logException(logger, level, e); + } + } + + public static void tryToClose(@Nullable ServerSocket ss, Logger logger, + Level level) { + try { + if (ss != null) ss.close(); + } catch (IOException e) { + logException(logger, level, e); + } + } + + public static void read(InputStream in, byte[] b) throws IOException { + int offset = 0; + while (offset < b.length) { + int read = in.read(b, offset, b.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + } + + // Workaround for a bug in Android 7, see + // https://android-review.googlesource.com/#/c/271775/ + public static InputStream getInputStream(Socket s) throws IOException { + try { + return s.getInputStream(); + } catch (NullPointerException e) { + throw new IOException(e); + } + } + + // Workaround for a bug in Android 7, see + // https://android-review.googlesource.com/#/c/271775/ + public static OutputStream getOutputStream(Socket s) throws IOException { + try { + return s.getOutputStream(); + } catch (NullPointerException e) { + throw new IOException(e); + } + } + + public static boolean isNonEmptyDirectory(File f) { + if (!f.isDirectory()) return false; + File[] children = f.listFiles(); + return children != null && children.length > 0; + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/OsUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/OsUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..52698ab2aad08cd1ea9a1efab26713b15c97550c --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/OsUtils.java @@ -0,0 +1,27 @@ +package org.briarproject.mailbox.core.util; + +import javax.annotation.Nullable; + +public class OsUtils { + + @Nullable + private static final String os = System.getProperty("os.name"); + @Nullable + private static final String vendor = System.getProperty("java.vendor"); + + public static boolean isWindows() { + return os != null && os.contains("Windows"); + } + + public static boolean isMac() { + return os != null && os.contains("Mac OS"); + } + + public static boolean isLinux() { + return os != null && os.contains("Linux") && !isAndroid(); + } + + public static boolean isAndroid() { + return vendor != null && vendor.contains("Android"); + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/PrivacyUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/PrivacyUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..246d4d2c99e1f5b997bdfa839a3b281253ef7bc7 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/PrivacyUtils.java @@ -0,0 +1,64 @@ +package org.briarproject.mailbox.core.util; + +import static org.briarproject.mailbox.core.util.StringUtils.isNullOrEmpty; +import static org.briarproject.mailbox.core.util.StringUtils.isValidMac; +import static org.briarproject.mailbox.core.util.StringUtils.toHexString; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import javax.annotation.Nullable; + +public class PrivacyUtils { + + public static String scrubOnion(String onion) { + // keep first three characters of onion address + return onion.substring(0, 3) + "[scrubbed]"; + } + + @Nullable + public static String scrubMacAddress(@Nullable String address) { + if (isNullOrEmpty(address) || !isValidMac(address)) return address; + // this is a fake address we need to know about + if (address.equals("02:00:00:00:00:00")) return address; + // keep first and last octet of MAC address + return address.substring(0, 3) + "[scrubbed]" + + address.substring(14, 17); + } + + public static String scrubInetAddress(InetAddress address) { + if (address instanceof Inet4Address) { + // Don't scrub local IPv4 addresses + if (address.isLoopbackAddress() || address.isLinkLocalAddress() || + address.isSiteLocalAddress()) { + return address.getHostAddress(); + } + // Keep first and last octet of non-local IPv4 addresses + return scrubIpv4Address(address.getAddress()); + } else { + // Keep first and last octet of IPv6 addresses + return scrubIpv6Address(address.getAddress()); + } + } + + private static String scrubIpv4Address(byte[] ipv4) { + return (ipv4[0] & 0xFF) + ".[scrubbed]." + (ipv4[3] & 0xFF); + } + + private static String scrubIpv6Address(byte[] ipv6) { + String hex = toHexString(ipv6).toLowerCase(); + return hex.substring(0, 2) + "[scrubbed]" + hex.substring(30); + } + + public static String scrubSocketAddress(InetSocketAddress address) { + return scrubInetAddress(address.getAddress()); + } + + public static String scrubSocketAddress(SocketAddress address) { + if (address instanceof InetSocketAddress) + return scrubSocketAddress((InetSocketAddress) address); + return "[scrubbed]"; + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/StringUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/StringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..2a5cf7991ddecd9fa982300a824c78b03808fd75 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/StringUtils.java @@ -0,0 +1,162 @@ +package org.briarproject.mailbox.core.util; + +import static java.nio.charset.CodingErrorAction.IGNORE; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.util.Collection; +import java.util.Random; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +public class StringUtils { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private static Pattern MAC = Pattern.compile("[0-9a-f]{2}:[0-9a-f]{2}:" + + "[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}", + CASE_INSENSITIVE); + + private static final char[] HEX = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + private static final Random random = new Random(); + + public static boolean isNullOrEmpty(@Nullable String s) { + return s == null || s.length() == 0; + } + + public static String join(Collection<String> strings, String separator) { + StringBuilder joined = new StringBuilder(); + for (String s : strings) { + if (joined.length() > 0) joined.append(separator); + joined.append(s); + } + return joined.toString(); + } + + public static byte[] toUtf8(String s) { + try { + return s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + public static String fromUtf8(byte[] bytes) { + return fromUtf8(bytes, 0, bytes.length); + } + + public static String fromUtf8(byte[] bytes, int off, int len) { + CharsetDecoder decoder = UTF_8.newDecoder(); + decoder.onMalformedInput(IGNORE); + decoder.onUnmappableCharacter(IGNORE); + ByteBuffer buffer = ByteBuffer.wrap(bytes, off, len); + try { + return decoder.decode(buffer).toString(); + } catch (CharacterCodingException e) { + throw new AssertionError(e); + } + } + + public static String truncateUtf8(String s, int maxUtf8Length) { + byte[] utf8 = toUtf8(s); + if (utf8.length <= maxUtf8Length) return s; + return fromUtf8(utf8, 0, maxUtf8Length); + } + + /** + * Converts the given byte array to a hex character array. + */ + private static char[] toHexChars(byte[] bytes) { + char[] hex = new char[bytes.length * 2]; + for (int i = 0, j = 0; i < bytes.length; i++) { + hex[j++] = HEX[(bytes[i] >> 4) & 0xF]; + hex[j++] = HEX[bytes[i] & 0xF]; + } + return hex; + } + + /** + * Converts the given byte array to a hex string. + */ + public static String toHexString(byte[] bytes) { + return new String(toHexChars(bytes)); + } + + /** + * Converts the given hex string to a byte array. + */ + public static byte[] fromHexString(String hex) { + int len = hex.length(); + if (len % 2 != 0) + throw new IllegalArgumentException("Not a hex string"); + byte[] bytes = new byte[len / 2]; + for (int i = 0, j = 0; i < len; i += 2, j++) { + int high = hexDigitToInt(hex.charAt(i)); + int low = hexDigitToInt(hex.charAt(i + 1)); + bytes[j] = (byte) ((high << 4) + low); + } + return bytes; + } + + private static int hexDigitToInt(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + throw new IllegalArgumentException("Not a hex digit: " + c); + } + + public static String trim(String s) { + return s.trim(); + } + + /** + * Returns true if the string is longer than maxLength + */ + public static boolean utf8IsTooLong(String s, int maxLength) { + return toUtf8(s).length > maxLength; + } + + public static boolean isValidMac(String mac) { + return MAC.matcher(mac).matches(); + } + + public static byte[] macToBytes(String mac) { + if (!MAC.matcher(mac).matches()) throw new IllegalArgumentException(); + return fromHexString(mac.replaceAll(":", "")); + } + + public static String macToString(byte[] mac) { + if (mac.length != 6) throw new IllegalArgumentException(); + StringBuilder s = new StringBuilder(); + for (byte b : mac) { + if (s.length() > 0) s.append(':'); + s.append(HEX[(b >> 4) & 0xF]); + s.append(HEX[b & 0xF]); + } + return s.toString(); + } + + public static String getRandomString(int length) { + char[] c = new char[length]; + for (int i = 0; i < length; i++) + c[i] = (char) ('a' + random.nextInt(26)); + return new String(c); + } + + public static String getRandomBase32String(int length) { + char[] c = new char[length]; + for (int i = 0; i < length; i++) { + int character = random.nextInt(32); + if (character < 26) c[i] = (char) ('a' + character); + else c[i] = (char) ('2' + (character - 26)); + } + return new String(c); + } +} diff --git a/mailbox-core/src/main/resources/bridges b/mailbox-core/src/main/resources/bridges new file mode 100644 index 0000000000000000000000000000000000000000..73188eba1dddfb6784faca58bccd6f2c7c1973dc --- /dev/null +++ b/mailbox-core/src/main/resources/bridges @@ -0,0 +1,10 @@ +Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0 +Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0 +Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0 +Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0 +Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0 +Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0 +Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0 +Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0 +Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0 +Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com \ No newline at end of file diff --git a/mailbox-core/src/main/resources/torrc b/mailbox-core/src/main/resources/torrc new file mode 100644 index 0000000000000000000000000000000000000000..3d27a7f20593c2902a0620cdc0fe5f3906781ab9 --- /dev/null +++ b/mailbox-core/src/main/resources/torrc @@ -0,0 +1,6 @@ +ControlPort 59051 +CookieAuthentication 1 +DisableNetwork 1 +RunAsDaemon 1 +SafeSocks 1 +SocksPort 59050