diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a4978c99cf79534b07d606ca9f565378e34c81da
--- /dev/null
+++ b/.github/workflows/checks.yaml
@@ -0,0 +1,33 @@
+name: Onion Wrapper CI
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+jobs:
+  build-linux:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+    - name: Setup Java
+      uses: actions/setup-java@v3
+      with:
+        distribution: 'temurin'
+        java-version: '17'
+    - name: Run Gradle tests
+      run: ./gradlew check --info --stacktrace
+  build-windows:
+    runs-on: windows-latest
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+    - name: Setup Java
+      uses: actions/setup-java@v3
+      with:
+        distribution: 'temurin'
+        java-version: '17'
+    - name: Run Gradle tests
+      run: ./gradlew check --info --stacktrace
diff --git a/.gitignore b/.gitignore
index 68aed59f6e8ddcf6deb0ac5a66a29a48f99ca659..0871d79b7155f4213f87868b9cfca5698bfbbc51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
 !/.idea/codeStyles
 .DS_Store
 /build
+/buildSrc/build
 /captures
 .externalNativeBuild
 .cxx
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a8370a04bfddd4c01bd26a22a4d65492aee4643f
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,161 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <option name="SOFT_MARGINS" value="80,100" />
+    <JavaCodeStyleSettings>
+      <option name="IMPORT_LAYOUT_TABLE">
+        <value>
+          <package name="android" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="com" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="junit" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="net" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="org" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="java" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="javax" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="" withSubpackages="true" static="true" />
+          <emptyLine />
+        </value>
+      </option>
+      <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
+      <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
+      <option name="JD_INDENT_ON_CONTINUATION" value="true" />
+    </JavaCodeStyleSettings>
+    <JetCodeStyleSettings>
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </JetCodeStyleSettings>
+    <codeStyleSettings language="JAVA">
+      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+      <option name="METHOD_CALL_CHAIN_WRAP" value="1" />
+      <option name="WRAP_LONG_LINES" value="true" />
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="XML">
+      <indentOptions>
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+      <arrangement>
+        <rules>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:android</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:id</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>style</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>ANDROID_ATTRIBUTE_ORDER</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>.*</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+        </rules>
+      </arrangement>
+    </codeStyleSettings>
+    <codeStyleSettings language="kotlin">
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </codeStyleSettings>
+  </code_scheme>
+</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000000000000000000000000000000000000..79ee123c2b23e069e35ed634d687e17f731cc702
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>
\ No newline at end of file
diff --git a/buildSrc/src/main/java/org/briarproject/onionwrapper/OS.java b/buildSrc/src/main/java/org/briarproject/onionwrapper/OS.java
new file mode 100644
index 0000000000000000000000000000000000000000..cc48f22bff21d16296fe62f263b62cc2cc5747b2
--- /dev/null
+++ b/buildSrc/src/main/java/org/briarproject/onionwrapper/OS.java
@@ -0,0 +1,17 @@
+package org.briarproject.onionwrapper;
+
+public enum OS {
+	Linux("linux"),
+	Windows("windows"),
+	MacOS("macos");
+
+	private String id;
+
+	OS(String id) {
+		this.id = id;
+	}
+
+	public String getId() {
+		return id;
+	}
+}
diff --git a/buildSrc/src/main/java/org/briarproject/onionwrapper/OsUtils.java b/buildSrc/src/main/java/org/briarproject/onionwrapper/OsUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..5b4ee27d11bf74127bb3dbfc6ee62f78f9e28b85
--- /dev/null
+++ b/buildSrc/src/main/java/org/briarproject/onionwrapper/OsUtils.java
@@ -0,0 +1,21 @@
+package org.briarproject.onionwrapper;
+
+import static org.briarproject.onionwrapper.StringUtils.startsWithIgnoreCase;
+
+public class OsUtils {
+
+	public static final OS currentOS;
+
+	static {
+		String os = System.getProperty("os.name");
+		if (os.equalsIgnoreCase("Mac OS X")) {
+			currentOS = OS.MacOS;
+		} else if (startsWithIgnoreCase(os, "Win")) {
+			currentOS = OS.Windows;
+		} else if (startsWithIgnoreCase(os, "Linux")) {
+			currentOS = OS.Linux;
+		} else {
+			throw new AssertionError("Unknown OS name: " + os);
+		}
+	}
+}
diff --git a/buildSrc/src/main/java/org/briarproject/onionwrapper/StringUtils.java b/buildSrc/src/main/java/org/briarproject/onionwrapper/StringUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a9b5db89dc5fbb1183c311b5b4656d4f8746814
--- /dev/null
+++ b/buildSrc/src/main/java/org/briarproject/onionwrapper/StringUtils.java
@@ -0,0 +1,9 @@
+package org.briarproject.onionwrapper;
+
+public class StringUtils {
+
+	// see https://stackoverflow.com/a/38947571
+	static boolean startsWithIgnoreCase(String s, String prefix) {
+		return s.regionMatches(true, 0, prefix, 0, prefix.length());
+	}
+}
diff --git a/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/StringUtils.java b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/StringUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd03d2f2e05b651336e136e08b9f15dd49934bfd
--- /dev/null
+++ b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/StringUtils.java
@@ -0,0 +1,12 @@
+package org.briarproject.onionwrapper;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+public class StringUtils {
+
+	// see https://stackoverflow.com/a/38947571
+	static boolean startsWithIgnoreCase(String s, String prefix) {
+		return s.regionMatches(true, 0, prefix, 0, prefix.length());
+	}
+}
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 d7b8afa57cdc85d007dbc284295c0bf48d194cff..2c6b65e1e62a466edef7e5223ce8691d7983f1a5 100644
--- a/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
+++ b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
@@ -12,6 +12,7 @@ import javax.annotation.concurrent.ThreadSafe;
 import static java.util.Arrays.asList;
 import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
+import static org.briarproject.onionwrapper.StringUtils.startsWithIgnoreCase;
 
 @ThreadSafe
 @NotNullByDefault
@@ -58,6 +59,16 @@ public class TestUtils {
 		return os != null && os.contains("Linux");
 	}
 
+	public static boolean isWindows() {
+		String os = System.getProperty("os.name");
+		return os != null && startsWithIgnoreCase(os, "Win");
+	}
+
+	public static boolean isMac() {
+		String os = System.getProperty("os.name");
+		return os != null && os.equalsIgnoreCase("Mac OS X");
+	}
+
 	@Nullable
 	public static String getArchitectureForTorBinary() {
 		String arch = System.getProperty("os.arch");
diff --git a/onionwrapper-java/build.gradle b/onionwrapper-java/build.gradle
index 1df23d95103b027d9d1ad27815fbfd779cea60e0..2f7e3f22a206d943289bdb1805a85d0caf3451d4 100644
--- a/onionwrapper-java/build.gradle
+++ b/onionwrapper-java/build.gradle
@@ -1,3 +1,7 @@
+import static org.briarproject.onionwrapper.OS.Linux
+import static org.briarproject.onionwrapper.OS.Windows
+import static org.briarproject.onionwrapper.OsUtils.currentOS
+
 plugins {
     id 'java-library'
     id 'com.vanniktech.maven.publish' version '0.18.0'
@@ -16,9 +20,11 @@ dependencies {
 
     testImplementation project(path: ':onionwrapper-core', configuration: 'testOutput')
     testImplementation 'junit:junit:4.13.2'
-    testImplementation 'org.briarproject:tor-linux:0.4.7.13-2'
-    testImplementation 'org.briarproject:obfs4proxy-linux:0.0.14-tor2'
-    testImplementation 'org.briarproject:snowflake-linux:2.5.1'
+    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"
+    }
 }
 
 mavenPublishing {
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 6681b05abfe62b7e232cc248ba4725bf4f2fa6dd..516b1ec9fbc962cf0a6a6e06c5d08852f013001d 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
@@ -16,6 +16,7 @@ import static org.briarproject.onionwrapper.TestUtils.deleteTestDirectory;
 import static org.briarproject.onionwrapper.TestUtils.getArchitectureForTorBinary;
 import static org.briarproject.onionwrapper.TestUtils.getTestDirectory;
 import static org.briarproject.onionwrapper.TestUtils.isLinux;
+import static org.briarproject.onionwrapper.TestUtils.isWindows;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.CONNECTED;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeNotNull;
@@ -34,7 +35,7 @@ public class BootstrapTest extends BaseTest {
 
 	@Before
 	public void setUp() {
-		assumeTrue(isLinux());
+		assumeTrue(isLinux() || isWindows());
 		assumeNotNull(getArchitectureForTorBinary());
 	}
 
@@ -47,8 +48,16 @@ public class BootstrapTest extends BaseTest {
 	@Test
 	public void testBootstrapping() throws Exception {
 		String architecture = requireNonNull(getArchitectureForTorBinary());
-		TorWrapper tor = new UnixTorWrapper(executor, executor, architecture, torDir,
-				CONTROL_PORT, SOCKS_PORT);
+		TorWrapper tor;
+		if (isLinux()) {
+			tor = new UnixTorWrapper(executor, executor, architecture, torDir,
+					CONTROL_PORT, SOCKS_PORT);
+		} else if (isWindows()) {
+			tor = new WindowsTorWrapper(executor, executor, architecture, torDir,
+					CONTROL_PORT, SOCKS_PORT);
+		} else {
+			throw new AssertionError("Running on unsupported OS");
+		}
 
 		boolean connected;
 		try {
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesLinuxTest.java
similarity index 95%
rename from onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesTest.java
rename to onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesLinuxTest.java
index 084070be56f90d0bb21ae9b152bebf0d0f117dc5..7ebbf50a754d7c948e64041bb33dc630f005f6fb 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesLinuxTest.java
@@ -7,7 +7,7 @@ import static org.briarproject.onionwrapper.TestUtils.isLinux;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assume.assumeTrue;
 
-public class ResourcesTest {
+public class ResourcesLinuxTest {
 
 	@Before
 	public void setUp() {
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesWindowsTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesWindowsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..01a013786170089f6af09618edeb3e3c63c38fa6
--- /dev/null
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesWindowsTest.java
@@ -0,0 +1,36 @@
+package org.briarproject.onionwrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.briarproject.onionwrapper.TestUtils.isWindows;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+public class ResourcesWindowsTest {
+
+	@Before
+	public void setUp() {
+		assumeTrue(isWindows());
+	}
+
+	@Test
+	public void testCanLoadTor() {
+		testCanLoadResource("x86_64/tor.exe");
+	}
+
+	@Test
+	public void testCanLoadObfs4() {
+		testCanLoadResource("x86_64/obfs4proxy.exe");
+	}
+
+	@Test
+	public void testCanLoadSnowflake() {
+		testCanLoadResource("x86_64/snowflake.exe");
+	}
+
+	private void testCanLoadResource(String name) {
+		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+		assertNotNull(classLoader.getResourceAsStream(name));
+	}
+}