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
This commit is contained in:
Torsten Grote
2016-01-07 19:50:20 -02:00
parent e603b4f60e
commit 63d87ae085
5 changed files with 330 additions and 7 deletions

View File

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

View File

@@ -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',
]
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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 = getIntent();
if (intent != null && sharedPref.getBoolean("pref_key_lock", true)) {
LOG.info("Signing out...");
signOut(true);
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);
}
// 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);
}
});
}
}