From 7fabc6f52d04412d55504b3e1492ec0c784cd57c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 29 Jul 2021 16:57:28 +0200 Subject: [PATCH] spawn server listening on localhost, used for oauth redirect_uri --- pom.xml | 7 ++ src/main/java/module-info.java | 1 + .../ui/keyloading/hub/AuthReceiver.java | 118 ++++++++++++++++++ .../keyloading/hub/HubKeyLoadingStrategy.java | 50 +++++--- .../hub/ReceiveEncryptedMasterkeyTask.java | 31 +++++ .../ui/keyloading/hub/AuthReceiverTest.java | 23 ++++ src/test/resources/logback-test.xml | 11 ++ 7 files changed, 222 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java create mode 100644 src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java create mode 100644 src/test/resources/logback-test.xml diff --git a/pom.xml b/pom.xml index 581a4c70c..8fd91628f 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ 1.5.2 1.7.31 1.2.3 + 10.0.6 5.7.2 @@ -136,6 +137,12 @@ ${bouncycastle.version} + + org.eclipse.jetty + jetty-server + ${jetty.version} + + 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