diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index 89484f4a1fd7eb7b35dd4f6b9724e0323508f668..0b7908081fb931c29814a8309fd0d28ef4bb97ec 100644
--- a/briar-android/build.gradle
+++ b/briar-android/build.gradle
@@ -12,6 +12,9 @@ dependencies {
 	compile project(':briar-api')
 	compile project(':briar-core')
 	compile fileTree(dir: 'libs', include: '*.jar')
+	// This shouldn't be necessary; per section 23.4.4 of the Gradle docs:
+	// "file dependencies are included in transitive project dependencies within the same build".
+	compile files('../briar-core/libs/jsocks.jar')
 	compile "com.android.support:support-v4:$supportVersion"
 	compile("com.android.support:appcompat-v7:$supportVersion") {
 		exclude module: 'support-v4'
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index ca1e962f4e95be6e1de366ec80c8a9edc9252721..ec531c92d000e5a3f6b84b336174ffccaf899502 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -4,6 +4,8 @@
 	<string name="nav_drawer_close_description">Close the navigation drawer</string>
 	<string name="app_name">Briar</string>
 	<string name="crash_report_title">Briar Crash Report</string>
+	<string name="crash_report_saved">Crash report saved. It will be sent the next time you log into Briar.</string>
+	<string name="crash_report_not_saved">Could not save crash report to disk.</string>
 	<string name="ongoing_notification_title">Signed into Briar</string>
 	<string name="ongoing_notification_text">Touch to show the dashboard.</string>
 	<string name="setup_title">Briar Setup</string>
@@ -173,6 +175,8 @@
 	<string name="dialog_message_connect_panic_app">Are you sure that you want to allow %1$s to trigger destructive panic button actions?</string>
 	<string name="dialog_title_welcome">Welcome to Briar</string>
 	<string name="dialog_welcome_message">Add a contact to start communicating securely or press the icon in the upper left corner of the screen for more options.</string>
+	<string name="dialog_title_share_crash_report">Send to developers?</string>
+	<string name="dialog_message_share_crash_report">Would you like to send this crash report to the developers? It will be stored encrypted on your device until the next time you log into Briar, and then sent securely to the developers.</string>
 	<string name="dialog_button_ok">OK</string>
 	<string name="dialog_button_introduce">Introduce</string>
 	<string name="dialog_button_accept">Accept</string>
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index 5b73d5e53bb1e57ab287a93ecebe04a9a5e3a414..a3ced844d27f26695e63a5a43675bcd25b64d0c0 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -37,6 +37,8 @@ import dagger.Component;
 })
 public interface AndroidComponent extends CoreEagerSingletons {
 
+	void inject(CrashReportActivity crashReportActivity);
+
 	void inject(SplashScreenActivity activity);
 
 	void inject(SetupActivity activity);
diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java
index 066b0d1085d6261be83d6d8f4b9fa9235e72aacc..e412c9b5bca509ad11613f15fd22a24c25ca1bc8 100644
--- a/briar-android/src/org/briarproject/android/AppModule.java
+++ b/briar-android/src/org/briarproject/android/AppModule.java
@@ -8,7 +8,9 @@ import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DatabaseConfig;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.reporting.DevConfig;
 import org.briarproject.api.ui.UiCallback;
+import org.briarproject.util.StringUtils;
 
 import java.io.File;
 
@@ -90,6 +92,33 @@ public class AppModule {
 		};
 	}
 
+	@Provides
+	@Singleton
+	public DevConfig provideDevConfig() {
+		return new DevConfig() {
+
+			// TODO fill in
+			private final byte[] DEV_PUB_KEY = StringUtils.fromHexString("");
+			private final String DEV_ONION = "";
+			private final int DEV_REPORT_PORT = 8080;
+
+			@Override
+			public byte[] getDevPublicKey() {
+				return DEV_PUB_KEY;
+			}
+
+			@Override
+			public String getDevOnionAddress() {
+				return DEV_ONION;
+			}
+
+			@Override
+			public int getDevReportPort() {
+				return DEV_REPORT_PORT;
+			}
+		};
+	}
+
 	@Provides
 	@Singleton
 	ReferenceManager provideReferenceManager() {
diff --git a/briar-android/src/org/briarproject/android/CrashReportActivity.java b/briar-android/src/org/briarproject/android/CrashReportActivity.java
index 881977c325ca36deaaedd82013675db4c4c68876..8e5fe4c8c5e5806f49d9add570c967614cd18530 100644
--- a/briar-android/src/org/briarproject/android/CrashReportActivity.java
+++ b/briar-android/src/org/briarproject/android/CrashReportActivity.java
@@ -2,7 +2,9 @@ package org.briarproject.android;
 
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
+import android.app.AlertDialog;
 import android.bluetooth.BluetoothAdapter;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
@@ -18,12 +20,15 @@ import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.LinearLayout;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.util.AndroidUtils;
+import org.briarproject.api.reporting.DevReporter;
 import org.briarproject.util.StringUtils;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -36,12 +41,10 @@ import java.util.Scanner;
 import java.util.logging.Logger;
 import java.util.regex.Pattern;
 
+import javax.inject.Inject;
+
 import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
 import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
-import static android.content.Intent.ACTION_SEND;
-import static android.content.Intent.EXTRA_EMAIL;
-import static android.content.Intent.EXTRA_SUBJECT;
-import static android.content.Intent.EXTRA_TEXT;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
@@ -59,6 +62,9 @@ public class CrashReportActivity extends AppCompatActivity
 	private LinearLayout status = null;
 	private View progress = null;
 
+	@Inject
+	protected DevReporter reporter;
+
 	private volatile String stack = null;
 	private volatile int pid = -1;
 	private volatile BluetoothAdapter bt = null;
@@ -68,6 +74,9 @@ public class CrashReportActivity extends AppCompatActivity
 		super.onCreate(state);
 		setContentView(R.layout.activity_crash);
 
+		((BriarApplication) getApplication()).getApplicationComponent()
+				.inject(this);
+
 		status = (LinearLayout) findViewById(R.id.crash_status);
 		progress = findViewById(R.id.progress_wheel);
 
@@ -94,7 +103,20 @@ public class CrashReportActivity extends AppCompatActivity
 	}
 
 	public void onClick(View view) {
-		share();
+		// TODO Encapsulate the dialog in a re-usable fragment
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(R.string.dialog_title_share_crash_report);
+		builder.setMessage(R.string.dialog_message_share_crash_report);
+		builder.setNegativeButton(R.string.cancel_button, null);
+		builder.setPositiveButton(R.string.send,
+				new DialogInterface.OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						saveCrashReport();
+					}
+				});
+		AlertDialog dialog = builder.create();
+		dialog.show();
 	}
 
 	private void refresh() {
@@ -324,7 +346,7 @@ public class CrashReportActivity extends AppCompatActivity
 		return Character.toUpperCase(first) + s.substring(1);
 	}
 
-	private void share() {
+	private void saveCrashReport() {
 		StringBuilder s = new StringBuilder();
 		for (Entry<String, String> e : getStatusMap().entrySet()) {
 			s.append(e.getKey());
@@ -332,12 +354,19 @@ public class CrashReportActivity extends AppCompatActivity
 			s.append(e.getValue());
 			s.append("\n\n");
 		}
-		String body = s.toString();
-		Intent i = new Intent(ACTION_SEND);
-		i.setType("message/rfc822");
-		i.putExtra(EXTRA_EMAIL, new String[] { "contact@briarproject.org" });
-		i.putExtra(EXTRA_SUBJECT, "Crash report");
-		i.putExtra(EXTRA_TEXT, body);
-		startActivity(Intent.createChooser(i, "Send to developers"));
+		final String crashReport = s.toString();
+		try {
+			reporter.encryptCrashReportToFile(
+					AndroidUtils.getCrashReportDir(this), crashReport);
+			Toast.makeText(this, R.string.crash_report_saved, Toast.LENGTH_LONG)
+					.show();
+			finish();
+		} catch (FileNotFoundException e) {
+			if (LOG.isLoggable(WARNING))
+				LOG.log(WARNING, "Error while saving encrypted crash report",
+						e);
+			Toast.makeText(this, R.string.crash_report_not_saved,
+					Toast.LENGTH_SHORT).show();
+		}
 	}
 }
diff --git a/briar-android/src/org/briarproject/android/util/AndroidUtils.java b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
index 005b20eb9b7d1aa8cb955b772d225c8143b65b47..155e50e2b756759e6fc6a1cde6ee30935ec0ae4f 100644
--- a/briar-android/src/org/briarproject/android/util/AndroidUtils.java
+++ b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
@@ -17,11 +17,15 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
+import static android.content.Context.MODE_PRIVATE;
+
 public class AndroidUtils {
 
 	// Fake Bluetooth address returned by BluetoothAdapter on API 23 and later
 	private static final String FAKE_BLUETOOTH_ADDRESS = "02:00:00:00:00:00";
 
+	private static final String STORED_CRASH_REPORTS = "crash-reports";
+
 	@SuppressLint("NewApi")
 	@SuppressWarnings("deprecation")
 	public static Collection<String> getSupportedArchitectures() {
@@ -84,4 +88,8 @@ public class AndroidUtils {
 			}
 		}
 	}
+
+	public static File getCrashReportDir(Context ctx) {
+		return ctx.getDir(STORED_CRASH_REPORTS, MODE_PRIVATE);
+	}
 }
diff --git a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
index feccd77ff41d90df68d1e644826213798da9024d..4ebf3886e5d60bf67cbdb8a2480029902fa6d638 100644
--- a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
+++ b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
@@ -10,6 +10,7 @@ import org.briarproject.api.plugins.BackoffFactory;
 import org.briarproject.api.plugins.PluginConfig;
 import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
 import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
+import org.briarproject.api.reporting.DevReporter;
 import org.briarproject.api.system.LocationUtils;
 import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory;
 import org.briarproject.plugins.tcp.AndroidLanTcpPluginFactory;
@@ -31,12 +32,13 @@ public class AndroidPluginsModule {
 	public PluginConfig providePluginConfig(@IoExecutor Executor ioExecutor,
 			AndroidExecutor androidExecutor,
 			SecureRandom random, BackoffFactory backoffFactory, Application app,
-			LocationUtils locationUtils, EventBus eventBus) {
+			LocationUtils locationUtils, DevReporter reporter,
+			EventBus eventBus) {
 		Context appContext = app.getApplicationContext();
 		DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor,
 				androidExecutor, appContext, random, backoffFactory);
 		DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, appContext,
-				locationUtils, eventBus);
+				locationUtils, reporter, eventBus);
 		DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
 				backoffFactory, appContext);
 		final Collection<DuplexPluginFactory> duplex =
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
index d3dbff7932243505f43d9426debf3bcec0f597b4..376a60ef59e75d74d1403625d6023dca7b8dd022 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
@@ -13,6 +13,7 @@ import net.freehaven.tor.control.TorControlConnection;
 import net.sourceforge.jsocks.socks.Socks5Proxy;
 import net.sourceforge.jsocks.socks.SocksSocket;
 
+import org.briarproject.android.util.AndroidUtils;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
@@ -25,6 +26,7 @@ import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 import org.briarproject.api.properties.TransportProperties;
+import org.briarproject.api.reporting.DevReporter;
 import org.briarproject.api.settings.Settings;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.system.LocationUtils;
@@ -83,6 +85,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 	private final Executor ioExecutor;
 	private final Context appContext;
 	private final LocationUtils locationUtils;
+	private final DevReporter reporter;
 	private final Clock clock;
 	private final DuplexPluginCallback callback;
 	private final String architecture;
@@ -104,12 +107,13 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 	private volatile BroadcastReceiver networkStateReceiver = null;
 
 	TorPlugin(Executor ioExecutor, Context appContext,
-			LocationUtils locationUtils, Clock clock,
+			LocationUtils locationUtils, DevReporter reporter, Clock clock,
 			DuplexPluginCallback callback, String architecture, int maxLatency,
 			int maxIdleTime, int pollingInterval) {
 		this.ioExecutor = ioExecutor;
 		this.appContext = appContext;
 		this.locationUtils = locationUtils;
+		this.reporter = reporter;
 		this.clock = clock;
 		this.callback = callback;
 		this.architecture = architecture;
@@ -172,13 +176,14 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 			String torPath = torFile.getAbsolutePath();
 			String configPath = configFile.getAbsolutePath();
 			String pid = String.valueOf(android.os.Process.myPid());
-			String[] cmd = { torPath, "-f", configPath, OWNER, pid };
-			String[] env = { "HOME=" + torDirectory.getAbsolutePath() };
+			String[] cmd = {torPath, "-f", configPath, OWNER, pid};
+			String[] env = {"HOME=" + torDirectory.getAbsolutePath()};
 			Process torProcess;
 			try {
 				torProcess = Runtime.getRuntime().exec(cmd, env, torDirectory);
 			} catch (SecurityException e1) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e1.toString(), e1);
+				if (LOG.isLoggable(WARNING))
+					LOG.log(WARNING, e1.toString(), e1);
 				return false;
 			}
 			// Log the process's standard output until it detaches
@@ -225,6 +230,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 			if (phase != null && phase.contains("PROGRESS=100")) {
 				LOG.info("Tor has already bootstrapped");
 				bootstrapped = true;
+				sendCrashReports();
 			}
 		}
 		// Register to receive network status events
@@ -355,6 +361,16 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 		}
 	}
 
+	private void sendCrashReports() {
+		ioExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				reporter.sendCrashReports(
+						AndroidUtils.getCrashReportDir(appContext), SOCKS_PORT);
+			}
+		});
+	}
+
 	private void bind() {
 		ioExecutor.execute(new Runnable() {
 			public void run() {
@@ -420,7 +436,8 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 				obs.startWatching();
 				// Use the control connection to update the Tor config
 				List<String> config = Arrays.asList(
-						"HiddenServiceDir " + serviceDirectory.getAbsolutePath(),
+						"HiddenServiceDir " +
+								serviceDirectory.getAbsolutePath(),
 						"HiddenServicePort 80 127.0.0.1:" + port);
 				controlConnection.setConf(config);
 				controlConnection.saveConf();
@@ -593,20 +610,24 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 		}
 	}
 
-	public void streamStatus(String status, String id, String target) {}
+	public void streamStatus(String status, String id, String target) {
+	}
 
 	public void orConnStatus(String status, String orName) {
 		if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status);
 	}
 
-	public void bandwidthUsed(long read, long written) {}
+	public void bandwidthUsed(long read, long written) {
+	}
 
-	public void newDescriptors(List<String> orList) {}
+	public void newDescriptors(List<String> orList) {
+	}
 
 	public void message(String severity, String msg) {
 		if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
 		if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) {
 			bootstrapped = true;
+			sendCrashReports();
 			if (isRunning()) callback.transportEnabled();
 		}
 	}
@@ -669,7 +690,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 					} else if (blocked) {
 						LOG.info("Disabling network, country is blocked");
 						enableNetwork(false);
-					} else if (wifiOnly & !connectedToWifi){
+					} else if (wifiOnly & !connectedToWifi) {
 						LOG.info("Disabling network due to wifi setting");
 						enableNetwork(false);
 					} else {
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java b/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java
index 836dd48b61ff0aace079062dd32024d899762307..edf0103da9fed1cb4ad300377d1044aad3097eee 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java
@@ -9,6 +9,7 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
 import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
+import org.briarproject.api.reporting.DevReporter;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.system.LocationUtils;
 import org.briarproject.system.SystemClock;
@@ -28,14 +29,17 @@ public class TorPluginFactory implements DuplexPluginFactory {
 	private final Executor ioExecutor;
 	private final Context appContext;
 	private final LocationUtils locationUtils;
+	private final DevReporter reporter;
 	private final EventBus eventBus;
 	private final Clock clock;
 
 	public TorPluginFactory(Executor ioExecutor, Context appContext,
-			LocationUtils locationUtils, EventBus eventBus) {
+			LocationUtils locationUtils, DevReporter reporter,
+			EventBus eventBus) {
 		this.ioExecutor = ioExecutor;
 		this.appContext = appContext;
 		this.locationUtils = locationUtils;
+		this.reporter = reporter;
 		this.eventBus = eventBus;
 		clock = new SystemClock();
 	}
@@ -68,9 +72,10 @@ public class TorPluginFactory implements DuplexPluginFactory {
 		// Use position-independent executable for SDK >= 16
 		if (Build.VERSION.SDK_INT >= 16) architecture += "-pie";
 
-		TorPlugin plugin = new TorPlugin(ioExecutor, appContext, locationUtils,
-				clock, callback, architecture, MAX_LATENCY, MAX_IDLE_TIME,
-				POLLING_INTERVAL);
+		TorPlugin plugin =
+				new TorPlugin(ioExecutor, appContext, locationUtils, reporter,
+						clock, callback, architecture, MAX_LATENCY,
+						MAX_IDLE_TIME, POLLING_INTERVAL);
 		eventBus.addListener(plugin);
 		return plugin;
 	}
diff --git a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
index 11631f22fda6db0351682106a9f0828d2c02e20f..5f72ae3120a1a5cdc00ea0845fa449d74c466a6c 100644
--- a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
+++ b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
@@ -3,6 +3,7 @@ package org.briarproject.api.crypto;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.transport.TransportKeys;
 
+import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 
@@ -156,4 +157,9 @@ public interface CryptoComponent {
 	 * authenticated (for example, if the password is wrong).
 	 */
 	byte[] decryptWithPassword(byte[] ciphertext, String password);
+
+	/**
+	 * Encrypts the given plaintext to the given public key.
+	 */
+	String encryptToKey(byte[] publicKey, byte[] plaintext);
 }
diff --git a/briar-api/src/org/briarproject/api/reporting/DevConfig.java b/briar-api/src/org/briarproject/api/reporting/DevConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..4152cd927724a3d049d72d58ecc199f17d8522fd
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/reporting/DevConfig.java
@@ -0,0 +1,10 @@
+package org.briarproject.api.reporting;
+
+public interface DevConfig {
+
+	byte[] getDevPublicKey();
+
+	String getDevOnionAddress();
+
+	int getDevReportPort();
+}
diff --git a/briar-api/src/org/briarproject/api/reporting/DevReporter.java b/briar-api/src/org/briarproject/api/reporting/DevReporter.java
new file mode 100644
index 0000000000000000000000000000000000000000..a2172b0f9d6a35299db7d5b39945dab952981dad
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/reporting/DevReporter.java
@@ -0,0 +1,28 @@
+package org.briarproject.api.reporting;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/**
+ * A task for reporting back to the developers.
+ */
+public interface DevReporter {
+
+	/**
+	 * Store a crash report encrypted on-disk to be sent later.
+	 *
+	 * @param crashReportDir the directory where crash reports are stored.
+	 * @param crashReport    the crash report in the form expected by the server.
+	 * @throws FileNotFoundException if the report could not be written.
+	 */
+	void encryptCrashReportToFile(File crashReportDir, String crashReport)
+			throws FileNotFoundException;
+
+	/**
+	 * Send crash reports previously stored on-disk.
+	 *
+	 * @param crashReportDir the directory where crash reports are stored.
+	 * @param socksPort      the SOCKS port of a Tor client.
+	 */
+	void sendCrashReports(File crashReportDir, int socksPort);
+}
diff --git a/briar-android/libs/jsocks.jar b/briar-core/libs/jsocks.jar
similarity index 100%
rename from briar-android/libs/jsocks.jar
rename to briar-core/libs/jsocks.jar
diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java
index 14f5e4a55cf331d3e83cfa13d0d90363abd7b921..62828dfffb6a351b752e18ace8581f6e6bcdf0ab 100644
--- a/briar-core/src/org/briarproject/CoreModule.java
+++ b/briar-core/src/org/briarproject/CoreModule.java
@@ -17,6 +17,7 @@ import org.briarproject.messaging.MessagingModule;
 import org.briarproject.plugins.PluginsModule;
 import org.briarproject.properties.PropertiesModule;
 import org.briarproject.reliability.ReliabilityModule;
+import org.briarproject.reporting.ReportingModule;
 import org.briarproject.settings.SettingsModule;
 import org.briarproject.sync.SyncModule;
 import org.briarproject.system.SystemModule;
@@ -42,6 +43,7 @@ import dagger.Module;
 		PluginsModule.class,
 		PropertiesModule.class,
 		ReliabilityModule.class,
+		ReportingModule.class,
 		SettingsModule.class,
 		SyncModule.class,
 		SystemModule.class,
diff --git a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
index bdf74dfaa881a6143b06a434ac9c6dcd173dc297..87a65ef649042ab83a83d4b1bb91d8490590add4 100644
--- a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
+++ b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
@@ -18,6 +18,7 @@ import org.briarproject.util.ByteUtils;
 import org.briarproject.util.StringUtils;
 import org.spongycastle.crypto.AsymmetricCipherKeyPair;
 import org.spongycastle.crypto.CipherParameters;
+import org.spongycastle.crypto.CryptoException;
 import org.spongycastle.crypto.Digest;
 import org.spongycastle.crypto.agreement.ECDHCBasicAgreement;
 import org.spongycastle.crypto.digests.SHA256Digest;
@@ -28,6 +29,7 @@ import org.spongycastle.crypto.params.ECPrivateKeyParameters;
 import org.spongycastle.crypto.params.ECPublicKeyParameters;
 import org.spongycastle.crypto.params.KeyParameter;
 
+import java.io.IOException;
 import java.nio.charset.Charset;
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
@@ -438,6 +440,18 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
+	public String encryptToKey(byte[] publicKey, byte[] plaintext) {
+		MessageEncrypter encrypter = new MessageEncrypter(secureRandom);
+		try {
+			byte[] ciphertext = encrypter.encrypt(publicKey, plaintext);
+			return AsciiArmour.wrap(ciphertext, 70);
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		} catch (CryptoException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
 	// Key derivation function based on a pseudo-random function - see
 	// NIST SP 800-108, section 5.1
 	private byte[] macKdf(SecretKey key, byte[]... inputs) {
diff --git a/briar-core/src/org/briarproject/crypto/MessageEncrypter.java b/briar-core/src/org/briarproject/crypto/MessageEncrypter.java
index a92eed0abd568987fdb06f7f2c9b630932c48b8f..5453bcd2f6f3ddcb0a5c79a677e5e77b41756c22 100644
--- a/briar-core/src/org/briarproject/crypto/MessageEncrypter.java
+++ b/briar-core/src/org/briarproject/crypto/MessageEncrypter.java
@@ -70,6 +70,14 @@ public class MessageEncrypter {
 		return generator.generateKeyPair();
 	}
 
+	byte[] encrypt(byte[] keyBytes, byte[] plaintext)
+			throws IOException, CryptoException {
+		InputStream in = new ByteArrayInputStream(keyBytes);
+		ECPublicKeyParameters publicKey =
+				(ECPublicKeyParameters) parser.readKey(in);
+		return encrypt(publicKey, plaintext);
+	}
+
 	byte[] encrypt(ECPublicKeyParameters pubKey, byte[] plaintext)
 			throws CryptoException {
 		IESEngine engine = getEngine();
@@ -159,10 +167,7 @@ public class MessageEncrypter {
 			}
 			// Encrypt a decrypted message
 			InputStream in = new FileInputStream(args[1]);
-			byte[] b = StringUtils.fromHexString(readFully(in).trim());
-			in = new ByteArrayInputStream(b);
-			ECPublicKeyParameters publicKey =
-					(ECPublicKeyParameters) encrypter.parser.readKey(in);
+			byte[] publicKey = StringUtils.fromHexString(readFully(in).trim());
 			String message = readFully(System.in);
 			byte[] plaintext = message.getBytes(Charset.forName("UTF-8"));
 			byte[] ciphertext = encrypter.encrypt(publicKey, plaintext);
diff --git a/briar-core/src/org/briarproject/reporting/DevReporterImpl.java b/briar-core/src/org/briarproject/reporting/DevReporterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..d34bbeeeffe577125fba20749b282b33dcc787ed
--- /dev/null
+++ b/briar-core/src/org/briarproject/reporting/DevReporterImpl.java
@@ -0,0 +1,124 @@
+package org.briarproject.reporting;
+
+import com.google.common.io.Files;
+
+import net.sourceforge.jsocks.socks.Socks5Proxy;
+import net.sourceforge.jsocks.socks.SocksException;
+import net.sourceforge.jsocks.socks.SocksSocket;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.reporting.DevConfig;
+import org.briarproject.api.reporting.DevReporter;
+import org.briarproject.util.StringUtils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.util.Date;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+
+class DevReporterImpl implements DevReporter {
+
+	private static final Logger LOG =
+			Logger.getLogger(DevReporterImpl.class.getName());
+
+	private static final int TIMEOUT = 30 * 1000; // 30 seconds
+	private static final String PREFIX = "briar-";
+	private static final String REPORT_EXT = ".report";
+	private static final String CRLF = "\r\n";
+
+	private CryptoComponent crypto;
+	private DevConfig devConfig;
+
+	public DevReporterImpl(CryptoComponent crypto, DevConfig devConfig) {
+		this.crypto = crypto;
+		this.devConfig = devConfig;
+	}
+
+	private Socket connectToDevelopers(int socksPort, int devPort)
+			throws UnknownHostException, SocksException, SocketException {
+		Socks5Proxy proxy = new Socks5Proxy("127.0.0.1", socksPort);
+		proxy.resolveAddrLocally(false);
+		Socket s =
+				new SocksSocket(proxy, devConfig.getDevOnionAddress(), devPort);
+		s.setSoTimeout(TIMEOUT);
+		return s;
+	}
+
+	@Override
+	public void encryptCrashReportToFile(File crashReportDir,
+			String crashReport) throws FileNotFoundException {
+		String encryptedReport =
+				crypto.encryptToKey(devConfig.getDevPublicKey(),
+						StringUtils.toUtf8(crashReport));
+
+		String filename = PREFIX + new Date().getTime() + REPORT_EXT;
+		File report = new File(crashReportDir, filename);
+		PrintWriter writer = null;
+		try {
+			writer = new PrintWriter(
+					new OutputStreamWriter(new FileOutputStream(report)));
+			writer.append(encryptedReport);
+			writer.flush();
+		} finally {
+			if (writer != null)
+				writer.close();
+		}
+	}
+
+	@Override
+	public void sendCrashReports(File crashReportDir, int socksPort) {
+		File[] reports = crashReportDir.listFiles();
+		if (reports == null || reports.length == 0)
+			return; // No crash reports to send
+
+		LOG.info("Connecting to developers' Hidden Service");
+		Socket s;
+		try {
+			s = connectToDevelopers(socksPort,
+					devConfig.getDevReportPort());
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING))
+				LOG.log(WARNING, "Tor SOCKS proxy failed", e);
+			return;
+		}
+
+		LOG.info("Sending crash reports to developers");
+		OutputStream output;
+		PrintWriter writer = null;
+		try {
+			output = s.getOutputStream();
+			writer = new PrintWriter(
+					new OutputStreamWriter(output, "UTF-8"), true);
+			for (File f : reports) {
+				List<String> encryptedReport = Files.readLines(f,
+						Charset.forName("UTF-8"));
+				writer.append(f.getName()).append(CRLF);
+				for (String line : encryptedReport) {
+					writer.append(line).append(CRLF);
+				}
+				writer.append(CRLF);
+				f.delete();
+			}
+			writer.flush();
+			LOG.info("Crash reports sent");
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING))
+				LOG.log(WARNING, "Connection to developers failed", e);
+		} finally {
+			if (writer != null)
+				writer.close();
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/reporting/ReportingModule.java b/briar-core/src/org/briarproject/reporting/ReportingModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c88261de3faae5b3d535d2683e0de242b1c7446
--- /dev/null
+++ b/briar-core/src/org/briarproject/reporting/ReportingModule.java
@@ -0,0 +1,18 @@
+package org.briarproject.reporting;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.reporting.DevConfig;
+import org.briarproject.api.reporting.DevReporter;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class ReportingModule {
+
+	@Provides
+	DevReporter provideDevReportTask(CryptoComponent crypto,
+			DevConfig devConfig) {
+		return new DevReporterImpl(crypto, devConfig);
+	}
+}