com.auth0
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index dfb03a6c4..33d384785 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -21,6 +21,7 @@ module org.cryptomator.desktop {
requires com.nulabinc.zxcvbn;
requires org.slf4j;
requires org.apache.commons.lang3;
+ requires org.eclipse.jetty.server;
requires dagger;
requires com.auth0.jwt;
requires org.bouncycastle.provider;
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java
new file mode 100644
index 000000000..dfb37a2ab
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java
@@ -0,0 +1,118 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.io.BaseEncoding;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.TransferQueue;
+import java.util.function.Consumer;
+
+/**
+ * A basic implementation for RFC 8252, Section 7.3:
+ *
+ * We're spawning a local http server on a system-assigned high port and
+ * use http://127.0.0.1:{PORT}/success as a redirect URI.
+ *
+ * Furthermore, we can deliver a html response to inform the user that the
+ * auth workflow finished and she can close the browser tab.
+ */
+class AuthReceiver implements AutoCloseable {
+
+ private static final String REDIRECT_SCHEME = "http";
+ private static final String LOOPBACK_ADDR = "127.0.0.1";
+ private static final String JSON_200 = """
+ {"status": "success"}
+ """;
+ private static final String JSON_400 = """
+ {"status": "missing param key"}
+ """;
+
+ private final Server server;
+ private final ServerConnector connector;
+ private final Handler handler;
+
+ private AuthReceiver(Server server, ServerConnector connector, Handler handler) {
+ assert server.isRunning();
+ this.server = server;
+ this.connector = connector;
+ this.handler = handler;
+ }
+
+ public URI getRedirectURL() {
+ try {
+ return new URI(REDIRECT_SCHEME, null, LOOPBACK_ADDR, connector.getLocalPort(), null, null, null);
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("URI constructed from well-formed components.", e);
+ }
+ }
+
+ public static AuthReceiver start() throws Exception {
+ Server server = new Server();
+ var handler = new Handler();
+ var connector = new ServerConnector(server);
+ connector.setPort(0);
+ connector.setHost(LOOPBACK_ADDR);
+ server.setConnectors(new Connector[]{connector});
+ server.setHandler(handler);
+ server.start();
+ return new AuthReceiver(server, connector, handler);
+ }
+
+ public String receive() throws InterruptedException {
+ return handler.receivedKeys.take();
+ }
+
+ @Override
+ public void close() throws Exception {
+ server.stop();
+ }
+
+ private static class Handler extends AbstractHandler {
+
+ private final BlockingQueue receivedKeys = new LinkedBlockingQueue<>();
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse res) throws IOException {
+ baseRequest.setHandled(true);
+ var key = req.getParameter("key");
+ byte[] response;
+ if (key != null) {
+ res.setStatus(HttpServletResponse.SC_OK);
+ response = JSON_200.getBytes(StandardCharsets.UTF_8);
+ } else {
+ res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ response = JSON_400.getBytes(StandardCharsets.UTF_8);
+ }
+ res.setContentType("application/json;charset=utf-8");
+ res.setContentLength(response.length);
+ res.getOutputStream().write(response);
+ res.getOutputStream().flush();
+
+ // the following line might trigger a server shutdown,
+ // so let's make sure the response is flushed first
+ if (key != null) {
+ receivedKeys.add(key);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
index 367db4d6e..38a6643b0 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
@@ -1,5 +1,7 @@
package org.cryptomator.ui.keyloading.hub;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.Masterkey;
@@ -12,23 +14,28 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import javax.inject.Inject;
+import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.net.URI;
import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoading
public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
- static final String SCHEME_HUB_HTTP = "hub+http";
- static final String SCHEME_HUB_HTTPS = "hub+https";
- private static final String SCHEME_HTTP = "http";
- private static final String SCHEME_HTTPS = "https";
+ private static final String SCHEME_PREFIX = "hub+";
+ static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
+ static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
+ private final Application application;
+ private final ExecutorService executor;
private final Vault vault;
private final Stage window;
private final Lazy p12LoadingScene;
@@ -36,7 +43,9 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
private final AtomicReference keyPairRef;
@Inject
- public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock p12LoadingLock, AtomicReference keyPairRef) {
+ public HubKeyLoadingStrategy(Application application, ExecutorService executor, @KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock p12LoadingLock, AtomicReference keyPairRef) {
+ this.application = application;
+ this.executor = executor;
this.vault = vault;
this.window = window;
this.p12LoadingScene = p12LoadingScene;
@@ -46,23 +55,14 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
@Override
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
- return switch (keyId.getScheme().toLowerCase()) {
- case SCHEME_HUB_HTTP -> loadKey(keyId, SCHEME_HTTP);
- case SCHEME_HUB_HTTPS -> loadKey(keyId, SCHEME_HTTPS);
- default -> throw new IllegalArgumentException("Only supports keys with schemes " + SCHEME_HUB_HTTP + " or " + SCHEME_HUB_HTTPS);
- };
- }
-
- private Masterkey loadKey(URI keyId, String adjustedScheme) {
- try {
- var foo = new URI(adjustedScheme, keyId.getSchemeSpecificPart(), keyId.getFragment());
- } catch (URISyntaxException e) {
- throw new IllegalStateException("URI known to be valid, if old URI was valid", e);
- }
-
+ Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
try {
loadP12();
LOG.info("keypair loaded {}", keyPairRef.get().getPublic());
+ var task = new ReceiveEncryptedMasterkeyTask(redirectUri -> {
+ openBrowser(keyId, redirectUri);
+ });
+ executor.submit(task);
throw new UnlockCancelledException("not yet implemented"); // TODO
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -70,6 +70,18 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
}
}
+ private void openBrowser(URI keyId, URI redirectUri) {
+ Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
+ var httpScheme = keyId.getScheme().substring(SCHEME_PREFIX.length());
+ var redirectParam = "redirect_uri="+ URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII);
+ try {
+ var uri = new URI(httpScheme, keyId.getAuthority(), keyId.getPath(), redirectParam, null);
+ application.getHostServices().showDocument(uri.toString());
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("URI constructed from params known to be valid", e);
+ }
+ }
+
private HubKeyLoadingModule.P12KeyLoading loadP12() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(p12LoadingScene.get());
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java
new file mode 100644
index 000000000..70a08cca6
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java
@@ -0,0 +1,31 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.io.BaseEncoding;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javafx.concurrent.Task;
+import java.net.URI;
+import java.util.function.Consumer;
+
+class ReceiveEncryptedMasterkeyTask extends Task {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ReceiveEncryptedMasterkeyTask.class);
+
+ private final Consumer redirectUriConsumer;
+
+ public ReceiveEncryptedMasterkeyTask(Consumer redirectUriConsumer) {
+ this.redirectUriConsumer = redirectUriConsumer;
+ }
+
+ @Override
+ protected byte[] call() throws Exception {
+ try (var receiver = AuthReceiver.start()) {
+ var redirectUri = receiver.getRedirectURL();
+ LOG.debug("Waiting for key on {}", redirectUri);
+ redirectUriConsumer.accept(redirectUri);
+ var token = receiver.receive();
+ return BaseEncoding.base64Url().decode(token);
+ }
+ }
+}
diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java
new file mode 100644
index 000000000..531f63415
--- /dev/null
+++ b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java
@@ -0,0 +1,23 @@
+package org.cryptomator.ui.keyloading.hub;
+
+public class AuthReceiverTest {
+
+ static {
+ System.setProperty("LOGLEVEL", "INFO");
+ }
+
+ public static void main(String[] args) {
+ try (var receiver = AuthReceiver.start()) {
+ System.out.println("Waiting on " + receiver.getRedirectURL());
+ var token = receiver.receive();
+ System.out.println("SUCCESS: " + token);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ System.out.println("CANCELLED");
+ } catch (Exception e) {
+ System.out.println("ERROR");
+ e.printStackTrace();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 000000000..51bfcac67
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
\ No newline at end of file