diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
index a92b8cbcc11e29efc6950bb5162d6bb609889180..3916c221a73ffbbe79bfc5f34ff0690a5b07b639 100644
--- a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
+++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
@@ -50,7 +50,6 @@ import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTED;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTING;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPED;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPING;
-import static org.briarproject.onionwrapper.util.OsUtils.isMac;
 
 @InterfaceNotNullByDefault
 abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
@@ -73,8 +72,9 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 
 	protected final Executor ioExecutor;
 	protected final Executor eventExecutor;
-	private final String architecture;
-	private final File torDirectory, configFile, doneFile, cookieFile;
+	protected final String architecture;
+	protected final File torDirectory;
+	private final File configFile, doneFile, cookieFile;
 	private final int torSocksPort;
 	private final int torControlPort;
 
@@ -111,10 +111,6 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		return new File(torDirectory, "tor");
 	}
 
-	protected File getLibEventFile() {
-		return new File(torDirectory, "libevent-2.1.7.dylib");
-	}
-
 	@Override
 	public File getObfs4ExecutableFile() {
 		return new File(torDirectory, "obfs4proxy");
@@ -207,9 +203,6 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		//noinspection ResultOfMethodCallIgnored
 		doneFile.delete();
 		installTorExecutable();
-		if (isMac()) {
-			installLibEvent();
-		}
 		installObfs4Executable();
 		installSnowflakeExecutable();
 		extract(getConfigInputStream(), configFile);
@@ -229,31 +222,15 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 			LOG.info("Installing Tor binary for " + architecture);
 		}
 		File torFile = getTorExecutableFile();
-		// Important: delete file here and with other binaries below to prevent
-		// problems on macOS in case the file signature changed.
-		//noinspection ResultOfMethodCallIgnored
-		torFile.delete();
 		extract(getExecutableInputStream("tor"), torFile);
 		if (!torFile.setExecutable(true, true)) throw new IOException();
 	}
 
-	protected void installLibEvent() throws IOException {
-		if (LOG.isLoggable(INFO)) {
-			LOG.info("Installing libevent binary for " + architecture);
-		}
-		File libEventFile = getLibEventFile();
-		//noinspection ResultOfMethodCallIgnored
-		libEventFile.delete();
-		extract(getExecutableInputStream("libevent-2.1.7.dylib"), libEventFile);
-	}
-
 	protected void installObfs4Executable() throws IOException {
 		if (LOG.isLoggable(INFO)) {
 			LOG.info("Installing obfs4proxy binary for " + architecture);
 		}
 		File obfs4File = getObfs4ExecutableFile();
-		//noinspection ResultOfMethodCallIgnored
-		obfs4File.delete();
 		extract(getExecutableInputStream("obfs4proxy"), obfs4File);
 		if (!obfs4File.setExecutable(true, true)) throw new IOException();
 	}
@@ -263,13 +240,11 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 			LOG.info("Installing snowflake binary for " + architecture);
 		}
 		File snowflakeFile = getSnowflakeExecutableFile();
-		//noinspection ResultOfMethodCallIgnored
-		snowflakeFile.delete();
 		extract(getExecutableInputStream("snowflake"), snowflakeFile);
 		if (!snowflakeFile.setExecutable(true, true)) throw new IOException();
 	}
 
-	private InputStream getExecutableInputStream(String basename) {
+	protected InputStream getExecutableInputStream(String basename) {
 		String ext = getExecutableExtension();
 		return requireNonNull(getResourceInputStream(architecture + "/" + basename, ext));
 	}
diff --git a/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
index 02c5136e733af07b6c4cca6a9d3e4c8d6ac3f4ee..9c7d1de34473a8cc1a1c6392c53bd0cf7aeedd94 100644
--- a/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
+++ b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
@@ -60,11 +60,13 @@ public class TestUtils {
 	public static String getArchitectureForTorBinary() {
 		String arch = System.getProperty("os.arch");
 		if (arch == null) return null;
-		if (isLinux() || isWindows()) {
+		if (isLinux()) {
 			//noinspection IfCanBeSwitch
 			if (arch.equals("amd64")) return "x86_64";
 			else if (arch.equals("aarch64")) return "aarch64";
 			else if (arch.equals("arm")) return "armhf";
+		} else if (isWindows()) {
+			if (arch.equals("amd64")) return "x86_64";
 		} else if (isMac()) {
 			if (arch.equals("amd64")) return "x86_64";
 			else if (arch.equals("aarch64")) return "aarch64";
diff --git a/onionwrapper-java/build.gradle b/onionwrapper-java/build.gradle
index 3b10d4320579689c55a28b0ba4803f382fab3093..5d742636a2c8de9c021c2ca8c4652b3b73b13132 100644
--- a/onionwrapper-java/build.gradle
+++ b/onionwrapper-java/build.gradle
@@ -1,4 +1,3 @@
-import static org.briarproject.onionwrapper.OS.*
 import static org.briarproject.onionwrapper.OsUtils.currentOS
 
 plugins {
@@ -18,21 +17,15 @@ checkstyle {
 
 dependencies {
     api project(':onionwrapper-core')
-    def jna_version = '5.10.0'
+    def jna_version = '5.13.0'
     implementation "net.java.dev.jna:jna:$jna_version"
     implementation "net.java.dev.jna:jna-platform:$jna_version"
 
     testImplementation project(path: ':onionwrapper-core', configuration: 'testOutput')
     testImplementation 'junit:junit:4.13.2'
-    if (currentOS == Linux || currentOS == Windows) {
-        testImplementation "org.briarproject:tor-$currentOS.id:0.4.7.13-2"
-        testImplementation "org.briarproject:obfs4proxy-$currentOS.id:0.0.14-tor2"
-        testImplementation "org.briarproject:snowflake-$currentOS.id:2.5.1"
-    } else if (currentOS == MacOS) {
-        testImplementation "org.briarproject:tor-macos:0.4.7.13-2"
-        testImplementation "org.briarproject:obfs4proxy-macos:0.0.14-tor2"
-        testImplementation "org.briarproject:snowflake-macos:2.5.1"
-    }
+    testImplementation "org.briarproject:tor-$currentOS.id:0.4.7.13-2"
+    testImplementation "org.briarproject:obfs4proxy-$currentOS.id:0.0.14-tor2"
+    testImplementation "org.briarproject:snowflake-$currentOS.id:2.5.1"
 }
 
 mavenPublishing {
diff --git a/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/MacTorWrapper.java b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/MacTorWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..48fe817da0fa4493d658b3b9491f36d2d2ba9e23
--- /dev/null
+++ b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/MacTorWrapper.java
@@ -0,0 +1,68 @@
+package org.briarproject.onionwrapper;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.Executor;
+
+import static java.util.logging.Level.INFO;
+
+@NotNullByDefault
+public class MacTorWrapper extends UnixTorWrapper {
+
+	static final String LIB_EVENT_VERSION = "2.1.7";
+
+	/**
+	 * @param ioExecutor The wrapper will use this executor to run IO tasks,
+	 * 		some of which may run for the lifetime of the wrapper, so the executor
+	 * 		should have an unlimited thread pool.
+	 * @param eventExecutor The wrapper will use this executor to call the
+	 *        {@link Observer observer} (if any). To ensure that events are observed
+	 * 		in the order they occur, this executor should have a single thread (eg
+	 * 		the app's main thread).
+	 * @param architecture The processor architecture of the Tor and pluggable
+	 * 		transport binaries.
+	 * @param torDirectory The directory where the Tor process should keep its
+	 * 		state.
+	 * @param torSocksPort The port number to use for Tor's SOCKS port.
+	 * @param torControlPort The port number to use for Tor's control port.
+	 */
+	public MacTorWrapper(Executor ioExecutor,
+			Executor eventExecutor,
+			String architecture,
+			File torDirectory,
+			int torSocksPort,
+			int torControlPort) {
+		super(ioExecutor, eventExecutor, architecture, torDirectory, torSocksPort, torControlPort);
+	}
+
+	@Override
+	protected void installTorExecutable() throws IOException {
+		super.installTorExecutable();
+		installLibEvent();
+	}
+
+	private void installLibEvent() throws IOException {
+		if (LOG.isLoggable(INFO)) {
+			LOG.info("Installing libevent binary for " + architecture);
+		}
+		File libEventFile = getLibEventFile();
+		extract(getExecutableInputStream("libevent-" + LIB_EVENT_VERSION + ".dylib"),
+				libEventFile);
+	}
+
+	private File getLibEventFile() {
+		return new File(torDirectory, "libevent-" + LIB_EVENT_VERSION + ".dylib");
+	}
+
+	@Override
+	protected void extract(InputStream in, File dest) throws IOException {
+		// Important: delete file to prevent problems on macOS in case the file signature changed
+		// for binaries.
+		//noinspection ResultOfMethodCallIgnored
+		dest.delete();
+		super.extract(in, dest);
+	}
+}
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
index e8d9cc6ee1334841e7349396ce91f5b677a7b34a..98bb14658c83423ecb0fa4c54a27e50b7903de8c 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
@@ -53,9 +53,12 @@ public class BootstrapTest extends BaseTest {
 	public void testBootstrapping() throws Exception {
 		String architecture = requireNonNull(getArchitectureForTorBinary());
 		TorWrapper tor;
-		if (isLinux() || isMac()) {
+		if (isLinux()) {
 			tor = new UnixTorWrapper(executor, executor, architecture, torDir,
 					CONTROL_PORT, SOCKS_PORT);
+		} else if (isMac()) {
+			tor = new MacTorWrapper(executor, executor, architecture, torDir,
+					CONTROL_PORT, SOCKS_PORT);
 		} else if (isWindows()) {
 			tor = new WindowsTorWrapper(executor, executor, architecture, torDir,
 					CONTROL_PORT, SOCKS_PORT);
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesMacTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesMacTest.java
index b8bd2fccc87eea64419af7f9d4f482c693ac1233..213f2f62020e71fa57da523b6e37e97a3f3c4d84 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesMacTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesMacTest.java
@@ -3,6 +3,7 @@ package org.briarproject.onionwrapper;
 import org.junit.Before;
 import org.junit.Test;
 
+import static org.briarproject.onionwrapper.MacTorWrapper.LIB_EVENT_VERSION;
 import static org.briarproject.onionwrapper.util.OsUtils.isMac;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assume.assumeTrue;
@@ -21,7 +22,7 @@ public class ResourcesMacTest {
 
 	@Test
 	public void testCanLoadLibEvent() {
-		testCanLoadResource("x86_64/libevent-2.1.7.dylib");
+		testCanLoadResource("x86_64/libevent-" + LIB_EVENT_VERSION + ".dylib");
 	}
 
 	@Test