From 63d87ae0857021a9d5c0d1ee6e052449c8433aa6 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Thu, 7 Jan 2016 19:50:20 -0200
Subject: [PATCH] Support for Destructive Panic Actions

PanicKit does distinguish between two kinds of panic responses:

* default responses such as logging out which are non-destructive and
  do not require user interaction, so that the basics work without
  configuration
* destructive responses such as deleting user data. These require
  some sort of authentication to make sure they are not triggered
  by malicious apps

The second type of responses is implemented with this commit.

Authentication is done by comparing the package name
which is very weak. It requires the user to opt-in to
destructive responses and to configure from which app
to receive those (since there might be many different panic
trigger apps).

While possible to uninstall an app and install one with the same
package name afterwards, this always triggers notifications to
the user (if the attacker does not have root access).

Still that is no sufficient security for Briar's requirements,
so that TrustedIntents are used as well to make sure that the
app sending the destructive trigger is signed by a signing key
that we specified before. Currently, that is the one from the
GuardianProject and from IilabEngineering who does the Amnesty
International Panic App.

The responsibility of checking that the panic TRIGGER is
legitimate lies with the app responding to the trigger, so Briar
in this case. This commit checks whether the TRIGGER comes from
a trusted app before performing destructive actions,
but does perform the default action even when triggered from
untrusted apps.

Closes #210
---
 briar-android/AndroidManifest.xml             |   5 +
 briar-android/build.gradle                    |   2 +
 briar-android/res/xml/panic_preferences.xml   |  18 ++
 .../panic/PanicPreferencesFragment.java       | 228 +++++++++++++++++-
 .../android/panic/PanicResponderActivity.java |  84 ++++++-
 5 files changed, 330 insertions(+), 7 deletions(-)

diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index ac262a3c20..e5e660ddd4 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -194,6 +194,11 @@
 		<activity
 			android:name=".android.panic.PanicPreferencesActivity"
 			android:label="@string/panic_setting" >
+			<intent-filter>
+				<action android:name="info.guardianproject.panic.action.CONNECT" />
+				<action android:name="info.guardianproject.panic.action.DISCONNECT" />
+				<category android:name="android.intent.category.DEFAULT" />
+			</intent-filter>
 		</activity>
 		<activity
 			android:name=".android.panic.PanicResponderActivity"
diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index 4c3835102e..b52c0c5217 100644
--- a/briar-android/build.gradle
+++ b/briar-android/build.gradle
@@ -17,6 +17,7 @@ dependencies {
     compile "com.android.support:preference-v14:23.1.1"
     compile "com.android.support:design:23.1.1"
     compile "info.guardianproject.panic:panic:0.5"
+    compile "info.guardianproject.trustedintents:trustedintents:0.2"
 }
 
 dependencyVerification {
@@ -29,6 +30,7 @@ dependencyVerification {
             'com.android.support:support-annotations:f347a35b9748a4103b39a6714a77e2100f488d623fd6268e259c177b200e9d82',
             'com.android.support:recyclerview-v7:7606373da0931a1e62588335465a0e390cd676c98117edab29220317495faefd',
             'info.guardianproject.panic:panic:a7ed9439826db2e9901649892cf9afbe76f00991b768d8f4c26332d7c9406cb2',
+            'info.guardianproject.trustedintents:trustedintents:6221456d8821a8d974c2acf86306900237cf6afaaa94a4c9c44e161350f80f3e',
     ]
 }
 
diff --git a/briar-android/res/xml/panic_preferences.xml b/briar-android/res/xml/panic_preferences.xml
index 3b40f0e3df..af9bbeca50 100644
--- a/briar-android/res/xml/panic_preferences.xml
+++ b/briar-android/res/xml/panic_preferences.xml
@@ -8,4 +8,22 @@
 		android:summary="@string/lock_setting_summary"
 		android:defaultValue="true"/>
 
+	<PreferenceCategory
+		android:title="Destructive Actions">
+
+		<ListPreference
+			android:key="pref_key_panic_app"
+			android:title="@string/panic_app_setting_title"
+			android:summary="@string/panic_app_setting_summary"
+			android:icon="@android:drawable/ic_menu_close_clear_cancel"/>
+
+		<CheckBoxPreference
+			android:key="pref_key_purge"
+			android:title="@string/purge_setting_title"
+			android:summary="@string/purge_setting_summary"
+			android:enabled="false"
+			android:defaultValue="false"/>
+
+	</PreferenceCategory>
+
 </PreferenceScreen>
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/panic/PanicPreferencesFragment.java b/briar-android/src/org/briarproject/android/panic/PanicPreferencesFragment.java
index 7fd096213c..3ee8915c25 100644
--- a/briar-android/src/org/briarproject/android/panic/PanicPreferencesFragment.java
+++ b/briar-android/src/org/briarproject/android/panic/PanicPreferencesFragment.java
@@ -1,14 +1,240 @@
 package org.briarproject.android.panic;
 
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
 import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.preference.CheckBoxPreference;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceFragmentCompat;
+import android.text.TextUtils;
 
 import org.briarproject.R;
 
-public class PanicPreferencesFragment extends PreferenceFragmentCompat {
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import info.guardianproject.panic.Panic;
+import info.guardianproject.panic.PanicResponder;
+
+public class PanicPreferencesFragment extends PreferenceFragmentCompat
+		implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(PanicPreferencesFragment.class.getName());
+
+	private PackageManager pm;
+	private CheckBoxPreference lockPref;
+	private ListPreference panicAppPref;
+	private CheckBoxPreference purgePref;
 
 	@Override
 	public void onCreatePreferences(Bundle bundle, String s) {
 		addPreferencesFromResource(R.xml.panic_preferences);
+
+		pm = getActivity().getPackageManager();
+
+		lockPref = (CheckBoxPreference) findPreference("pref_key_lock");
+		panicAppPref = (ListPreference) findPreference("pref_key_panic_app");
+		purgePref = (CheckBoxPreference) findPreference("pref_key_purge");
+
+		// check for connect/disconnect intents from panic trigger apps
+		if (PanicResponder.checkForDisconnectIntent(getActivity())) {
+			LOG.info("Received DISCONNECT intent from Panic Trigger App.");
+			// the necessary action should have been performed by the check
+			getActivity().finish();
+		} else {
+			// check if we got a connect intent from a not yet connected app
+			String packageName =
+					PanicResponder.getConnectIntentSender(getActivity());
+			if (!TextUtils.isEmpty((packageName)) &&
+					!TextUtils.equals(packageName,
+							PanicResponder
+									.getTriggerPackageName(getActivity()))) {
+
+				// A new panic trigger app asks us to connect
+				LOG.info("Received CONNECT intent from new Panic Trigger App.");
+
+				// Show dialog allowing the user to opt-in
+				showOptInDialog();
+			}
+		}
+
+		ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+		ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+		entries.add(0, getString(R.string.panic_app_setting_none));
+		entryValues.add(0, Panic.PACKAGE_NAME_NONE);
+
+		for (ResolveInfo resolveInfo : PanicResponder.resolveTriggerApps(pm)) {
+			if (resolveInfo.activityInfo == null)
+				continue;
+			entries.add(resolveInfo.activityInfo.loadLabel(pm));
+			entryValues.add(resolveInfo.activityInfo.packageName);
+		}
+
+		panicAppPref.setEntries(
+				entries.toArray(new CharSequence[entries.size()]));
+		panicAppPref.setEntryValues(
+				entryValues.toArray(new CharSequence[entryValues.size()]));
+		panicAppPref.setDefaultValue(Panic.PACKAGE_NAME_NONE);
+
+		panicAppPref.setOnPreferenceChangeListener(
+				new Preference.OnPreferenceChangeListener() {
+					@Override
+					public boolean onPreferenceChange(Preference preference,
+							Object newValue) {
+						String packageName = (String) newValue;
+						PanicResponder.setTriggerPackageName(getActivity(),
+								packageName);
+						showPanicApp(packageName);
+
+						if (packageName.equals(Panic.PACKAGE_NAME_NONE)) {
+							purgePref.setChecked(false);
+							purgePref.setEnabled(false);
+							getActivity().setResult(Activity.RESULT_CANCELED);
+						} else {
+							purgePref.setEnabled(true);
+						}
+
+						return true;
+					}
+				});
+
+		if (entries.size() <= 1) {
+			panicAppPref.setOnPreferenceClickListener(
+					new Preference.OnPreferenceClickListener() {
+						@Override
+						public boolean onPreferenceClick(
+								Preference preference) {
+							Intent intent = new Intent(Intent.ACTION_VIEW);
+							intent.setData(Uri.parse(
+									"market://details?id=info.guardianproject.ripple"));
+							getActivity().startActivity(intent);
+							return true;
+						}
+					});
+		}
 	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		getPreferenceScreen().getSharedPreferences()
+				.registerOnSharedPreferenceChangeListener(this);
+		showPanicApp(PanicResponder.getTriggerPackageName(getActivity()));
+	}
+
+	@Override
+	public void onPause() {
+		super.onPause();
+		getPreferenceScreen().getSharedPreferences()
+				.unregisterOnSharedPreferenceChangeListener(this);
+	}
+
+	@Override
+	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+			String key) {
+		// enable locking if purging gets enabled
+		if (key.equals("pref_key_purge")
+				&& sharedPreferences.getBoolean("pref_key_purge", false)) {
+			lockPref.setChecked(true);
+		}
+		// disable purging if locking gets disabled
+		if (key.equals("pref_key_lock")
+				&& !sharedPreferences.getBoolean("pref_key_lock",  true)
+				&&  sharedPreferences.getBoolean("pref_key_purge", false)) {
+			purgePref.setChecked(false);
+		}
+	}
+
+	private void showPanicApp(String triggerPackageName) {
+		if (TextUtils.isEmpty(triggerPackageName)
+				|| triggerPackageName.equals(Panic.PACKAGE_NAME_NONE)) {
+			// no panic app set
+			panicAppPref.setValue(Panic.PACKAGE_NAME_NONE);
+			panicAppPref
+					.setSummary(getString(R.string.panic_app_setting_summary));
+			panicAppPref.setIcon(
+					android.R.drawable.ic_menu_close_clear_cancel);
+			purgePref.setEnabled(false);
+		} else {
+			// display connected panic app
+			try {
+				panicAppPref.setValue(triggerPackageName);
+				panicAppPref.setSummary(pm.getApplicationLabel(
+						pm.getApplicationInfo(triggerPackageName, 0)));
+				panicAppPref.setIcon(
+						pm.getApplicationIcon(triggerPackageName));
+				purgePref.setEnabled(true);
+			} catch (PackageManager.NameNotFoundException e) {
+				// revert back to no app, just to be safe
+				PanicResponder.setTriggerPackageName(getActivity(),
+						Panic.PACKAGE_NAME_NONE);
+				showPanicApp(Panic.PACKAGE_NAME_NONE);
+			}
+		}
+	}
+
+	private void showOptInDialog() {
+		DialogInterface.OnClickListener okListener =
+				new DialogInterface.OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog,
+							int which) {
+						PanicResponder.setTriggerPackageName(getActivity());
+						showPanicApp(PanicResponder
+								.getTriggerPackageName(getActivity()));
+						getActivity().setResult(Activity.RESULT_OK);
+					}
+				};
+		DialogInterface.OnClickListener cancelListener =
+				new DialogInterface.OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog,
+							int which) {
+						getActivity().setResult(Activity.RESULT_CANCELED);
+						getActivity().finish();
+					}
+				};
+
+		AlertDialog.Builder builder =
+				new AlertDialog.Builder(getContext());
+		builder.setTitle(
+				getString(R.string.dialog_title_connect_panic_app));
+
+		CharSequence app = getString(R.string.unknown_app);
+		String packageName = getCallingPackageName();
+		if (packageName != null) {
+			try {
+				app = pm.getApplicationLabel(
+						pm.getApplicationInfo(packageName, 0));
+			} catch (PackageManager.NameNotFoundException e) {
+				LOG.warning(e.toString());
+			}
+		}
+
+		String text = String.format(
+				getString(R.string.dialog_message_connect_panic_app), app);
+		builder.setMessage(text);
+		builder.setPositiveButton(android.R.string.ok, okListener);
+		builder.setNegativeButton(android.R.string.cancel, cancelListener);
+		builder.show();
+	}
+
+	private String getCallingPackageName() {
+		ComponentName componentName = getActivity().getCallingActivity();
+		String packageName = null;
+		if (componentName != null) {
+			packageName = componentName.getPackageName();
+		}
+		return packageName;
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java b/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java
index df758e295e..1643767af0 100644
--- a/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java
+++ b/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java
@@ -7,25 +7,78 @@ import android.os.Bundle;
 import android.support.v7.preference.PreferenceManager;
 
 import org.briarproject.android.BriarActivity;
+import org.briarproject.api.db.DatabaseConfig;
+import org.briarproject.util.FileUtils;
+import org.iilab.IilabEngineeringRSA2048Pin;
 
 import java.util.logging.Logger;
 
+import javax.inject.Inject;
+
+import info.guardianproject.GuardianProjectRSA4096;
+import info.guardianproject.panic.Panic;
+import info.guardianproject.panic.PanicResponder;
+import info.guardianproject.trustedintents.TrustedIntents;
+
 public class PanicResponderActivity extends BriarActivity {
 
 	private static final Logger LOG =
 			Logger.getLogger(PanicResponderActivity.class.getName());
+	@Inject private DatabaseConfig databaseConfig;
 
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 
-		SharedPreferences sharedPref = PreferenceManager
-				.getDefaultSharedPreferences(this);
+		TrustedIntents trustedIntents = TrustedIntents.get(this);
+		// Guardian Project Ripple
+		trustedIntents.addTrustedSigner(GuardianProjectRSA4096.class);
+		// Amnesty International's Panic Button, made by iilab.org
+		trustedIntents.addTrustedSigner(IilabEngineeringRSA2048Pin.class);
+
+		Intent intent = trustedIntents.getIntentFromTrustedSender(this);
+		if (intent != null) {
+			// received intent from trusted app
+			if (Panic.isTriggerIntent(intent)) {
+				SharedPreferences sharedPref = PreferenceManager
+						.getDefaultSharedPreferences(this);
+
+				LOG.info("Received Panic Trigger...");
+
+				if (PanicResponder.receivedTriggerFromConnectedApp(this)) {
+					LOG.info("Panic Trigger came from connected app.");
+					LOG.info("Performing destructive responses...");
+
+					// Performing destructive panic responses
+					if (sharedPref.getBoolean("pref_key_purge", false)) {
+						LOG.info("Purging all data...");
+						deleteAllData();
+					}
+					// still sign out if enabled
+					else if (sharedPref.getBoolean("pref_key_lock", true)) {
+						LOG.info("Signing out...");
+						signOut(true);
+					}
 
-		Intent intent = getIntent();
-		if (intent != null && sharedPref.getBoolean("pref_key_lock", true)) {
-			LOG.info("Signing out...");
-			signOut(true);
+					// TODO add other panic behavior such as:
+					// * send a pre-defined message to certain contacts (#212)
+					// * uninstall the app (#211)
+
+				}
+				// Performing non-destructive default panic response
+				else if (sharedPref.getBoolean("pref_key_lock", true)) {
+					LOG.info("Signing out...");
+					signOut(true);
+				}
+			}
+		}
+		// received intent from non-trusted app
+		else {
+			intent = getIntent();
+			if (intent != null && Panic.isTriggerIntent(intent)) {
+				LOG.info("Signing out...");
+				signOut(true);
+			}
 		}
 
 		if (Build.VERSION.SDK_INT >= 21) {
@@ -34,4 +87,23 @@ public class PanicResponderActivity extends BriarActivity {
 			finish();
 		}
 	}
+
+	private void deleteAllData() {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				// TODO somehow delete/shred the database more thoroughly
+				FileUtils
+						.deleteFileOrDir(
+								databaseConfig.getDatabaseDirectory());
+				clearSharedPrefs();
+				PanicResponder.deleteAllAppData(PanicResponderActivity.this);
+
+				// nothing left to do after everything is deleted,
+				// so still sign out
+				LOG.info("Signing out...");
+				signOut(true);
+			}
+		});
+	}
 }
\ No newline at end of file
-- 
GitLab