diff --git a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java index a2f555b18beba8d65160339efb9db45f07ff5727..1aeac072da1b731226eda111eadd0f70bf8b71c5 100644 --- a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java +++ b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java @@ -13,6 +13,7 @@ import org.briarproject.api.plugins.duplex.DuplexPluginConfig; import org.briarproject.api.plugins.duplex.DuplexPluginFactory; import org.briarproject.api.plugins.simplex.SimplexPluginConfig; import org.briarproject.api.plugins.simplex.SimplexPluginFactory; +import org.briarproject.api.system.LocationUtils; import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory; import org.briarproject.plugins.tcp.LanTcpPluginFactory; import org.briarproject.plugins.tor.TorPluginFactory; @@ -39,12 +40,13 @@ public class AndroidPluginsModule extends AbstractModule { DuplexPluginConfig getDuplexPluginConfig( @PluginExecutor Executor pluginExecutor, AndroidExecutor androidExecutor, Context appContext, - CryptoComponent crypto, ShutdownManager shutdownManager) { + CryptoComponent crypto, LocationUtils locationUtils, + ShutdownManager shutdownManager) { DuplexPluginFactory droidtooth = new DroidtoothPluginFactory( pluginExecutor, androidExecutor, appContext, crypto.getSecureRandom()); DuplexPluginFactory tor = new TorPluginFactory(pluginExecutor, - appContext, shutdownManager); + appContext, locationUtils, shutdownManager); DuplexPluginFactory lan = new LanTcpPluginFactory(pluginExecutor); final Collection<DuplexPluginFactory> factories = Arrays.asList(droidtooth, tor, lan); diff --git a/briar-android/src/org/briarproject/plugins/tor/TorNetworkMetadata.java b/briar-android/src/org/briarproject/plugins/tor/TorNetworkMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..a8d75b616d7c323aadd9fd658b3c71bb381a95eb --- /dev/null +++ b/briar-android/src/org/briarproject/plugins/tor/TorNetworkMetadata.java @@ -0,0 +1,39 @@ +package org.briarproject.plugins.tor; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +import org.briarproject.api.system.LocationUtils; + +public class TorNetworkMetadata { + + private static final Logger LOG = + Logger.getLogger(TorNetworkMetadata.class.getName()); + + // for country codes see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + // below list from https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki + // TODO: get a more complete list + public static final Set<String> BLOCKED_IN_COUNTRIES = new HashSet<String>(Arrays.asList( + "CN", + "IR", + "SY", + //"ET", // possibly lifted - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=et&events=off#userstats-relay-country + //"KZ", // unclear due to botnet - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=kz&events=off#userstats-relay-country + //"PH", // unclear due to botnet - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=ph&events=off#userstats-relay-country + //"AE", // unclear due to botnet - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=ae&events=off#userstats-relay-country + //"GB", // for testing + "ZZ" + )); + + public static boolean isTorProbablyBlocked(LocationUtils locationUtils) { + String countryCode = locationUtils.getCurrentCountry(); + if (BLOCKED_IN_COUNTRIES.contains(countryCode)) { + LOG.info("Tor is probably blocked in your country: " + countryCode); + return true; + } + return false; + } + +} diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java index 6112889ffb7a654ff8743d64fca94047009387a7..99e92959233126a39be059c905bbb82eb4edbbfb 100644 --- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java +++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java @@ -44,6 +44,7 @@ import org.briarproject.util.StringUtils; import socks.Socks5Proxy; import socks.SocksSocket; +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -275,6 +276,7 @@ class TorPlugin implements DuplexPlugin, EventHandler { out.close(); } + @SuppressLint("NewApi") private boolean setExecutable(File f) { if(Build.VERSION.SDK_INT >= 9) { return f.setExecutable(true, true); diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java b/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java index 91720ff9f587270275d9997ac26f6f58edda6a70..e5ba67129a3b66338e26cf47d0b915bef9c47791 100644 --- a/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java +++ b/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java @@ -1,30 +1,39 @@ package org.briarproject.plugins.tor; import java.util.concurrent.Executor; +import java.util.logging.Logger; import org.briarproject.api.TransportId; import org.briarproject.api.lifecycle.ShutdownManager; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexPluginFactory; +import org.briarproject.api.system.LocationUtils; +import org.briarproject.plugins.AndroidPluginsModule; +import org.briarproject.plugins.tor.TorNetworkMetadata; import android.content.Context; import android.os.Build; public class TorPluginFactory implements DuplexPluginFactory { + private static final Logger LOG = + Logger.getLogger(TorPluginFactory.class.getName()); + private static final int MAX_FRAME_LENGTH = 1024; private static final long MAX_LATENCY = 60 * 1000; // 1 minute private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes private final Executor pluginExecutor; private final Context appContext; + private final LocationUtils locationUtils; private final ShutdownManager shutdownManager; public TorPluginFactory(Executor pluginExecutor, Context appContext, - ShutdownManager shutdownManager) { + LocationUtils locationUtils, ShutdownManager shutdownManager) { this.pluginExecutor = pluginExecutor; this.appContext = appContext; + this.locationUtils = locationUtils; this.shutdownManager = shutdownManager; } @@ -35,6 +44,11 @@ public class TorPluginFactory implements DuplexPluginFactory { public DuplexPlugin createPlugin(DuplexPluginCallback callback) { // Check that we have a Tor binary for this architecture if(!Build.CPU_ABI.startsWith("armeabi")) return null; + // Check that we don't know that Tor is blocked here + if (TorNetworkMetadata.isTorProbablyBlocked(locationUtils)) { + LOG.info("Tor has been pre-emptively disabled since it is probably blocked"); + return null; + } return new TorPlugin(pluginExecutor,appContext, shutdownManager, callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL); } diff --git a/briar-android/src/org/briarproject/system/AndroidLocationUtils.java b/briar-android/src/org/briarproject/system/AndroidLocationUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..2f52682076164118e22478f53bdf93ae13a459b0 --- /dev/null +++ b/briar-android/src/org/briarproject/system/AndroidLocationUtils.java @@ -0,0 +1,126 @@ +package org.briarproject.system; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; + +import org.briarproject.api.system.LocationUtils; + +import roboguice.inject.ContextSingleton; +import android.annotation.SuppressLint; +import android.content.Context; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import com.google.inject.Inject; + +@ContextSingleton +class AndroidLocationUtils implements LocationUtils { + + private static final Logger LOG = + Logger.getLogger(AndroidLocationUtils.class.getName()); + + final Context context; + + @Inject + public AndroidLocationUtils(Context context) { + this.context = context; + } + + /** + * This guesses the current country from the first of these sources that + * succeeds (also in order of likelihood of being correct): + * + * <ul> + * <li>Phone network. This works even when no SIM card is inserted, or a + * foreign SIM card is inserted.</li> + * <li><del>Location service (GPS/WiFi/etc).</del> <em>This is disabled for + * now, until we figure out an offline method of converting a long/lat + * into a country code, that doesn't involve a network call.</em> + * <li>SIM card. This is only an heuristic and assumes the user is not + * roaming.</li> + * <li>User Locale. This is an even worse heuristic.</li> + * </ul> + * + * Note: this is very similar to <a href="https://android.googlesource.com/platform/frameworks/base/+/cd92588%5E/location/java/android/location/CountryDetector.java"> + * this API</a> except it seems that Google doesn't want us to use it for + * some reason - both that class and {@code Context.COUNTRY_CODE} are + * annotated {@code @hide}. + */ + @SuppressLint("DefaultLocale") + @Override + public String getCurrentCountry() { + String countryCode; + countryCode = getCountryFromPhoneNetwork(); + if (!TextUtils.isEmpty(countryCode)) { + return countryCode.toUpperCase(); // android api gives lowercase for some reason + } + // When we enable this, we will need to add ACCESS_FINE_LOCATION + //countryCode = getCountryFromLocation(); + //if (!TextUtils.isEmpty(countryCode)) { + // return countryCode; + //} + countryCode = getCountryFromSimCard(); + if (!TextUtils.isEmpty(countryCode)) { + LOG.info("Could not determine current country; fall back to SIM card country."); + return countryCode.toUpperCase(); // android api gives lowercase for some reason + } + LOG.info("Could not determine current country; fall back to user-defined locale."); + return Locale.getDefault().getCountry(); + } + + String getCountryFromPhoneNetwork() { + TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); + return tm.getNetworkCountryIso(); + } + + String getCountryFromSimCard() { + TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); + return tm.getSimCountryIso(); + } + + // TODO: this is not currently used, because it involves a network call + // it should be possible to determine country just from the long/lat, but + // this would involve something like tzdata for countries. + String getCountryFromLocation() { + Location location = getLastKnownLocation(); + if (location == null) return null; + Geocoder code = new Geocoder(context); + try { + List<Address> addresses = code.getFromLocation(location.getLatitude(), location.getLongitude(), 1); + if (addresses.isEmpty()) return null; + return addresses.get(0).getCountryCode(); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the last location from all location providers. + * Since we're only checking the country, we don't care about the accuracy. + * If we ever need the accuracy, we can do something like: + * https://code.google.com/p/android-protips-location/source/browse/trunk\ + * /src/com/radioactiveyak/location_best_practices/utils/GingerbreadLastLocationFinder.java + */ + Location getLastKnownLocation() { + LocationManager locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE); + Location bestResult = null; + long bestTime = Long.MIN_VALUE; + for (String provider: locationManager.getAllProviders()) { + Location location = locationManager.getLastKnownLocation(provider); + if (location == null) continue; + long time = location.getTime(); + if (time > bestTime) { + bestResult = location; + bestTime = time; + } + } + return bestResult; + } + +} diff --git a/briar-android/src/org/briarproject/system/AndroidSystemModule.java b/briar-android/src/org/briarproject/system/AndroidSystemModule.java index 570d4fbbc79b0fba9f7ceff17c741de14f70b0b0..aa6769e59bf8bcf53723af9494985d1f38e36c6c 100644 --- a/briar-android/src/org/briarproject/system/AndroidSystemModule.java +++ b/briar-android/src/org/briarproject/system/AndroidSystemModule.java @@ -2,6 +2,7 @@ package org.briarproject.system; import org.briarproject.api.system.Clock; import org.briarproject.api.system.FileUtils; +import org.briarproject.api.system.LocationUtils; import org.briarproject.api.system.SeedProvider; import org.briarproject.api.system.Timer; @@ -14,5 +15,6 @@ public class AndroidSystemModule extends AbstractModule { bind(Timer.class).to(SystemTimer.class); bind(SeedProvider.class).to(AndroidSeedProvider.class); bind(FileUtils.class).to(AndroidFileUtils.class); + bind(LocationUtils.class).to(AndroidLocationUtils.class); } } diff --git a/briar-api/src/org/briarproject/api/system/LocationUtils.java b/briar-api/src/org/briarproject/api/system/LocationUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c0c82844db7e3ae67b9e1c8979b55a73c076c20f --- /dev/null +++ b/briar-api/src/org/briarproject/api/system/LocationUtils.java @@ -0,0 +1,13 @@ +package org.briarproject.api.system; + +public interface LocationUtils { + + /** Get the country the device is currently-located in, or "" if it cannot + * be determined. Should never return {@code null}. + * + * <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(); + +}