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 @@ + + + + + + +