diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
index b6e6a819f..d1e13e24f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
@@ -91,6 +91,17 @@ public final class Constants {
public static final String FILE_USE_COMPRESSION = "useFileCompression";
public static final String TEXT_USE_COMPRESSION = "useTextCompression";
public static final String USE_ARMOR = "useArmor";
+ // proxy settings
+ public static final String USE_NORMAL_PROXY = "useNormalProxy";
+ public static final String USE_TOR_PROXY = "useTorProxy";
+ public static final String PROXY_HOST = "proxyHost";
+ public static final String PROXY_PORT = "proxyPort";
+ }
+
+ public static final class ProxyOrbot {
+ public static final String PROXY_HOST = "127.0.0.1";
+ public static final int PROXY_HTTP_PORT = 8118;
+ public static final int PROXY_SOCKS_PORT = 9050;
}
public static final class Defaults {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
index 442bdf8f7..00d2d7a1b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
@@ -23,6 +23,7 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
@@ -35,8 +36,11 @@ import android.widget.LinearLayout;
import org.spongycastle.bcpg.CompressionAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference;
+import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences;
+import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
import java.util.List;
@@ -270,6 +274,145 @@ public class SettingsActivity extends PreferenceActivity {
}
}
+ public static class ProxyPrefsFragment extends PreferenceFragment {
+ private CheckBoxPreference mUseTor;
+ private CheckBoxPreference mUseNormalProxy;
+ private EditTextPreference mProxyHost;
+ private EditTextPreference mProxyPort;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.proxy_prefs);
+
+ mUseTor = (CheckBoxPreference) findPreference(Constants.Pref.USE_TOR_PROXY);
+ mUseNormalProxy = (CheckBoxPreference) findPreference(Constants.Pref.USE_NORMAL_PROXY);
+ mProxyHost = (EditTextPreference) findPreference(Constants.Pref.PROXY_HOST);
+ mProxyPort = (EditTextPreference) findPreference(Constants.Pref.PROXY_PORT);
+
+ initializeUseTorPref();
+ initializeUseNormalProxyPref();
+ initialiseEditTextPreferences();
+
+ if (mUseTor.isChecked()) disableNormalProxyPrefs();
+ else if (mUseNormalProxy.isChecked()) disableUseTorPrefs();
+ }
+
+ private void initializeUseTorPref() {
+ mUseTor.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if ((Boolean)newValue) {
+ OrbotHelper orbotHelper = new OrbotHelper(ProxyPrefsFragment.this.getActivity());
+ boolean installed = orbotHelper.isOrbotInstalled();
+ if (!installed) {
+ Log.d(Constants.TAG, "Prompting to install Tor");
+ orbotHelper.promptToInstall(ProxyPrefsFragment.this.getActivity());
+ // don't let the user check the box until he's installed orbot
+ return false;
+ } else {
+ disableNormalProxyPrefs();
+ // let the enable tor box be checked
+ return true;
+ }
+ }
+ else {
+ // we're unchecking Tor, so enable other proxy
+ enableNormalProxyPrefs();
+ return true;
+ }
+ }
+ });
+ }
+
+ private void initializeUseNormalProxyPref() {
+ mUseNormalProxy.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if ((Boolean) newValue) {
+ disableUseTorPrefs();
+ } else {
+ enableUseTorPrefs();
+ }
+ return true;
+ }
+ });
+ }
+
+ private void initialiseEditTextPreferences() {
+ mProxyHost.setSummary(mProxyHost.getText());
+ mProxyPort.setSummary(mProxyPort.getText());
+
+ mProxyHost.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue.equals("")) {
+ Notify.create(
+ ProxyPrefsFragment.this.getActivity(),
+ R.string.pref_proxy_host_err_invalid,
+ Notify.Style.ERROR
+ ).show();
+ return false;
+ } else {
+ mProxyHost.setSummary((CharSequence) newValue);
+ return true;
+ }
+ }
+ });
+
+ mProxyPort.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ try {
+ int port = Integer.parseInt((String) newValue);
+ if(port < 0 || port > 65535) {
+ Notify.create(
+ ProxyPrefsFragment.this.getActivity(),
+ R.string.pref_proxy_port_err_invalid,
+ Notify.Style.ERROR
+ ).show();
+ return false;
+ }
+ // no issues, save port
+ mProxyPort.setSummary("" + port);
+ return true;
+ } catch (NumberFormatException e) {
+ Notify.create(
+ ProxyPrefsFragment.this.getActivity(),
+ R.string.pref_proxy_port_err_invalid,
+ Notify.Style.ERROR
+ ).show();
+ return false;
+ }
+ }
+ });
+ }
+
+ private void disableNormalProxyPrefs() {
+ mUseNormalProxy.setChecked(false);
+ mUseNormalProxy.setEnabled(false);
+ mProxyHost.setEnabled(false);
+ mProxyPort.setEnabled(false);
+ }
+
+ private void enableNormalProxyPrefs() {
+ mUseNormalProxy.setEnabled(true);
+ mProxyHost.setEnabled(true);
+ mProxyPort.setEnabled(true);
+ }
+
+ private void disableUseTorPrefs() {
+ mUseTor.setChecked(false);
+ mUseTor.setEnabled(false);
+ }
+
+ private void enableUseTorPrefs() {
+ mUseTor.setEnabled(true);
+ }
+ }
+
@TargetApi(Build.VERSION_CODES.KITKAT)
protected boolean isValidFragment(String fragmentName) {
return AdvancedPrefsFragment.class.getName().equals(fragmentName)
@@ -332,7 +475,8 @@ public class SettingsActivity extends PreferenceActivity {
String[] servers = sPreferences.getKeyServers();
String serverSummary = context.getResources().getQuantityString(
R.plurals.n_keyservers, servers.length, servers.length);
- return serverSummary + "; " + context.getString(R.string.label_preferred) + ": " + sPreferences.getPreferredKeyserver();
+ return serverSummary + "; " + context.getString(R.string.label_preferred) + ": " + sPreferences
+ .getPreferredKeyserver();
}
private static void initializeUseDefaultYubiKeyPin(final CheckBoxPreference mUseDefaultYubiKeyPin) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
index f4c6f7f94..1363092f4 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
@@ -224,6 +224,53 @@ public class Preferences {
return mSharedPreferences.getBoolean(Pref.ENCRYPT_FILENAMES, true);
}
+ public boolean getUseNormalProxy() {
+ return mSharedPreferences.getBoolean(Constants.Pref.USE_NORMAL_PROXY, false);
+ }
+
+ public void setUseNormalProxy(boolean use) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.Pref.USE_NORMAL_PROXY, use);
+ editor.commit();
+ }
+
+ public boolean getUseTorProxy() {
+ return mSharedPreferences.getBoolean(Constants.Pref.USE_TOR_PROXY, false);
+ }
+
+ public void setUseTorProxy(boolean use) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.Pref.USE_TOR_PROXY, use);
+ editor.commit();
+ }
+
+ public String getProxyHost() {
+ return mSharedPreferences.getString(Constants.Pref.PROXY_HOST, null);
+ }
+
+ public void setProxyHost(String host) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putString(Constants.Pref.PROXY_HOST, host);
+ editor.commit();
+ }
+
+ /**
+ * we store port as String for easy interfacing with EditTextPreference, but return it as an integer
+ * @return port number of proxy
+ */
+ public int getProxyPort() {
+ return Integer.parseInt(mSharedPreferences.getString(Pref.PROXY_PORT, "-1"));
+ }
+ /**
+ * we store port as String for easy interfacing with EditTextPreference, but return it as an integer
+ * @param port proxy port
+ */
+ public void setProxyPort(String port) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putString(Pref.PROXY_PORT, port);
+ editor.commit();
+ }
+
public CloudSearchPrefs getCloudSearchPrefs() {
return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true),
mSharedPreferences.getBoolean(Pref.SEARCH_KEYBASE, true),
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java
new file mode 100644
index 000000000..44a12e188
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java
@@ -0,0 +1,124 @@
+
+package org.sufficientlysecure.keychain.util.orbot;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import org.sufficientlysecure.keychain.R;
+
+
+public class OrbotHelper {
+
+ private final static int REQUEST_CODE_STATUS = 100;
+
+ public final static String ORBOT_PACKAGE_NAME = "org.torproject.android";
+ public final static String TOR_BIN_PATH = "/data/data/org.torproject.android/app_bin/tor";
+
+ public final static String ACTION_START_TOR = "org.torproject.android.START_TOR";
+ public final static String ACTION_REQUEST_HS = "org.torproject.android.REQUEST_HS_PORT";
+ public final static int HS_REQUEST_CODE = 9999;
+
+ private Context mContext = null;
+
+ public OrbotHelper(Context context)
+ {
+ mContext = context;
+ }
+
+ public boolean isOrbotRunning()
+ {
+ int procId = TorServiceUtils.findProcessId(TOR_BIN_PATH);
+
+ return (procId != -1);
+ }
+
+ public boolean isOrbotInstalled()
+ {
+ return isAppInstalled(ORBOT_PACKAGE_NAME);
+ }
+
+ private boolean isAppInstalled(String uri) {
+ PackageManager pm = mContext.getPackageManager();
+ boolean installed = false;
+ try {
+ pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
+ installed = true;
+ } catch (PackageManager.NameNotFoundException e) {
+ installed = false;
+ }
+ return installed;
+ }
+
+ public void promptToInstall(Activity activity)
+ {
+ String uriMarket = activity.getString(R.string.market_orbot);
+ // show dialog - install from market, f-droid or direct APK
+ showDownloadDialog(activity, activity.getString(R.string.install_orbot_),
+ activity.getString(R.string.you_must_have_orbot),
+ activity.getString(R.string.orbot_yes), activity.getString(R.string.orbot_no), uriMarket);
+ }
+
+ private static AlertDialog showDownloadDialog(final Activity activity,
+ CharSequence stringTitle, CharSequence stringMessage, CharSequence stringButtonYes,
+ CharSequence stringButtonNo, final String uriString) {
+ AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
+ downloadDialog.setTitle(stringTitle);
+ downloadDialog.setMessage(stringMessage);
+ downloadDialog.setPositiveButton(stringButtonYes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ Uri uri = Uri.parse(uriString);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ activity.startActivity(intent);
+ }
+ });
+ downloadDialog.setNegativeButton(stringButtonNo, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ }
+ });
+ return downloadDialog.show();
+ }
+
+ public void requestOrbotStart(final Activity activity)
+ {
+
+ AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
+ downloadDialog.setTitle(R.string.start_orbot_);
+ downloadDialog
+ .setMessage(R.string.orbot_doesn_t_appear_to_be_running_would_you_like_to_start_it_up_and_connect_to_tor_);
+ downloadDialog.setPositiveButton(R.string.orbot_yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ activity.startActivityForResult(getOrbotStartIntent(), 1);
+ }
+ });
+ downloadDialog.setNegativeButton(R.string.orbot_no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ }
+ });
+ downloadDialog.show();
+
+ }
+
+ public void requestHiddenServiceOnPort(Activity activity, int port)
+ {
+ Intent intent = new Intent(ACTION_REQUEST_HS);
+ intent.setPackage(ORBOT_PACKAGE_NAME);
+ intent.putExtra("hs_port", port);
+
+ activity.startActivityForResult(intent, HS_REQUEST_CODE);
+ }
+
+ public static Intent getOrbotStartIntent() {
+ Intent intent = new Intent(ACTION_START_TOR);
+ intent.setPackage(ORBOT_PACKAGE_NAME);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java
new file mode 100644
index 000000000..ea77582fc
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java
@@ -0,0 +1,234 @@
+/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */
+/* See LICENSE for licensing information */
+
+package org.sufficientlysecure.keychain.util.orbot;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.URLEncoder;
+import java.util.StringTokenizer;
+
+import android.util.Log;
+
+public class TorServiceUtils {
+
+ private final static String TAG = "TorUtils";
+ // various console cmds
+ public final static String SHELL_CMD_CHMOD = "chmod";
+ public final static String SHELL_CMD_KILL = "kill -9";
+ public final static String SHELL_CMD_RM = "rm";
+ public final static String SHELL_CMD_PS = "ps";
+ public final static String SHELL_CMD_PIDOF = "pidof";
+
+ public final static String CHMOD_EXE_VALUE = "700";
+
+ public static boolean isRootPossible()
+ {
+
+ StringBuilder log = new StringBuilder();
+
+ try {
+
+ // Check if Superuser.apk exists
+ File fileSU = new File("/system/app/Superuser.apk");
+ if (fileSU.exists())
+ return true;
+
+ fileSU = new File("/system/app/superuser.apk");
+ if (fileSU.exists())
+ return true;
+
+ fileSU = new File("/system/bin/su");
+ if (fileSU.exists())
+ {
+ String[] cmd = {
+ "su"
+ };
+ int exitCode = TorServiceUtils.doShellCommand(cmd, log, false, true);
+ if (exitCode != 0)
+ return false;
+ else
+ return true;
+ }
+
+ // Check for 'su' binary
+ String[] cmd = {
+ "which su"
+ };
+ int exitCode = TorServiceUtils.doShellCommand(cmd, log, false, true);
+
+ if (exitCode == 0) {
+ Log.d(TAG, "root exists, but not sure about permissions");
+ return true;
+
+ }
+
+ } catch (IOException e) {
+ // this means that there is no root to be had (normally) so we won't
+ // log anything
+ Log.e(TAG, "Error checking for root access", e);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error checking for root access", e);
+ // this means that there is no root to be had (normally)
+ }
+
+ Log.e(TAG, "Could not acquire root permissions");
+
+ return false;
+ }
+
+ public static int findProcessId(String command)
+ {
+ int procId = -1;
+
+ try
+ {
+ procId = findProcessIdWithPidOf(command);
+
+ if (procId == -1)
+ procId = findProcessIdWithPS(command);
+ } catch (Exception e)
+ {
+ try
+ {
+ procId = findProcessIdWithPS(command);
+ } catch (Exception e2)
+ {
+ Log.e(TAG, "Unable to get proc id for command: " + URLEncoder.encode(command), e2);
+ }
+ }
+
+ return procId;
+ }
+
+ // use 'pidof' command
+ public static int findProcessIdWithPidOf(String command) throws Exception
+ {
+
+ int procId = -1;
+
+ Runtime r = Runtime.getRuntime();
+
+ Process procPs = null;
+
+ String baseName = new File(command).getName();
+ // fix contributed my mikos on 2010.12.10
+ procPs = r.exec(new String[] {
+ SHELL_CMD_PIDOF, baseName
+ });
+ // procPs = r.exec(SHELL_CMD_PIDOF);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream()));
+ String line = null;
+
+ while ((line = reader.readLine()) != null)
+ {
+
+ try
+ {
+ // this line should just be the process id
+ procId = Integer.parseInt(line.trim());
+ break;
+ } catch (NumberFormatException e)
+ {
+ Log.e("TorServiceUtils", "unable to parse process pid: " + line, e);
+ }
+ }
+
+ return procId;
+
+ }
+
+ // use 'ps' command
+ public static int findProcessIdWithPS(String command) throws Exception
+ {
+
+ int procId = -1;
+
+ Runtime r = Runtime.getRuntime();
+
+ Process procPs = null;
+
+ procPs = r.exec(SHELL_CMD_PS);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream()));
+ String line = null;
+
+ while ((line = reader.readLine()) != null)
+ {
+ if (line.indexOf(' ' + command) != -1)
+ {
+
+ StringTokenizer st = new StringTokenizer(line, " ");
+ st.nextToken(); // proc owner
+
+ procId = Integer.parseInt(st.nextToken().trim());
+
+ break;
+ }
+ }
+
+ return procId;
+
+ }
+
+ public static int doShellCommand(String[] cmds, StringBuilder log, boolean runAsRoot,
+ boolean waitFor) throws Exception
+ {
+
+ Process proc = null;
+ int exitCode = -1;
+
+ if (runAsRoot)
+ proc = Runtime.getRuntime().exec("su");
+ else
+ proc = Runtime.getRuntime().exec("sh");
+
+ OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream());
+
+ for (int i = 0; i < cmds.length; i++)
+ {
+ // TorService.logMessage("executing shell cmd: " + cmds[i] +
+ // "; runAsRoot=" + runAsRoot + ";waitFor=" + waitFor);
+
+ out.write(cmds[i]);
+ out.write("\n");
+ }
+
+ out.flush();
+ out.write("exit\n");
+ out.flush();
+
+ if (waitFor)
+ {
+
+ final char buf[] = new char[10];
+
+ // Consume the "stdout"
+ InputStreamReader reader = new InputStreamReader(proc.getInputStream());
+ int read = 0;
+ while ((read = reader.read(buf)) != -1) {
+ if (log != null)
+ log.append(buf, 0, read);
+ }
+
+ // Consume the "stderr"
+ reader = new InputStreamReader(proc.getErrorStream());
+ read = 0;
+ while ((read = reader.read(buf)) != -1) {
+ if (log != null)
+ log.append(buf, 0, read);
+ }
+
+ exitCode = proc.waitFor();
+
+ }
+
+ return exitCode;
+
+ }
+}
diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml
index e8d1f1e70..8e9f38163 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -49,6 +49,7 @@
"Subkeys"
"Cloud search"
"Password Cache"
+ "Proxy Settings"
"Confirm"
"Actions"
"Key"
@@ -165,6 +166,16 @@
"keybase.io"
"Search keys on keybase.io"
+
+ "Don't use a proxy"
+ "Enable Tor"
+ "Requires Orbot to be installed"
+ "Enable other proxy"
+ "Proxy Host"
+ "Proxy host cannot be empty"
+ "Proxy Port"
+ "Invalid port number entered"
+
"<no name>"
"<none>"
@@ -180,6 +191,15 @@
"Secret Key:"
+
+ "Install Orbot?"
+ "market://search?q=pname:org.torproject.android"
+ "You must have Orbot installed and activated to proxy traffic through it. Would you like to install it from Google Play?"
+ "Yes"
+ "No"
+ "Start Orbot?"
+ "Orbot doesn\'t appear to be running. Would you like to start it up and connect to Tor?"
+
"None"
"15 secs"
diff --git a/OpenKeychain/src/main/res/xml/preference_headers.xml b/OpenKeychain/src/main/res/xml/preference_headers.xml
index e3447ff48..70e400567 100644
--- a/OpenKeychain/src/main/res/xml/preference_headers.xml
+++ b/OpenKeychain/src/main/res/xml/preference_headers.xml
@@ -5,4 +5,7 @@
+
diff --git a/OpenKeychain/src/main/res/xml/proxy_prefs.xml b/OpenKeychain/src/main/res/xml/proxy_prefs.xml
new file mode 100644
index 000000000..e77ac6d71
--- /dev/null
+++ b/OpenKeychain/src/main/res/xml/proxy_prefs.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+