prepare local webserver for cross-origin requests

This commit is contained in:
Sebastian Stenzel
2021-08-09 18:11:44 +02:00
parent d938b1c3f7
commit 43dbdb3e8f
8 changed files with 96 additions and 50 deletions

10
pom.xml
View File

@@ -142,6 +142,16 @@
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- JWT -->
<dependency>

View File

@@ -29,6 +29,8 @@ module org.cryptomator.desktop {
requires org.bouncycastle.pkix;
requires org.apache.commons.lang3;
requires org.eclipse.jetty.server;
requires org.eclipse.jetty.webapp;
requires org.eclipse.jetty.servlets;
/* TODO: filename-based modules: */
requires static javax.inject; /* ugly dagger/guava crap */

View File

@@ -1,6 +1,7 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.base.Preconditions;
import com.google.common.io.BaseEncoding;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
@@ -43,8 +44,6 @@ public class AuthController implements FxController {
private final AtomicReference<URI> hubUriRef;
private final ErrorComponent.Builder errorComponent;
private final ObjectProperty<URI> redirectUriRef;
private final ObjectBinding<URI> authUri;
private final StringBinding authUriHost;
private final BooleanBinding ready;
private final AuthReceiveTask receiveTask;
@@ -58,9 +57,7 @@ public class AuthController implements FxController {
this.hubUriRef = hubUriRef;
this.errorComponent = errorComponent;
this.redirectUriRef = new SimpleObjectProperty<>();
this.authUri = Bindings.createObjectBinding(this::getAuthUri, redirectUriRef);
this.authUriHost = Bindings.createStringBinding(this::getAuthUriHost, authUri);
this.ready = authUri.isNotNull();
this.ready = redirectUriRef.isNotNull();
this.receiveTask = new AuthReceiveTask(redirectUriRef::set);
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
}
@@ -81,7 +78,7 @@ public class AuthController implements FxController {
private void receivedKey(WorkerStateEvent workerStateEvent) {
var authParams = receiveTask.getValue();
LOG.info("Cryptomator Hub login succeeded: {} encrypted with {}", authParams, keyPair.getPublic());
LOG.info("Cryptomator Hub login succeeded: {} encrypted with {}", authParams.getEphemeralPublicKey(), keyPair.getPublic());
// TODO decrypt and return masterkey
authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.SUCCESS);
window.close();
@@ -104,40 +101,25 @@ public class AuthController implements FxController {
@FXML
public void openBrowser() {
assert getAuthUri() != null;
application.getHostServices().showDocument(getAuthUri().toString());
assert ready.get();
var hubUri = Objects.requireNonNull(hubUriRef.get());
var redirectUri = Objects.requireNonNull(redirectUriRef.get());
var sb = new StringBuilder(hubUri.toString());
sb.append("?redirect_uri=").append(URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII));
sb.append("&device_id=").append("desktop-app-3000");
sb.append("&device_key=").append(BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded()));
var url = sb.toString();
application.getHostServices().showDocument(url);
}
/* Getter/Setter */
public ObjectBinding<URI> authUriProperty() {
return authUri;
}
public URI getAuthUri() {
public String getHubUriHost() {
var hubUri = hubUriRef.get();
var redirectUri = redirectUriRef.get();
if (hubUri == null || redirectUri == null) {
return null;
}
var redirectParam = "redirect_uri=" + URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII);
try {
return new URI(hubUri.getScheme(), hubUri.getAuthority(), hubUri.getPath(), redirectParam, null);
} catch (URISyntaxException e) {
throw new IllegalStateException("URI constructed from params known to be valid", e);
}
}
public StringBinding authUriHostProperty() {
return authUriHost;
}
public String getAuthUriHost() {
var authUri = getAuthUri();
if (authUri == null) {
if (hubUri == null) {
return null;
} else {
return authUri.getHost();
return hubUri.getHost();
}
}

View File

@@ -1,12 +1,44 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.io.BaseEncoding;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* Parameters required to decrypt the masterkey:
* ECIES parameters required to decrypt the masterkey:
* <ul>
* <li><code>m</code> Encrypted Masterkey (Base64-encoded)</li>
* <li><code>epk</code> Ephemeral Public Key (TODO: PEM-encoded?)</li>
* <li><code>m</code> Encrypted Masterkey (base64url-encoded ciphertext)</li>
* <li><code>epk</code> Ephemeral Public Key (base64url-encoded PKCS8)</li>
* </ul>
*
* No separate tag required, since we use GCM for encryption.
*/
record AuthParams(String m, String epk) {
public byte[] getCiphertext() {
return BaseEncoding.base64Url().decode(m());
}
public ECPublicKey getEphemeralPublicKey() {
try {
byte[] keyBytes = BaseEncoding.base64Url().decode(epk());
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
if (key instanceof ECPublicKey k) {
return k;
} else {
throw new IllegalArgumentException("Key not an EC public key.");
}
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException("Invalid license public key", e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -5,13 +5,25 @@ 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 org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import javax.servlet.DispatcherType;
import javax.servlet.FilterConfig;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
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.EnumSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -37,13 +49,13 @@ class AuthReceiver implements AutoCloseable {
private final Server server;
private final ServerConnector connector;
private final Handler handler;
private final CallbackServlet servlet;
private AuthReceiver(Server server, ServerConnector connector, Handler handler) {
private AuthReceiver(Server server, ServerConnector connector, CallbackServlet servlet) {
assert server.isRunning();
this.server = server;
this.connector = connector;
this.handler = handler;
this.servlet = servlet;
}
public URI getRedirectURL() {
@@ -55,19 +67,27 @@ class AuthReceiver implements AutoCloseable {
}
public static AuthReceiver start() throws Exception {
Server server = new Server();
var handler = new Handler();
var server = new Server();
var context = new ServletContextHandler();
var corsFilter = new FilterHolder(new CrossOriginFilter());
corsFilter.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); // TODO restrict to hub host
context.addFilter(corsFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
var servlet = new CallbackServlet();
context.addServlet(new ServletHolder(servlet), "/*");
var connector = new ServerConnector(server);
connector.setPort(0);
connector.setHost(LOOPBACK_ADDR);
server.setConnectors(new Connector[]{connector});
server.setHandler(handler);
server.setHandler(context);
server.start();
return new AuthReceiver(server, connector, handler);
return new AuthReceiver(server, connector, servlet);
}
public AuthParams receive() throws InterruptedException {
return handler.receivedKeys.take();
return servlet.receivedKeys.take();
}
@Override
@@ -75,13 +95,13 @@ class AuthReceiver implements AutoCloseable {
server.stop();
}
private static class Handler extends AbstractHandler {
private static class CallbackServlet extends HttpServlet {
private final BlockingQueue<AuthParams> receivedKeys = new LinkedBlockingQueue<>();
// TODO change to POST?
@Override
public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse res) throws IOException {
baseRequest.setHandled(true);
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
var m = req.getParameter("m"); // encrypted masterkey
var epk = req.getParameter("epk"); // ephemeral public key
byte[] response;

View File

@@ -64,7 +64,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
var hubUriScheme = keyId.getScheme().substring(SCHEME_PREFIX.length());
try {
return new URI(hubUriScheme, keyId.getSchemeSpecificPart(), null);
return new URI(hubUriScheme, keyId.getSchemeSpecificPart(), keyId.getFragment());
} catch (URISyntaxException e) {
throw new IllegalStateException("URI constructed from params known to be valid", e);
}

View File

@@ -23,7 +23,7 @@ import java.security.spec.ECGenParameterSpec;
class P12AccessHelper {
private static final String EC_ALG = "EC";
private static final String EC_CURVE_NAME = "secp256r1";
private static final String EC_CURVE_NAME = "secp256r1"; // TODO switch to secp384r1
private static final String SIGNATURE_ALG = "SHA256withECDSA";
private static final String KEYSTORE_ALIAS_KEY = "key";
private static final String KEYSTORE_ALIAS_CERT = "crt";

View File

@@ -28,7 +28,7 @@
</ImageView>
<TextFlow visible="${controller.ready}" managed="${controller.ready}">
<Text text="TODO: please login via " />
<Hyperlink styleClass="hyperlink-underline" text="${controller.authUriHost}" onAction="#openBrowser"/>
<Hyperlink styleClass="hyperlink-underline" text="${controller.hubUriHost}" onAction="#openBrowser"/>
</TextFlow>
<FontAwesome5Spinner glyphSize="12" visible="${!controller.ready}" managed="${!controller.ready}"/>
</HBox>