Certificate export now uses the MitmService API

This commit is contained in:
emanuele-f 2022-02-15 18:13:19 +01:00
parent 6f42015873
commit 00984fc224
11 changed files with 374 additions and 181 deletions

View File

@ -0,0 +1,225 @@
/*
* This file is part of PCAPdroid.
*
* PCAPdroid is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PCAPdroid is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2022 - Emanuele Faranda
*/
package com.emanuelef.remote_capture;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.emanuelef.remote_capture.interfaces.MitmListener;
import com.emanuelef.remote_capture.model.Prefs;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class MitmAddon {
/* API */
public static final String PACKAGE_NAME = "com.pcapdroid.mitm";
public static final String MITM_PERMISSION = "com.pcapdroid.permission.MITM";
public static final String MITM_SERVICE = PACKAGE_NAME + ".MitmService";
public static final int MSG_START_MITM = 1;
public static final int MSG_GET_CA_CERTIFICATE = 2;
public static final String CERTIFICATE_RESULT = "certificate";
/* END API */
private static final String TAG = "MitmAddon";
private final Context mContext;
private final MitmListener mReceiver;
private final Messenger mMessenger;
private Messenger mService;
public MitmAddon(Context ctx, MitmListener receiver) {
// Important: the application context is required here, otherwise bind/unbind will not work properly
mContext = ctx.getApplicationContext();
mReceiver = receiver;
mMessenger = new Messenger(new ReplyHandler(receiver));
}
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "Service connected");
mService = new Messenger(service);
mReceiver.onMitmServiceConnect();
}
public void onServiceDisconnected(ComponentName className) {
Log.d(TAG, "Service disconnected");
disconnect(); // call unbind to prevent new connections
mReceiver.onMitmServiceDisconnect();
}
};
public static boolean isInstalled(Context ctx) {
try {
ctx.getPackageManager().getPackageInfo(PACKAGE_NAME, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
public static boolean hasMitmPermission(Context ctx) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return ctx.checkSelfPermission(MitmAddon.MITM_PERMISSION) == PackageManager.PERMISSION_GRANTED;
return true;
}
public static void setDecryptionSetupDone(Context ctx, boolean done) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
prefs.edit()
.putBoolean(Prefs.PREF_TLS_DECRYPTION_SETUP_DONE, done)
.apply();
}
public static boolean needsSetup(Context ctx) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
if(!Prefs.isTLSDecryptionSetupDone(prefs))
return true;
// Perform some other quick checks just in case the env has changed
if(!isInstalled(ctx) || !hasMitmPermission(ctx)) {
setDecryptionSetupDone(ctx, false);
return true;
}
return false;
}
private static class ReplyHandler extends Handler {
private final WeakReference<MitmListener> mReceiver;
ReplyHandler(MitmListener receiver) {
mReceiver = new WeakReference<>(receiver);
}
@Override
public void handleMessage(@NonNull Message msg) {
Log.d(TAG, "Message: " + msg.what);
MitmListener receiver = mReceiver.get();
if(receiver == null)
return;
if(msg.what == MitmAddon.MSG_GET_CA_CERTIFICATE) {
String ca_pem = null;
if(msg.getData() != null) {
Bundle res = msg.getData();
ca_pem = res.getString(MitmAddon.CERTIFICATE_RESULT);
}
receiver.onMitmGetCaCertificateResult(ca_pem);
}
}
}
// Asynchronously connect to the service. The onConnect callback will be called.
public boolean connect(int extra_flags) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(MitmAddon.PACKAGE_NAME, MitmAddon.MITM_SERVICE));
if(!mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE | extra_flags)) {
mContext.unbindService(mConnection);
return false;
}
return true;
}
// This must be always called after connect, e.g. in the OnDestroy
public void disconnect() {
if(mService != null) {
Log.d(TAG, "Unbinding service...");
mContext.unbindService(mConnection);
mService = null;
}
}
public boolean isConnected() {
return (mService != null);
}
public boolean requestCaCertificate() {
if(mService == null) {
Log.e(TAG, "Not connected");
return false;
}
Message msg = Message.obtain(null, MitmAddon.MSG_GET_CA_CERTIFICATE);
msg.replyTo = mMessenger;
try {
mService.send(msg);
return true;
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
}
// Start the mitm proxy and returns a ParcelFileDescriptor for the data communication.
// The proxy can be stopped by closing the descriptor and then calling disconnect().
public ParcelFileDescriptor startProxy(int port) {
if(mService == null) {
Log.e(TAG, "Not connected");
return null;
}
ParcelFileDescriptor[] pair;
try {
pair = ParcelFileDescriptor.createReliableSocketPair();
} catch (IOException e) {
e.printStackTrace();
return null;
}
Message msg = Message.obtain(null, MitmAddon.MSG_START_MITM, port, 0, pair[0]);
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
Utils.safeClose(pair[0]);
Utils.safeClose(pair[1]);
return null;
}
// The other end of the pipe is sent, close it locally
Utils.safeClose(pair[0]);
return pair[1];
}
}

View File

@ -22,22 +22,21 @@ package com.emanuelef.remote_capture;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import android.util.LruCache;
import com.emanuelef.remote_capture.activities.MitmSetupWizard;
import com.emanuelef.remote_capture.interfaces.ConnectionsListener;
import com.emanuelef.remote_capture.interfaces.MitmListener;
import com.emanuelef.remote_capture.model.ConnectionDescriptor;
import com.emanuelef.remote_capture.model.MitmAddon;
import org.jetbrains.annotations.Nullable;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
@ -53,70 +52,23 @@ import java.util.StringTokenizer;
*
* The raw payload data follows the header.
*/
public class MitmReceiver implements Runnable, ConnectionsListener {
public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener {
private static final String TAG = "MitmReceiver";
public static final int MAX_PLAINTEXT_LENGTH = 1024; // sync with pcapdroid.h
public static final int TLS_DECRYPTION_PROXY_PORT = 7780;
private Thread mThread;
private final ConnectionsRegister mReg;
private final Context mContext;
private Messenger mService;
private boolean bound;
private final MitmAddon mAddon;
private ParcelFileDescriptor mSocketFd;
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "Service connected");
mService = new Messenger(service);
ParcelFileDescriptor[] pair;
try {
// Create a pair of connected fds
pair = ParcelFileDescriptor.createReliableSocketPair();
} catch (IOException e) {
e.printStackTrace();
return;
}
mSocketFd = pair[1];
Message msg = Message.obtain(null, MitmAddon.MSG_START_MITM, TLS_DECRYPTION_PROXY_PORT, 0, pair[0]);
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
Utils.safeClose(pair[0]);
Utils.safeClose(pair[1]);
return;
}
bound = true;
// Sent, close here
Utils.safeClose(pair[0]);
if(mThread != null)
mThread.interrupt();
mThread = new Thread(MitmReceiver.this);
mThread.start();
}
public void onServiceDisconnected(ComponentName className) {
Log.d(TAG, "Service disconnected");
mService = null;
Utils.safeClose(mSocketFd);
mSocketFd = null;
bound = false;
}
};
// Shared state
private final LruCache<Integer, Integer> mPortToConnId = new LruCache<>(64);
public MitmReceiver(Context ctx) {
// Important: the application context is required here, otherwise bind/unbind will not work properly
mContext = ctx.getApplicationContext();
mContext = ctx;
mReg = CaptureService.requireConnsRegister();
mAddon = new MitmAddon(mContext, this);
}
public boolean start() throws IOException {
@ -125,8 +77,7 @@ public class MitmReceiver implements Runnable, ConnectionsListener {
Intent intent = new Intent();
intent.setComponent(new ComponentName(MitmAddon.PACKAGE_NAME, MitmAddon.MITM_SERVICE));
if(!mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT)) {
mContext.unbindService(mConnection);
if(!mAddon.connect(Context.BIND_IMPORTANT)) {
Utils.showToastLong(mContext, R.string.mitm_start_failed);
return false;
}
@ -152,19 +103,17 @@ public class MitmReceiver implements Runnable, ConnectionsListener {
}
mThread = null;
if(bound) {
Log.d(TAG, "Unbinding service...");
mContext.unbindService(mConnection);
bound = false;
}
mAddon.disconnect();
Log.d(TAG, "stop done");
}
@Override
public void run() {
Log.d(TAG, "Receiving data...");
try(DataInputStream istream = new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(mSocketFd))) {
while(bound) {
while(mAddon.isConnected()) {
String payload_type;
int port;
int payload_len;
@ -214,6 +163,8 @@ public class MitmReceiver implements Runnable, ConnectionsListener {
if(mSocketFd != null) // ignore termination
e.printStackTrace();
}
Log.d(TAG, "End receiving data");
}
@Override
@ -232,6 +183,47 @@ public class MitmReceiver implements Runnable, ConnectionsListener {
}
}
@Override
public void onMitmServiceConnect() {
// when connected, verify that the certificate is installed before starting the proxy.
// will continue on onMitmGetCaCertificateResult.
if(!mAddon.requestCaCertificate())
mAddon.disconnect();
}
@Override
public void onMitmGetCaCertificateResult(@Nullable String ca_pem) {
if(!Utils.isCAInstalled(ca_pem)) {
// The certificate has been uninstalled from the system
Utils.showToastLong(mContext, R.string.cert_reinstall_required);
MitmAddon.setDecryptionSetupDone(mContext, false);
CaptureService.stopService();
return;
}
// Certificate installation verified, start the proxy
mSocketFd = mAddon.startProxy(TLS_DECRYPTION_PROXY_PORT);
if(mSocketFd == null) {
mAddon.disconnect();
return;
}
if(mThread != null)
mThread.interrupt();
mThread = new Thread(MitmReceiver.this);
mThread.start();
}
@Override
public void onMitmServiceDisconnect() {
Utils.safeClose(mSocketFd);
mSocketFd = null;
// Stop the capture if running
CaptureService.stopService();
}
ConnectionDescriptor getConnByLocalPort(int local_port) {
Integer conn_id;

View File

@ -975,4 +975,15 @@ public class Utils {
return false;
}
}
public static boolean isCAInstalled(String ca_pem) {
if(ca_pem == null)
return false;
X509Certificate ca_cert = x509FromPem(ca_pem);
if(ca_cert == null)
return false;
return isCAInstalled(ca_cert);
}
}

View File

@ -70,7 +70,7 @@ import com.emanuelef.remote_capture.model.AppState;
import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.model.CaptureSettings;
import com.emanuelef.remote_capture.model.ListInfo;
import com.emanuelef.remote_capture.model.MitmAddon;
import com.emanuelef.remote_capture.MitmAddon;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;

View File

@ -42,7 +42,7 @@ import androidx.preference.SwitchPreference;
import com.emanuelef.remote_capture.Billing;
import com.emanuelef.remote_capture.PCAPdroid;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.model.MitmAddon;
import com.emanuelef.remote_capture.MitmAddon;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.R;

View File

@ -30,7 +30,7 @@ import androidx.annotation.Nullable;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.model.MitmAddon;
import com.emanuelef.remote_capture.MitmAddon;
public class GrantPermission extends StepFragment {
private final ActivityResultLauncher<String> requestPermissionLauncher =

View File

@ -26,7 +26,7 @@ import androidx.annotation.Nullable;
import android.view.View;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.model.MitmAddon;
import com.emanuelef.remote_capture.MitmAddon;
public class InstallAddon extends StepFragment {
@Override

View File

@ -25,6 +25,7 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.activity.result.ActivityResult;
@ -35,18 +36,19 @@ import androidx.annotation.Nullable;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.model.MitmAddon;
import com.emanuelef.remote_capture.interfaces.MitmListener;
import com.emanuelef.remote_capture.MitmAddon;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.cert.X509Certificate;
public class InstallCertificate extends StepFragment {
public class InstallCertificate extends StepFragment implements MitmListener {
private static final String TAG = "InstallCertificate";
private MitmAddon mAddon;
private String mCaPem;
private X509Certificate mCaCert;
private final ActivityResultLauncher<Intent> mitmCtrlLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this::mitmCtrlResult);
private final ActivityResultLauncher<Intent> certFileLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this::certFileResult);
@ -57,15 +59,15 @@ public class InstallCertificate extends StepFragment {
mStepButton.setText(R.string.export_action);
mStepButton.setEnabled(false);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClassName(MitmAddon.PACKAGE_NAME, MitmAddon.CONTROL_ACTIVITY);
intent.putExtra(MitmAddon.ACTION_EXTRA, MitmAddon.ACTION_GET_CA_CERTIFICATE);
mAddon = new MitmAddon(requireContext(), this);
if(!mAddon.connect(0))
certFail();
}
try {
mitmCtrlLauncher.launch(intent);
} catch (ActivityNotFoundException e) {
mStepLabel.setText(R.string.no_intent_handler_found);
}
@Override
public void onDestroyView() {
mAddon.disconnect();
super.onDestroyView();
}
@Override
@ -76,28 +78,6 @@ public class InstallCertificate extends StepFragment {
super.onResume();
}
private void mitmCtrlResult(final ActivityResult result) {
if((result.getResultCode() == Activity.RESULT_OK) && (result.getData() != null)) {
Intent res = result.getData();
mCaPem = res.getStringExtra(MitmAddon.CERTIFICATE_RESULT);
if(mCaPem != null) {
//Log.d(TAG, "certificate: " + cert_str);
mCaCert = Utils.x509FromPem(mCaPem);
if(mCaCert != null) {
if(Utils.isCAInstalled(mCaCert))
certOk();
else {
MitmAddon.setDecryptionSetupDone(requireContext(), false);
installCaCertificate();
}
} else
certFail();
}
}
}
private void certOk() {
MitmAddon.setDecryptionSetupDone(requireContext(), true);
mStepLabel.setText(R.string.cert_installed_correctly);
@ -154,4 +134,38 @@ public class InstallCertificate extends StepFragment {
Utils.showToastLong(ctx, R.string.cert_exported_now_installed);
}
}
@Override
public void onMitmGetCaCertificateResult(@Nullable String ca_pem) {
mAddon.disconnect();
mCaPem = ca_pem;
if(mCaPem != null) {
Log.d(TAG, "Got certificate");
//Log.d(TAG, "certificate: " + cert_str);
mCaCert = Utils.x509FromPem(mCaPem);
if(mCaCert != null) {
if(Utils.isCAInstalled(mCaCert))
certOk();
else {
MitmAddon.setDecryptionSetupDone(requireContext(), false);
installCaCertificate();
}
} else
certFail();
}
}
@Override
public void onMitmServiceConnect() {
if(!mAddon.requestCaCertificate())
certFail();
}
@Override
public void onMitmServiceDisconnect() {
if(mCaPem == null)
certFail();
}
}

View File

@ -0,0 +1,28 @@
/*
* This file is part of PCAPdroid.
*
* PCAPdroid is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PCAPdroid is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2020-21 - Emanuele Faranda
*/
package com.emanuelef.remote_capture.interfaces;
import org.jetbrains.annotations.Nullable;
public interface MitmListener {
void onMitmGetCaCertificateResult(@Nullable String ca_pem);
void onMitmServiceConnect();
void onMitmServiceDisconnect();
}

View File

@ -1,78 +0,0 @@
/*
* This file is part of PCAPdroid.
*
* PCAPdroid is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PCAPdroid is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2022 - Emanuele Faranda
*/
package com.emanuelef.remote_capture.model;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.preference.PreferenceManager;
public class MitmAddon {
public static final String PACKAGE_NAME = "com.pcapdroid.mitm";
public static final String MITM_PERMISSION = "com.pcapdroid.permission.MITM";
public static final String MITM_SERVICE = PACKAGE_NAME + ".MitmService";
public static final int MSG_START_MITM = 1;
public static final String CONTROL_ACTIVITY = PACKAGE_NAME + ".MitmCtrl";
public static final String ACTION_EXTRA = "action";
public static final String ACTION_GET_CA_CERTIFICATE = "getCAcert";
public static final String CERTIFICATE_RESULT = "certificate";
public static boolean isInstalled(Context ctx) {
try {
ctx.getPackageManager().getPackageInfo(PACKAGE_NAME, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
public static boolean hasMitmPermission(Context ctx) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return ctx.checkSelfPermission(MitmAddon.MITM_PERMISSION) == PackageManager.PERMISSION_GRANTED;
return true;
}
public static void setDecryptionSetupDone(Context ctx, boolean done) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
prefs.edit()
.putBoolean(Prefs.PREF_TLS_DECRYPTION_SETUP_DONE, done)
.apply();
}
public static boolean needsSetup(Context ctx) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
if(!Prefs.isTLSDecryptionSetupDone(prefs))
return true;
// Perform some other quick checks just in case the env has changed
if(!isInstalled(ctx) || !hasMitmPermission(ctx)) {
setDecryptionSetupDone(ctx, false);
return true;
}
return false;
}
}

View File

@ -279,5 +279,6 @@
<string name="ca_installation_failed">CA certificate installation failed</string>
<string name="cert_exported_now_installed">Certificate exported, now install it from the Android settings</string>
<string name="cert_installed_correctly">The CA certificate is installed</string>
<string name="cert_reinstall_required">The CA certificate is not installed, run the mitm setup wizard</string>
<string name="done">Done</string>
</resources>