diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java
index ae68db6e1c5e500601984a861e27f24a2c72ba5b..f367736152b0d494f1283eff97254c29857346b9 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java
@@ -41,11 +41,12 @@ class AndroidTorPlugin extends TorPlugin {
 			Clock clock, ResourceProvider resourceProvider,
 			CircumventionProvider circumventionProvider,
 			BatteryManager batteryManager, Backoff backoff,
+			TorRendezvousCrypto torRendezvousCrypto,
 			PluginCallback callback, String architecture, int maxLatency,
 			int maxIdleTime) {
 		super(ioExecutor, networkManager, locationUtils, torSocketFactory,
 				clock, resourceProvider, circumventionProvider, batteryManager,
-				backoff, callback, architecture, maxLatency, maxIdleTime,
+				backoff, torRendezvousCrypto, callback, architecture, maxLatency, maxIdleTime,
 				appContext.getDir("tor", MODE_PRIVATE));
 		this.appContext = appContext;
 		PowerManager pm = (PowerManager)
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java
index 560072343a52cb2d4f084248ad9f3805a29cdac8..40535497588885bbb11f2515aa01079d191f21c6 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java
@@ -106,10 +106,12 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
 
 		Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
 				MAX_POLLING_INTERVAL, BACKOFF_BASE);
+		TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl();
 		AndroidTorPlugin plugin = new AndroidTorPlugin(ioExecutor, scheduler,
 				appContext, networkManager, locationUtils, torSocketFactory,
 				clock, resourceProvider, circumventionProvider, batteryManager,
-				backoff, callback, architecture, MAX_LATENCY, MAX_IDLE_TIME);
+				backoff, torRendezvousCrypto, callback, architecture,
+				MAX_LATENCY, MAX_IDLE_TIME);
 		eventBus.addListener(plugin);
 		return plugin;
 	}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
index 76d8f6109a99bf7913cbde2809ef8cafad325862..70821a1eed5ff95592c20bc13d79c70193f98a49 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
@@ -105,6 +105,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	private final Clock clock;
 	private final BatteryManager batteryManager;
 	private final Backoff backoff;
+	private final TorRendezvousCrypto torRendezvousCrypto;
 	private final PluginCallback callback;
 	private final String architecture;
 	private final CircumventionProvider circumventionProvider;
@@ -131,6 +132,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 			Clock clock, ResourceProvider resourceProvider,
 			CircumventionProvider circumventionProvider,
 			BatteryManager batteryManager, Backoff backoff,
+			TorRendezvousCrypto torRendezvousCrypto,
 			PluginCallback callback, String architecture, int maxLatency,
 			int maxIdleTime, File torDirectory) {
 		this.ioExecutor = ioExecutor;
@@ -142,6 +144,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		this.circumventionProvider = circumventionProvider;
 		this.batteryManager = batteryManager;
 		this.backoff = backoff;
+		this.torRendezvousCrypto = torRendezvousCrypto;
 		this.callback = callback;
 		this.architecture = architecture;
 		this.maxLatency = maxLatency;
@@ -609,13 +612,58 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 
 	@Override
 	public boolean supportsRendezvous() {
-		return false;
+		return true;
 	}
 
 	@Override
 	public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
 			boolean alice, ConnectionHandler incoming) {
-		throw new UnsupportedOperationException();
+		byte[] aliceSeed = k.getKeyMaterial(32);
+		byte[] bobSeed = k.getKeyMaterial(32);
+		byte[] localSeed = alice ? aliceSeed : bobSeed;
+		byte[] remoteSeed = alice ? bobSeed : aliceSeed;
+		String blob = torRendezvousCrypto.getPrivateKeyBlob(localSeed);
+		String localOnion = torRendezvousCrypto.getOnionAddress(localSeed);
+		String remoteOnion = torRendezvousCrypto.getOnionAddress(remoteSeed);
+		TransportProperties remote = new TransportProperties();
+		remote.put(PROP_ONION_V3, remoteOnion);
+		try {
+			ServerSocket ss = new ServerSocket();
+			ss.bind(new InetSocketAddress("127.0.0.1", 0));
+			int port = ss.getLocalPort();
+			ioExecutor.execute(() -> {
+				try {
+					//noinspection InfiniteLoopStatement
+					while (true) {
+						Socket s = ss.accept();
+						incoming.handleConnection(
+								new TorTransportConnection(this, s));
+					}
+				} catch (IOException e) {
+					// This is expected when the socket is closed
+					if (LOG.isLoggable(INFO)) LOG.info(e.toString());
+				}
+			});
+			Map<Integer, String> portLines =
+					singletonMap(80, "127.0.0.1:" + port);
+			controlConnection.addOnion(blob, portLines);
+			return new RendezvousEndpoint() {
+
+				@Override
+				public TransportProperties getRemoteTransportProperties() {
+					return remote;
+				}
+
+				@Override
+				public void close() throws IOException {
+					controlConnection.delOnion(localOnion);
+					tryToClose(ss);
+				}
+			};
+		} catch (IOException e) {
+			logException(LOG, WARNING, e);
+			return null;
+		}
 	}
 
 	@Override
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorRendezvousCrypto.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorRendezvousCrypto.java
new file mode 100644
index 0000000000000000000000000000000000000000..a7c2fe9f01e3a8fa62ebbeeb7d18d95e9cf723a0
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorRendezvousCrypto.java
@@ -0,0 +1,8 @@
+package org.briarproject.bramble.plugin.tor;
+
+interface TorRendezvousCrypto {
+
+	String getOnionAddress(byte[] seed);
+
+	String getPrivateKeyBlob(byte[] seed);
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorRendezvousCryptoImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorRendezvousCryptoImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..aea31d84dc1b9f391424119c906ba0fd1a4c40fb
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorRendezvousCryptoImpl.java
@@ -0,0 +1,46 @@
+package org.briarproject.bramble.plugin.tor;
+
+import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
+import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
+import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
+
+import org.briarproject.bramble.util.Base32;
+import org.spongycastle.crypto.Digest;
+import org.spongycastle.crypto.digests.SHA3Digest;
+import org.spongycastle.util.encoders.Base64;
+
+import java.nio.charset.Charset;
+
+import static java.lang.System.arraycopy;
+
+public class TorRendezvousCryptoImpl implements TorRendezvousCrypto {
+
+	private static final EdDSANamedCurveSpec CURVE_SPEC =
+			EdDSANamedCurveTable.getByName("Ed25519");
+
+	@Override
+	public String getOnionAddress(byte[] seed) {
+		EdDSAPrivateKeySpec spec = new EdDSAPrivateKeySpec(seed, CURVE_SPEC);
+		byte[] publicKey = spec.getA().toByteArray();
+		Digest digest = new SHA3Digest(256);
+		byte[] label = ".onion checksum".getBytes(Charset.forName("US-ASCII"));
+		digest.update(label, 0, label.length);
+		digest.update(publicKey, 0, publicKey.length);
+		digest.update((byte) 3);
+		byte[] checksum = new byte[digest.getDigestSize()];
+		digest.doFinal(checksum, 0);
+		byte[] address = new byte[publicKey.length + 3];
+		arraycopy(publicKey, 0, address, 0, publicKey.length);
+		arraycopy(checksum, 0, address, publicKey.length, 2);
+		address[address.length - 1] = 3;
+		return Base32.encode(address).toLowerCase();
+	}
+
+	@Override
+	public String getPrivateKeyBlob(byte[] seed) {
+		EdDSAPrivateKeySpec spec = new EdDSAPrivateKeySpec(seed, CURVE_SPEC);
+		byte[] hash = spec.getH();
+		byte[] base64 = Base64.encode(hash);
+		return "ED25519-V3:" + new String(base64, Charset.forName("US-ASCII"));
+	}
+}
diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java
index d08300b2af61e796ad668f21482b299512bb4c1d..9b213cba7e332eff85d3a0ed887bf3a7bb386dbc 100644
--- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java
+++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java
@@ -25,12 +25,13 @@ abstract class JavaTorPlugin extends TorPlugin {
 			Clock clock, ResourceProvider resourceProvider,
 			CircumventionProvider circumventionProvider,
 			BatteryManager batteryManager, Backoff backoff,
+			TorRendezvousCrypto torRendezvousCrypto,
 			PluginCallback callback, String architecture, int maxLatency,
 			int maxIdleTime, File torDirectory) {
 		super(ioExecutor, networkManager, locationUtils, torSocketFactory,
 				clock, resourceProvider, circumventionProvider, batteryManager,
-				backoff, callback, architecture, maxLatency, maxIdleTime,
-				torDirectory);
+				backoff, torRendezvousCrypto, callback, architecture,
+				maxLatency, maxIdleTime, torDirectory);
 	}
 
 	@Override
diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java
index 753840a76b53acc27ae83da5c20e967b10473bbf..e57ea83f3e806cbb80acf77c87730628d6db13ed 100644
--- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java
+++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java
@@ -25,12 +25,13 @@ class UnixTorPlugin extends JavaTorPlugin {
 			Clock clock, ResourceProvider resourceProvider,
 			CircumventionProvider circumventionProvider,
 			BatteryManager batteryManager, Backoff backoff,
+			TorRendezvousCrypto torRendezvousCrypto,
 			PluginCallback callback, String architecture, int maxLatency,
 			int maxIdleTime, File torDirectory) {
 		super(ioExecutor, networkManager, locationUtils, torSocketFactory,
 				clock, resourceProvider, circumventionProvider, batteryManager,
-				backoff, callback, architecture, maxLatency, maxIdleTime,
-				torDirectory);
+				backoff, torRendezvousCrypto, callback, architecture,
+				maxLatency, maxIdleTime, torDirectory);
 	}
 
 	@Override
diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java
index 0e84257129a805cc4a87bf7e2f306102c9b28240..07cbead567577ad9da962c9584f08e101ee0dab8 100644
--- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java
+++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java
@@ -96,10 +96,12 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
 
 		Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
 				MAX_POLLING_INTERVAL, BACKOFF_BASE);
+		TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl();
 		UnixTorPlugin plugin = new UnixTorPlugin(ioExecutor, networkManager,
 				locationUtils, torSocketFactory, clock, resourceProvider,
-				circumventionProvider, batteryManager, backoff, callback,
-				architecture, MAX_LATENCY, MAX_IDLE_TIME, torDirectory);
+				circumventionProvider, batteryManager, backoff,
+				torRendezvousCrypto, callback, architecture, MAX_LATENCY,
+				MAX_IDLE_TIME, torDirectory);
 		eventBus.addListener(plugin);
 		return plugin;
 	}