diff --git a/bramble-android/.gitignore b/bramble-android/.gitignore index 6653bfd64..7b257d328 100644 --- a/bramble-android/.gitignore +++ b/bramble-android/.gitignore @@ -3,3 +3,4 @@ gen build .settings src/main/res/raw/*.zip +src/main/jniLibs \ No newline at end of file diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index 81d73b7fe..d2cce2d99 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -5,14 +5,14 @@ apply plugin: 'witness' apply from: 'witness.gradle' android { - compileSdkVersion 29 - buildToolsVersion '29.0.2' + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion 16 - targetSdkVersion 28 - versionCode 10210 - versionName "1.2.10" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName rootProject.ext.versionName consumerProguardFiles 'proguard-rules.txt' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -53,10 +53,12 @@ dependencies { } def torBinariesDir = 'src/main/res/raw' +def torLibsDir = 'src/main/jniLibs' task cleanTorBinaries { doLast { delete fileTree(torBinariesDir) { include '*.zip' } + delete fileTree(torLibsDir) { include '**/*.so' } } } @@ -67,8 +69,36 @@ task unpackTorBinaries { copy { from configurations.tor.collect { zipTree(it) } into torBinariesDir - // TODO: Remove after next Tor upgrade, which won't include non-PIE binaries - include 'geoip.zip', '*_pie.zip' + 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 @@ -76,5 +106,6 @@ task unpackTorBinaries { tasks.withType(MergeResources) { inputs.dir torBinariesDir + inputs.dir torLibsDir dependsOn unpackTorBinaries } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java index 7428fac0c..a4b2a1d8f 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java @@ -16,19 +16,42 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager; 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.util.AndroidUtils; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Executor; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import javax.net.SocketFactory; +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; + @MethodsNotNullByDefault @ParametersNotNullByDefault class AndroidTorPlugin extends TorPlugin { + private static final List 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 Application app; private final AndroidWakeLock wakeLock; + private final File torLib, obfs4Lib; AndroidTorPlugin(Executor ioExecutor, Executor wakefulIoExecutor, @@ -55,6 +78,9 @@ class AndroidTorPlugin extends TorPlugin { maxIdleTime, torDirectory); this.app = app; wakeLock = wakeLockManager.createWakeLock("TorPlugin"); + String nativeLibDir = app.getApplicationInfo().nativeLibraryDir; + torLib = new File(nativeLibDir, TOR_LIB_NAME); + obfs4Lib = new File(nativeLibDir, OBFS4_LIB_NAME); } @Override @@ -85,4 +111,112 @@ class AndroidTorPlugin extends TorPlugin { super.stop(); 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(app.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 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 findApkFiles(File root) { + List files = new ArrayList<>(); + findApkFiles(root, files); + return files; + } + + private void findApkFiles(File f, List 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 getSupportedLibraryPaths(String libName) { + List architectures = new ArrayList<>(); + for (String abi : AndroidUtils.getSupportedArchitectures()) { + if (LIBRARY_ARCHITECTURES.contains(abi)) { + architectures.add("lib/" + abi + "/" + libName); + } + } + return architectures; + } } 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 24816197b..5fc93f885 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 @@ -132,7 +132,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private final CircumventionProvider circumventionProvider; private final ResourceProvider resourceProvider; private final int maxLatency, maxIdleTime, socketTimeout; - private final File torDirectory, torFile, geoIpFile, obfs4File, configFile; + private final File torDirectory, geoIpFile, configFile; private final File doneFile, cookieFile; private final AtomicBoolean used = new AtomicBoolean(false); @@ -181,9 +181,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { socketTimeout = Integer.MAX_VALUE; else socketTimeout = maxIdleTime * 2; this.torDirectory = torDirectory; - torFile = new File(torDirectory, "tor"); geoIpFile = new File(torDirectory, "geoip"); - obfs4File = new File(torDirectory, "obfs4proxy"); configFile = new File(torDirectory, "torrc"); doneFile = new File(torDirectory, "done"); cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); @@ -192,6 +190,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { new PoliteExecutor("TorPlugin", ioExecutor, 1); } + protected File getTorExecutableFile() { + return new File(torDirectory, "tor"); + } + + protected File getObfs4ExecutableFile() { + return new File(torDirectory, "obfs4proxy"); + } + @Override public TransportId getId() { return TorConstants.ID; @@ -224,6 +230,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { 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()); @@ -322,44 +329,43 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private void installAssets() throws PluginException { - InputStream in = null; - OutputStream out = null; try { // The done file may already exist from a previous installation //noinspection ResultOfMethodCallIgnored doneFile.delete(); - // Unzip the Tor binary to the filesystem - in = getTorInputStream(); - out = new FileOutputStream(torFile); - copyAndClose(in, out); - // Make the Tor binary executable - if (!torFile.setExecutable(true, true)) throw new IOException(); - // Unzip the GeoIP database to the filesystem - in = getGeoIpInputStream(); - out = new FileOutputStream(geoIpFile); - copyAndClose(in, out); - // Unzip the Obfs4 proxy to the filesystem - in = getObfs4InputStream(); - out = new FileOutputStream(obfs4File); - copyAndClose(in, out); - // Make the Obfs4 proxy executable - if (!obfs4File.setExecutable(true, true)) throw new IOException(); - // Copy the config file to the filesystem - in = getConfigInputStream(); - out = new FileOutputStream(configFile); - copyAndClose(in, out); + installTorExecutable(); + installObfs4Executable(); + extract(getGeoIpInputStream(), geoIpFile); + extract(getConfigInputStream(), configFile); if (!doneFile.createNewFile()) LOG.warning("Failed to create done file"); } catch (IOException e) { - tryToClose(in, LOG, WARNING); - tryToClose(out, LOG, WARNING); throw new PluginException(e); } } - private InputStream getTorInputStream() throws IOException { + 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); @@ -376,8 +382,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private InputStream getObfs4InputStream() throws IOException { - if (LOG.isLoggable(INFO)) - LOG.info("Installing obfs4proxy binary for " + architecture); InputStream in = resourceProvider .getResourceInputStream("obfs4proxy_" + architecture, ".zip"); ZipInputStream zin = new ZipInputStream(in); @@ -569,6 +573,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { if (enable) { Collection conf = new ArrayList<>(); conf.add("UseBridges 1"); + File obfs4File = getObfs4ExecutableFile(); if (needsMeek) { conf.add("ClientTransportPlugin meek_lite exec " + obfs4File.getAbsolutePath()); diff --git a/briar-android/build.gradle b/briar-android/build.gradle index e68700a55..8e07ab8bb 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -16,14 +16,14 @@ def getStdout = { command, defaultValue -> } android { - compileSdkVersion 29 - buildToolsVersion '29.0.2' + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion 16 - targetSdkVersion 28 - versionCode 10210 - versionName "1.2.10" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName rootProject.ext.versionName applicationId "org.briarproject.briar.android" buildConfigField "String", "GitHash", "\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\"" diff --git a/build.gradle b/build.gradle index 157f13f45..9f48c87fb 100644 --- a/build.gradle +++ b/build.gradle @@ -33,3 +33,12 @@ buildscript { classpath files('libs/gradle-witness.jar') } } + +project.ext { + buildToolsVersion = '30.0.2' + compileSdkVersion = 30 + minSdkVersion = 16 + targetSdkVersion = 29 + versionCode = 10210 + versionName = '1.2.10' +} \ No newline at end of file