Fix export of SSLKEYLOG

The keylog export dialog now appears automatically when the capture is
stopped and PCAP dump is enabled
This commit is contained in:
emanuele-f 2022-04-26 16:32:11 +02:00
parent 4e89c54724
commit 6854284db0
13 changed files with 83 additions and 148 deletions

View File

@ -59,7 +59,6 @@ import com.emanuelef.remote_capture.activities.CaptureCtrl;
import com.emanuelef.remote_capture.activities.ConnectionsActivity;
import com.emanuelef.remote_capture.activities.MainActivity;
import com.emanuelef.remote_capture.fragments.ConnectionsFragment;
import com.emanuelef.remote_capture.interfaces.SslkeylogDumpListener;
import com.emanuelef.remote_capture.model.AppDescriptor;
import com.emanuelef.remote_capture.model.BlacklistDescriptor;
import com.emanuelef.remote_capture.model.Blacklists;
@ -76,6 +75,7 @@ import com.emanuelef.remote_capture.interfaces.PcapDumper;
import com.emanuelef.remote_capture.pcap_dump.UDPDumper;
import com.pcapdroid.mitm.MitmAPI;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@ -785,13 +785,6 @@ public class CaptureService extends VpnService implements Runnable {
return reg;
}
// See MitmReceiver.dumpSslkeylogfile
public static boolean dumpSslkeylogfile(SslkeylogDumpListener listener) {
if(INSTANCE != null)
return INSTANCE.mMitmReceiver.dumpSslkeylogfile(listener);
return false;
}
public static boolean isCapturingAsRoot() {
return((INSTANCE != null) &&
(INSTANCE.isRootCapture() == 1));

View File

@ -49,8 +49,8 @@ import java.io.IOException;
import java.lang.ref.WeakReference;
public class MitmAddon {
public static final long PACKAGE_VERSION_CODE = 5;
public static final String PACKAGE_VERSION_NAME = "v0.5";
public static final long PACKAGE_VERSION_CODE = 6;
public static final String PACKAGE_VERSION_NAME = "v0.6";
private static final String TAG = "MitmAddon";
private final Context mContext;
private final MitmListener mReceiver;
@ -150,15 +150,6 @@ public class MitmAddon {
}
receiver.onMitmGetCaCertificateResult(ca_pem);
} else if(msg.what == MitmAPI.MSG_GET_SSLKEYLOG) {
byte []sslkeylog = null;
if(msg.getData() != null) {
Bundle res = msg.getData();
sslkeylog = res.getByteArray(MitmAPI.SSLKEYLOG_RESULT);
}
receiver.onMitmSslkeylogfileResult(sslkeylog);
}
}
}
@ -205,26 +196,9 @@ public class MitmAddon {
}
}
public boolean requestSslkeylogfile() {
if(mService == null) {
Log.e(TAG, "Not connected");
return false;
}
Message msg = Message.obtain(null, MitmAPI.MSG_GET_SSLKEYLOG);
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, String proxyAuth) {
public ParcelFileDescriptor startProxy(MitmAPI.MitmConfig conf) {
if(mService == null) {
Log.e(TAG, "Not connected");
return null;
@ -238,15 +212,6 @@ public class MitmAddon {
return null;
}
MitmAPI.MitmConfig conf = new MitmAPI.MitmConfig();
conf.proxyPort = port;
conf.proxyAuth = proxyAuth;
/* upstream certificate verification is disabled because the app does not provide a way to let the user
accept a given cert. Moreover, it provides a workaround for a bug with HTTPS proxies described in
https://github.com/mitmproxy/mitmproxy/issues/5109 */
conf.sslInsecure = true;
// Note: ParcelFileDescriptor must be passed as parcelable
Message msg = Message.obtain(null, MitmAPI.MSG_START_MITM, 0, 0, pair[0]);

View File

@ -20,6 +20,7 @@
package com.emanuelef.remote_capture;
import android.content.Context;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.util.Log;
@ -28,14 +29,17 @@ import android.util.SparseArray;
import com.emanuelef.remote_capture.interfaces.ConnectionsListener;
import com.emanuelef.remote_capture.interfaces.MitmListener;
import com.emanuelef.remote_capture.interfaces.SslkeylogDumpListener;
import com.emanuelef.remote_capture.model.ConnectionDescriptor;
import com.emanuelef.remote_capture.model.PayloadChunk;
import com.emanuelef.remote_capture.model.PayloadChunk.ChunkType;
import com.emanuelef.remote_capture.model.Prefs;
import com.pcapdroid.mitm.MitmAPI;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -61,9 +65,9 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
private final ConnectionsRegister mReg;
private final Context mContext;
private final MitmAddon mAddon;
private final String mProxyAuth;
private final MitmAPI.MitmConfig mConfig;
private ParcelFileDescriptor mSocketFd;
private SslkeylogDumpListener mSslkeylogListener;
private BufferedOutputStream mKeylog;
// Shared state
private final LruCache<Integer, Integer> mPortToConnId = new LruCache<>(64);
@ -80,6 +84,7 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
TCP_ERROR,
WEBSOCKET_CLIENT_MSG,
WEBSOCKET_SERVER_MSG,
MASTER_SECRET,
}
private static class PendingPayload {
@ -102,7 +107,23 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
mContext = ctx;
mReg = CaptureService.requireConnsRegister();
mAddon = new MitmAddon(mContext, this);
mProxyAuth = proxyAuth;
mConfig = new MitmAPI.MitmConfig();
mConfig.proxyPort = TLS_DECRYPTION_PROXY_PORT;
mConfig.proxyAuth = proxyAuth;
mConfig.dumpMasterSecrets = (CaptureService.getDumpMode() != Prefs.DumpMode.NONE);
/* upstream certificate verification is disabled because the app does not provide a way to let the user
accept a given cert. Moreover, it provides a workaround for a bug with HTTPS proxies described in
https://github.com/mitmproxy/mitmproxy/issues/5109 */
mConfig.sslInsecure = true;
//noinspection ResultOfMethodCallIgnored
getKeylogFilePath(mContext).delete();
}
public static File getKeylogFilePath(Context ctx) {
return new File(ctx.getCacheDir(), "SSLKEYLOG.txt");
}
public boolean start() throws IOException {
@ -193,18 +214,25 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
byte[] payload = new byte[payload_len];
istream.readFully(payload);
ConnectionDescriptor conn = getConnByLocalPort(port);
//Log.d(TAG, "PAYLOAD." + pType.name() + "[" + payload_len + " B]: port=" + port + ", match=" + (conn != null));
if(pType == PayloadType.MASTER_SECRET)
logMasterSecret(payload);
else {
ConnectionDescriptor conn = getConnByLocalPort(port);
//Log.d(TAG, "PAYLOAD." + pType.name() + "[" + payload_len + " B]: port=" + port + ", match=" + (conn != null));
if(conn != null)
handlePayload(conn, pType, payload, tstamp);
else
// We may receive a payload before seeing the connection in connectionsAdded
addPendingPayload(new PendingPayload(pType, payload, port, tstamp));
if(conn != null)
handlePayload(conn, pType, payload, tstamp);
else
// We may receive a payload before seeing the connection in connectionsAdded
addPendingPayload(new PendingPayload(pType, payload, port, tstamp));
}
}
} catch (IOException e) {
if(mSocketFd != null) // ignore termination
e.printStackTrace();
} finally {
Utils.safeClose(mKeylog);
mKeylog = null;
}
Log.d(TAG, "End receiving data");
@ -293,11 +321,23 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
return PayloadType.WEBSOCKET_CLIENT_MSG;
case "ws_srvmsg":
return PayloadType.WEBSOCKET_SERVER_MSG;
case "secret":
return PayloadType.MASTER_SECRET;
default:
return PayloadType.UNKNOWN;
}
}
private void logMasterSecret(byte[] master_secret) throws IOException {
if(mKeylog == null)
mKeylog = new BufferedOutputStream(
mContext.getContentResolver().openOutputStream(
Uri.fromFile(getKeylogFilePath(mContext)), "rwt"));
mKeylog.write(master_secret);
mKeylog.write(0xa);
}
@Override
public void connectionsChanges(int num_connetions) {}
@Override
@ -350,7 +390,7 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
}
// Certificate installation verified, start the proxy
mSocketFd = mAddon.startProxy(TLS_DECRYPTION_PROXY_PORT, mProxyAuth);
mSocketFd = mAddon.startProxy(mConfig);
if(mSocketFd == null) {
mAddon.disconnect();
return;
@ -367,7 +407,6 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
public void onMitmServiceDisconnect() {
// Stop the capture if running, CaptureService will call MitmReceiver::stop
CaptureService.stopService();
mSslkeylogListener = null;
}
ConnectionDescriptor getConnByLocalPort(int local_port) {
@ -386,26 +425,4 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
// success
return conn;
}
/* Requests to dump the sslkeylogfile of the remote mitm-addon.
* Returns false if the the dump cannot be done. The listener onSslkeylogDumpResult method can
* only be invoked when returning true. */
public boolean dumpSslkeylogfile(SslkeylogDumpListener listener) {
if(mAddon.isConnected() && mAddon.requestSslkeylogfile()) {
// will continue in onMitmSslkeylogfileResult
mSslkeylogListener = listener;
return true;
}
return false;
}
@Override
public void onMitmSslkeylogfileResult(@Nullable byte []contents) {
if(mSslkeylogListener == null)
return;
mSslkeylogListener.onSslkeylogDumpResult(contents);
mSslkeylogListener = null;
}
}

View File

@ -884,6 +884,7 @@ public class Utils {
if(!has_contents) {
try {
//noinspection ResultOfMethodCallIgnored
(new File(path + ".tmp")).delete(); // if exists
} catch (Exception e) {
// ignore

View File

@ -64,6 +64,7 @@ import android.widget.TextView;
import com.emanuelef.remote_capture.Billing;
import com.emanuelef.remote_capture.BuildConfig;
import com.emanuelef.remote_capture.CaptureHelper;
import com.emanuelef.remote_capture.MitmReceiver;
import com.emanuelef.remote_capture.fragments.ConnectionsFragment;
import com.emanuelef.remote_capture.fragments.StatusFragment;
import com.emanuelef.remote_capture.interfaces.AppStateListener;
@ -91,6 +92,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
private AppState mState;
private AppStateListener mListener;
private Uri mPcapUri;
private File mKeylogFile;
private BroadcastReceiver mReceiver;
private String mPcapFname;
private DrawerLayout mDrawer;
@ -156,17 +158,27 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
Log.d(TAG, "Service status: " + status);
if (status.equals(CaptureService.SERVICE_STATUS_STARTED)) {
mKeylogFile = null;
appStateRunning();
} else if (status.equals(CaptureService.SERVICE_STATUS_STOPPED)) {
// The service may still be active (on premature native termination)
if (CaptureService.isServiceActive())
CaptureService.stopService();
mKeylogFile = MitmReceiver.getKeylogFilePath(MainActivity.this);
if(!mKeylogFile.exists())
mKeylogFile = null;
Log.d(TAG, "sslkeylog? " + (mKeylogFile != null));
if((mPcapUri != null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE)) {
showPcapActionDialog(mPcapUri);
mPcapUri = null;
mPcapFname = null;
}
// will export the keylogfile after saving/sharing pcap
} else if(mKeylogFile != null)
startExportSslkeylogfile();
appStateReady();
}
@ -473,9 +485,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
Intent intent = new Intent(MainActivity.this, SettingsActivity.class);
startActivity(intent);
return true;
} else if (id == R.id.export_sslkeylogfile) {
startExportSslkeylogfile();
return true;
}
return super.onOptionsItemSelected(item);
@ -670,6 +679,10 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
dialog.cancel();
});
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.cancel());
builder.setOnDismissListener(dialogInterface -> {
if(mKeylogFile != null)
startExportSslkeylogfile();
});
builder.create().show();
}
@ -727,20 +740,13 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
private void sslkeyfileExportResult(final ActivityResult result) {
if(result.getResultCode() == RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if(!CaptureService.dumpSslkeylogfile(sslkeylog -> exportSslkeylogfile(uri, sslkeylog)))
try(OutputStream out = getContentResolver().openOutputStream(result.getData().getData(), "rwt")) {
Utils.copy(mKeylogFile, out);
Utils.showToast(this, R.string.save_ok);
} catch (IOException e) {
e.printStackTrace();
Utils.showToastLong(this, R.string.export_failed);
}
}
private void exportSslkeylogfile(Uri export_uri, @Nullable byte[] sslkeylog) {
try(OutputStream out = getContentResolver().openOutputStream(export_uri, "rwt")) {
out.write(sslkeylog);
Utils.showToast(this, R.string.save_ok);
} catch (IOException e) {
e.printStackTrace();
Utils.showToastLong(this, R.string.cannot_write_file);
}
}
}
}

View File

@ -25,7 +25,6 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@ -49,7 +48,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.RecyclerView;
@ -68,7 +66,6 @@ import com.emanuelef.remote_capture.adapters.ConnectionsAdapter;
import com.emanuelef.remote_capture.model.FilterDescriptor;
import com.emanuelef.remote_capture.model.MatchList;
import com.emanuelef.remote_capture.model.MatchList.RuleType;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.views.EmptyRecyclerView;
import com.emanuelef.remote_capture.interfaces.ConnectionsListener;
import com.emanuelef.remote_capture.activities.EditFilterActivity;
@ -94,7 +91,6 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener
private ChipGroup mActiveFilter;
private MenuItem mMenuFilter;
private MenuItem mMenuItemSearch;
private MenuItem mMenuSslkeylogExport;
private MenuItem mSave;
private BroadcastReceiver mReceiver;
private Uri mCsvFname;
@ -619,7 +615,6 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener
mSave = menu.findItem(R.id.save);
mMenuFilter = menu.findItem(R.id.edit_filter);
mMenuItemSearch = menu.findItem(R.id.search);
mMenuSslkeylogExport = menu.findItem(R.id.export_sslkeylogfile);
mSearchView = (SearchView) mMenuItemSearch.getActionView();
mSearchView.setOnQueryTextListener(this);
@ -660,10 +655,6 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener
mMenuItemSearch.setVisible(is_enabled); // NOTE: setEnabled does not work for this
//mMenuFilter.setEnabled(is_enabled);
mSave.setEnabled(is_enabled);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
mMenuSslkeylogExport.setVisible(Utils.supportsFileDialog(ctx) && Prefs.getTlsDecryptionEnabled(prefs));
mMenuSslkeylogExport.setEnabled(CaptureService.isServiceActive());
}
private void dumpCsv() {

View File

@ -217,7 +217,4 @@ public class InstallCertificate extends StepFragment implements MitmListener {
if(mCaPem == null)
certFail();
}
@Override
public void onMitmSslkeylogfileResult(@Nullable byte[] contents) {}
}

View File

@ -23,7 +23,6 @@ import org.jetbrains.annotations.Nullable;
public interface MitmListener {
void onMitmGetCaCertificateResult(@Nullable String ca_pem);
void onMitmSslkeylogfileResult(@Nullable byte[] contents);
void onMitmServiceConnect();
void onMitmServiceDisconnect();
}

View File

@ -1,25 +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 2020-22 - Emanuele Faranda
*/
package com.emanuelef.remote_capture.interfaces;
import androidx.annotation.Nullable;
public interface SslkeylogDumpListener {
void onSslkeylogDumpResult(@Nullable byte[] sslkeylog);
}

View File

@ -193,6 +193,8 @@ public class Blacklists {
for(File f: files) {
if(!validLists.contains(f)) {
Log.d(TAG, "Removing unknown list: " + f.getPath());
//noinspection ResultOfMethodCallIgnored
f.delete();
}
}

View File

@ -19,8 +19,6 @@
package com.pcapdroid.mitm;
import android.os.ParcelFileDescriptor;
import java.io.Serializable;
/* API to integrate MitmAddon */
@ -33,7 +31,6 @@ public class MitmAPI {
public static final int MSG_START_MITM = 1;
public static final int MSG_GET_CA_CERTIFICATE = 2;
public static final int MSG_STOP_MITM = 3;
public static final int MSG_GET_SSLKEYLOG = 4;
public static final String MITM_CONFIG = "mitm_config";
public static final String CERTIFICATE_RESULT = "certificate";
public static final String SSLKEYLOG_RESULT = "sslkeylog";
@ -41,6 +38,7 @@ public class MitmAPI {
public static final class MitmConfig implements Serializable {
public int proxyPort; // the SOCKS5 port to use to accept mitm-ed connections
public boolean sslInsecure; // true to disable upstream certificate check
public boolean dumpMasterSecrets; // true to enable the TLS master secrets dump messages (similar to SSLKEYLOG)
public String proxyAuth; // SOCKS5 proxy authentication, "user:pass"
}
}

View File

@ -28,12 +28,4 @@
android:orderInCategory="30"
android:icon="@drawable/ic_save"
app:showAsAction="never" />
<item
android:id="@+id/export_sslkeylogfile"
android:title="@string/export_sslkeylog"
android:orderInCategory="40"
android:icon="@drawable/ic_save"
android:visible="false"
app:showAsAction="never" />
</menu>

View File

@ -288,7 +288,6 @@
<string name="mitm_addon_bad_version">Bad PCAPdroid mitm addon version. Uninstall it and retry</string>
<string name="mitm_addon_new_version">The PCAPdroid plugin must be upgraded</string>
<string name="upgrade_action">Upgrade</string>
<string name="export_sslkeylog">Export SSL keylog</string>
<string name="export_failed">Export failed</string>
<string name="connection">Connection</string>
<string name="encrypted">Encrypted</string>