Allow PCAPdroid capture control without prompt via API key

It's now possible to generate an API key to be set in the Intent,
allowing to control the PCAPdroid capture without prompt

Closes #516
This commit is contained in:
emanuele-f 2025-05-10 12:00:25 +02:00 committed by Emanuele Faranda
parent 5d82cf10a7
commit 64611f2ed2
7 changed files with 139 additions and 9 deletions

View File

@ -106,6 +106,7 @@ public class CaptureCtrl extends AppCompatActivity {
Intent intent = getIntent();
String action = intent.getStringExtra("action");
String api_key = intent.getStringExtra("api_key");
if(action == null) {
Log.e(TAG, "no action provided");
@ -118,6 +119,17 @@ public class CaptureCtrl extends AppCompatActivity {
return;
}
if(api_key != null) {
// authenticate via API key
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String my_key = Prefs.getApiKey(prefs);
if (!my_key.isEmpty() && my_key.equals(api_key)) {
processRequest(intent, action);
return;
}
}
// Check if a control permission rule was set
mPermissions = PCAPdroid.getInstance().getCtrlPermissions();
AppDescriptor app = getCallingApp();

View File

@ -132,6 +132,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
public static final String PAID_FEATURES_URL = DOCS_URL + "/paid_features";
public static final String FIREWALL_DOCS_URL = PAID_FEATURES_URL + "#51-firewall";
public static final String MALWARE_DETECTION_DOCS_URL = PAID_FEATURES_URL + "#52-malware-detection";
public static final String API_DOCS_URL = GITHUB_PROJECT_URL + "/blob/master/docs/app_api.md";
public static final String PCAPNG_DOCS_URL = PAID_FEATURES_URL + "#53-pcapng-format";
private final ActivityResultLauncher<Intent> sslkeyfileExportLauncher =

View File

@ -18,6 +18,9 @@
*/
package com.emanuelef.remote_capture.activities.prefs;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
@ -28,21 +31,31 @@ import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.MenuProvider;
import androidx.preference.PreferenceManager;
import com.emanuelef.remote_capture.PCAPdroid;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.activities.BaseActivity;
import com.emanuelef.remote_capture.activities.MainActivity;
import com.emanuelef.remote_capture.adapters.CtrlPermissionsAdapter;
import com.emanuelef.remote_capture.model.CtrlPermissions;
import com.emanuelef.remote_capture.model.Prefs;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
public class EditCtrlPermissions extends BaseActivity {
public class EditCtrlPermissions extends BaseActivity implements MenuProvider {
private static final String TAG = "EditCtrlPermissions";
private TextView mEmptyText;
private CtrlPermissionsAdapter mAdapter;
private ListView mListView;
private CtrlPermissions mPermissions;
private MenuItem mShowApiKey;
private final ArrayList<CtrlPermissions.Rule> mSelected = new ArrayList<>();
@Override
@ -51,9 +64,11 @@ public class EditCtrlPermissions extends BaseActivity {
setTitle(R.string.control_permissions);
setContentView(R.layout.simple_list_activity);
addMenuProvider(this);
findViewById(R.id.simple_list).setFitsSystemWindows(true);
mEmptyText = findViewById(R.id.list_empty);
mEmptyText.setText(R.string.no_permissions_set_info);
mListView = findViewById(R.id.listview);
mPermissions = PCAPdroid.getInstance().getCtrlPermissions();
@ -126,7 +141,81 @@ public class EditCtrlPermissions extends BaseActivity {
recheckListSize();
}
@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.ctrl_permissions_menu, menu);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
mShowApiKey = menu.findItem(R.id.show_api_key);
if (Prefs.getApiKey(prefs).isEmpty())
mShowApiKey.setVisible(false);
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if(id == R.id.user_guide) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(MainActivity.API_DOCS_URL));
Utils.startActivity(this, browserIntent);
return true;
} else if (id == R.id.generate_api_key) {
generateApiKey(false);
return true;
} else if (id == R.id.show_api_key) {
showApiKey();
return true;
}
return false;
}
private void recheckListSize() {
mEmptyText.setVisibility((mAdapter.getCount() == 0) ? View.VISIBLE : View.GONE);
}
private void generateApiKey(boolean confirmOverwrite) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (!confirmOverwrite && !Prefs.getApiKey(prefs).isEmpty()) {
new AlertDialog.Builder(this)
.setTitle(R.string.warning)
.setMessage(R.string.api_key_discard_confirm)
.setPositiveButton(R.string.ok, (dialog, whichButton) -> generateApiKey(true))
.setNegativeButton(R.string.cancel_action, (dialog, whichButton) -> {})
.show();
return;
}
final String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
final int key_length = 32;
SecureRandom random = new SecureRandom();
StringBuilder apiKey = new StringBuilder(key_length);
for (int i = 0; i < key_length; i++) {
int index = random.nextInt(chars.length());
apiKey.append(chars.charAt(index));
}
prefs.edit()
.putString(Prefs.PREF_API_KEY, apiKey.toString())
.apply();
if (mShowApiKey != null)
mShowApiKey.setVisible(true);
showApiKey();
}
private void showApiKey() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String key = Prefs.getApiKey(prefs);
if (key.isEmpty())
return;
new AlertDialog.Builder(this)
.setTitle(R.string.api_key)
.setMessage(key)
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {})
.setNeutralButton(R.string.copy_to_clipboard, (dialogInterface, i) ->
Utils.copyToClipboard(this, key)).show();
}
}

View File

@ -461,14 +461,11 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
mIpMode = requirePreference(Prefs.PREF_IP_MODE);
Preference ctrlPerm = requirePreference("control_permissions");
if(!PCAPdroid.getInstance().getCtrlPermissions().hasRules())
ctrlPerm.setVisible(false);
else
ctrlPerm.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(requireContext(), EditCtrlPermissions.class);
startActivity(intent);
return true;
});
ctrlPerm.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(requireContext(), EditCtrlPermissions.class);
startActivity(intent);
return true;
});
}
private void rootCaptureHideShow(boolean enabled) {

View File

@ -110,6 +110,7 @@ public class Prefs {
public static final String PREF_PCAPNG_ENABLED = "pcapng_format";
public static final String PREF_RESTART_ON_DISCONNECT = "restart_on_disconnect";
public static final String PREF_IGNORED_MITM_VERSION = "ignored_mitm_version";
public static final String PREF_API_KEY = "api_key";
public enum DumpMode {
NONE,
@ -238,6 +239,7 @@ public class Prefs {
public static String getDnsServerV4(SharedPreferences p) { return(p.getString(PREF_DNS_SERVER_V4, "1.1.1.1")); }
public static String getDnsServerV6(SharedPreferences p) { return(p.getString(PREF_DNS_SERVER_V6, "2606:4700:4700::1111")); }
public static boolean isIgnoredMitmVersion(SharedPreferences p, String v) { return p.getString(PREF_IGNORED_MITM_VERSION, "").equals(v); }
public static String getApiKey(SharedPreferences p) { return(p.getString(PREF_API_KEY, "")); }
// Gets a StringSet from the prefs
// The preference should either be a StringSet or a String

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/user_guide"
android:title="@string/user_guide"
android:orderInCategory="10"
android:icon="@drawable/ic_book"
app:showAsAction="ifRoom" />
<item
android:id="@+id/show_api_key"
android:title="@string/show_api_key"
android:orderInCategory="20"
app:showAsAction="never" />
<item
android:id="@+id/generate_api_key"
android:title="@string/generate_api_key"
android:orderInCategory="20"
app:showAsAction="never" />
</menu>

View File

@ -217,6 +217,11 @@
<string name="control_permissions">Control permissions</string>
<string name="control_permissions_summary">Check which apps are allowed to control the PCAPdroid capture</string>
<string name="control_permissions_item">%1$s: %2$s</string>
<string name="no_permissions_set_info">No permissions set. Invoke PCAPdroid via StartActivityForResult to show the permissions prompt</string>
<string name="generate_api_key">Generate API key</string>
<string name="show_api_key">Show API key</string>
<string name="api_key">API key</string>
<string name="api_key_discard_confirm">Do you really want to discard the current API key and generate a new one?</string>
<string name="country">Country</string>
<string name="asn">ASN</string>
<string name="country_val">Country: %1$s</string>