Explorar o código

keep window open while waiting for http callback

Sebastian Stenzel %!s(int64=4) %!d(string=hai) anos
pai
achega
d938b1c3f7
Modificáronse 20 ficheiros con 357 adicións e 142 borrados
  1. 8 5
      src/main/java/module-info.java
  2. 1 0
      src/main/java/org/cryptomator/ui/common/FxmlFile.java
  3. 151 0
      src/main/java/org/cryptomator/ui/keyloading/hub/AuthController.java
  4. 12 0
      src/main/java/org/cryptomator/ui/keyloading/hub/AuthParams.java
  5. 35 0
      src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java
  6. 11 22
      src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java
  7. 24 5
      src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
  8. 17 29
      src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
  9. 1 1
      src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java
  10. 7 25
      src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java
  11. 17 8
      src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java
  12. 19 6
      src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java
  13. 0 31
      src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java
  14. 2 2
      src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java
  15. 2 2
      src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
  16. 2 2
      src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java
  17. 2 2
      src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java
  18. 44 0
      src/main/resources/fxml/hub_auth.fxml
  19. 1 1
      src/main/resources/fxml/hub_p12_create.fxml
  20. 1 1
      src/main/resources/fxml/hub_p12_load.fxml

+ 8 - 5
src/main/java/module-info.java

@@ -9,23 +9,26 @@ module org.cryptomator.desktop {
 	requires org.cryptomator.frontend.fuse;
 	requires org.cryptomator.frontend.webdav;
 	requires org.cryptomator.integrations.api;
+	// jdk:
 	requires java.desktop;
 	requires java.net.http;
 	requires javafx.base;
 	requires javafx.graphics;
 	requires javafx.controls;
 	requires javafx.fxml;
-	requires com.tobiasdiez.easybind;
+	requires jdk.crypto.ec;
+	// 3rd party:
+	requires com.auth0.jwt;
 	requires com.google.common;
 	requires com.google.gson;
 	requires com.nulabinc.zxcvbn;
-	requires org.slf4j;
-	requires org.apache.commons.lang3;
-	requires org.eclipse.jetty.server;
+	requires com.tobiasdiez.easybind;
 	requires dagger;
-	requires com.auth0.jwt;
+	requires org.slf4j;
 	requires org.bouncycastle.provider;
 	requires org.bouncycastle.pkix;
+	requires org.apache.commons.lang3;
+	requires org.eclipse.jetty.server;
 
 	/* TODO: filename-based modules: */
 	requires static javax.inject; /* ugly dagger/guava crap */

+ 1 - 0
src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -15,6 +15,7 @@ public enum FxmlFile {
 	HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), //
 	HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
 	HUB_P12("/fxml/hub_p12.fxml"), //
+	HUB_AUTH("/fxml/hub_auth.fxml"), //
 	LOCK_FORCED("/fxml/lock_forced.fxml"), //
 	LOCK_FAILED("/fxml/lock_failed.fxml"), //
 	MAIN_WINDOW("/fxml/main_window.fxml"), //

+ 151 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/AuthController.java

@@ -0,0 +1,151 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.base.Preconditions;
+import org.cryptomator.ui.common.ErrorComponent;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.keyloading.KeyLoading;
+import org.cryptomator.ui.keyloading.KeyLoadingScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoadingScoped
+public class AuthController implements FxController {
+
+	private static final Logger LOG = LoggerFactory.getLogger(AuthController.class);
+
+	private final Application application;
+	private final ExecutorService executor;
+	private final Stage window;
+	private final KeyPair keyPair;
+	private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> authFlowLock;
+	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;
+
+	@Inject
+	public AuthController(Application application, ExecutorService executor, @KeyLoading Stage window, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.AuthFlow> authFlowLock, AtomicReference<URI> hubUriRef, ErrorComponent.Builder errorComponent) {
+		this.application = application;
+		this.executor = executor;
+		this.window = window;
+		this.keyPair = Objects.requireNonNull(keyPairRef.get());
+		this.authFlowLock = authFlowLock;
+		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.receiveTask = new AuthReceiveTask(redirectUriRef::set);
+		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
+	}
+
+	@FXML
+	public void initialize() {
+		Preconditions.checkState(hubUriRef.get() != null);
+		receiveTask.setOnSucceeded(this::receivedKey);
+		receiveTask.setOnFailed(this::keyRetrievalFailed);
+		executor.submit(receiveTask);
+	}
+
+	private void keyRetrievalFailed(WorkerStateEvent workerStateEvent) {
+		LOG.error("Cryptomator Hub login failed with error", receiveTask.getException());
+		authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.FAILED);
+		errorComponent.cause(receiveTask.getException()).window(window).build().showErrorScene();
+	}
+
+	private void receivedKey(WorkerStateEvent workerStateEvent) {
+		var authParams = receiveTask.getValue();
+		LOG.info("Cryptomator Hub login succeeded: {} encrypted with {}", authParams, keyPair.getPublic());
+		// TODO decrypt and return masterkey
+		authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.SUCCESS);
+		window.close();
+	}
+
+	@FXML
+	public void cancel() {
+		window.close();
+	}
+
+	private void windowClosed(WindowEvent windowEvent) {
+		// stop server, if it is still running
+		receiveTask.cancel();
+		// if not already interacted, mark this workflow as cancelled:
+		if (authFlowLock.awaitingInteraction().get()) {
+			LOG.debug("Authorization cancelled by user.");
+			authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED);
+		}
+	}
+
+	@FXML
+	public void openBrowser() {
+		assert getAuthUri() != null;
+		application.getHostServices().showDocument(getAuthUri().toString());
+	}
+
+	/* Getter/Setter */
+
+	public ObjectBinding<URI> authUriProperty() {
+		return authUri;
+	}
+
+	public URI getAuthUri() {
+		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) {
+			return null;
+		} else {
+			return authUri.getHost();
+		}
+	}
+
+	public BooleanBinding readyProperty() {
+		return ready;
+	}
+
+	public boolean isReady() {
+		return ready.get();
+	}
+}

+ 12 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/AuthParams.java

@@ -0,0 +1,12 @@
+package org.cryptomator.ui.keyloading.hub;
+
+/**
+ * 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>
+ * </ul>
+ */
+record AuthParams(String m, String epk) {
+
+}

+ 35 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java

@@ -0,0 +1,35 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import java.net.URI;
+import java.util.function.Consumer;
+
+class AuthReceiveTask extends Task<AuthParams> {
+
+	private static final Logger LOG = LoggerFactory.getLogger(AuthReceiveTask.class);
+
+	private final Consumer<URI> redirectUriConsumer;
+
+	/**
+	 * Spawns a server and waits for the redirectUri to be called.
+	 *
+	 * @param redirectUriConsumer A callback invoked with the redirectUri, as soon as the server has started
+	 */
+	public AuthReceiveTask(Consumer<URI> redirectUriConsumer) {
+		this.redirectUriConsumer = redirectUriConsumer;
+	}
+
+	@Override
+	protected AuthParams call() throws Exception {
+		try (var receiver = AuthReceiver.start()) {
+			var redirectUri = receiver.getRedirectURL();
+			Platform.runLater(() -> redirectUriConsumer.accept(redirectUri));
+			LOG.debug("Waiting for key on {}", redirectUri);
+			return receiver.receive();
+		}
+	}
+}

+ 11 - 22
src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java

@@ -1,6 +1,5 @@
 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;
@@ -13,19 +12,8 @@ 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:
@@ -41,11 +29,11 @@ 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"}
-					""";
+			{"status": "success"}
+			""";
 	private static final String JSON_400 = """
-					{"status": "missing param key"}
-					""";
+			{"status": "missing param"}
+			""";
 
 	private final Server server;
 	private final ServerConnector connector;
@@ -78,7 +66,7 @@ class AuthReceiver implements AutoCloseable {
 		return new AuthReceiver(server, connector, handler);
 	}
 
-	public String receive() throws InterruptedException {
+	public AuthParams receive() throws InterruptedException {
 		return handler.receivedKeys.take();
 	}
 
@@ -89,14 +77,15 @@ class AuthReceiver implements AutoCloseable {
 
 	private static class Handler extends AbstractHandler {
 
-		private final BlockingQueue<String> receivedKeys = new LinkedBlockingQueue<>();
+		private final BlockingQueue<AuthParams> 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");
+			var m = req.getParameter("m"); // encrypted masterkey
+			var epk = req.getParameter("epk"); // ephemeral public key
 			byte[] response;
-			if (key != null) {
+			if (m != null && epk != null) {
 				res.setStatus(HttpServletResponse.SC_OK);
 				response = JSON_200.getBytes(StandardCharsets.UTF_8);
 			} else {
@@ -110,8 +99,8 @@ class AuthReceiver implements AutoCloseable {
 
 			// the following line might trigger a server shutdown,
 			// so let's make sure the response is flushed first
-			if (key != null) {
-				receivedKeys.add(key);
+			if (m != null && epk != null) {
+				receivedKeys.add(new AuthParams(m, epk));
 			}
 		}
 	}

+ 24 - 5
src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java

@@ -18,6 +18,7 @@ import org.cryptomator.ui.keyloading.KeyLoadingScoped;
 import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
 
 import javafx.scene.Scene;
+import java.net.URI;
 import java.security.KeyPair;
 import java.util.ResourceBundle;
 import java.util.concurrent.atomic.AtomicReference;
@@ -25,10 +26,10 @@ import java.util.concurrent.atomic.AtomicReference;
 @Module
 public abstract class HubKeyLoadingModule {
 
-	public enum P12KeyLoading {
-		LOADED,
-		CREATED,
-		CANCELED
+	public enum AuthFlow {
+		SUCCESS,
+		FAILED,
+		CANCELLED
 	}
 
 	@Provides
@@ -39,10 +40,16 @@ public abstract class HubKeyLoadingModule {
 
 	@Provides
 	@KeyLoadingScoped
-	static UserInteractionLock<P12KeyLoading> provideP12KeyLoadingLock() {
+	static UserInteractionLock<AuthFlow> provideAuthFlowLock() {
 		return new UserInteractionLock<>(null);
 	}
 
+	@Provides
+	@KeyLoadingScoped
+	static AtomicReference<URI> provideHubUri() {
+		return new AtomicReference<>();
+	}
+
 	@Binds
 	@IntoMap
 	@KeyLoadingScoped
@@ -62,6 +69,13 @@ public abstract class HubKeyLoadingModule {
 		return fxmlLoaders.createScene(FxmlFile.HUB_P12);
 	}
 
+	@Provides
+	@FxmlScene(FxmlFile.HUB_AUTH)
+	@KeyLoadingScoped
+	static Scene provideHubAuthScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene(FxmlFile.HUB_AUTH);
+	}
+
 	@Binds
 	@IntoMap
 	@FxControllerKey(P12Controller.class)
@@ -84,4 +98,9 @@ public abstract class HubKeyLoadingModule {
 		return new NewPasswordController(resourceBundle, strengthRater);
 	}
 
+	@Binds
+	@IntoMap
+	@FxControllerKey(AuthController.class)
+	abstract FxController bindAuthController(AuthController controller);
+
 }

+ 17 - 29
src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java

@@ -1,7 +1,6 @@
 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;
@@ -14,17 +13,13 @@ 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
@@ -34,55 +29,48 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
 	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<Scene> p12LoadingScene;
-	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
-	private final AtomicReference<KeyPair> keyPairRef;
+	private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction;
+	private final AtomicReference<URI> hubUriRef;
 
 	@Inject
-	public HubKeyLoadingStrategy(Application application, ExecutorService executor, @KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock, AtomicReference<KeyPair> keyPairRef) {
-		this.application = application;
-		this.executor = executor;
+	public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction, AtomicReference<URI> hubUriRef) {
 		this.vault = vault;
 		this.window = window;
 		this.p12LoadingScene = p12LoadingScene;
-		this.p12LoadingLock = p12LoadingLock;
-		this.keyPairRef = keyPairRef;
+		this.userInteraction = userInteraction;
+		this.hubUriRef = hubUriRef;
 	}
 
 	@Override
 	public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
-		Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
+		hubUriRef.set(getHubUri(keyId));
 		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
+			switch (auth()) {
+				case SUCCESS -> LOG.debug("TODO success"); // TODO return key
+				//case FAILED -> LOG.error("failed to load keypair");
+				case CANCELLED -> throw new UnlockCancelledException("User cancelled auth workflow");
+			}
+			throw new UnlockCancelledException("not yet implemented"); // TODO remove
 		} catch (InterruptedException e) {
 			Thread.currentThread().interrupt();
 			throw new UnlockCancelledException("Loading interrupted", e);
 		}
 	}
 
-	private void openBrowser(URI keyId, URI redirectUri) {
+	private URI getHubUri(URI keyId) {
 		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);
+		var hubUriScheme = keyId.getScheme().substring(SCHEME_PREFIX.length());
 		try {
-			var uri = new URI(httpScheme, keyId.getAuthority(), keyId.getPath(), redirectParam, null);
-			application.getHostServices().showDocument(uri.toString());
+			return new URI(hubUriScheme, keyId.getSchemeSpecificPart(), null);
 		} catch (URISyntaxException e) {
 			throw new IllegalStateException("URI constructed from params known to be valid", e);
 		}
 	}
 
-	private HubKeyLoadingModule.P12KeyLoading loadP12() throws InterruptedException {
+	private HubKeyLoadingModule.AuthFlow auth() throws InterruptedException {
 		Platform.runLater(() -> {
 			window.setScene(p12LoadingScene.get());
 			window.show();
@@ -94,7 +82,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
 				window.centerOnScreen();
 			}
 		});
-		return p12LoadingLock.awaitInteraction();
+		return userInteraction.awaitInteraction();
 	}
 
 }

+ 1 - 1
src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java

@@ -93,7 +93,7 @@ class P12AccessHelper {
 			keyGen.initialize(new ECGenParameterSpec(EC_CURVE_NAME));
 			return keyGen;
 		} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
-			throw new IllegalStateException("secp256r1 curve not supported");
+			throw new IllegalStateException(EC_CURVE_NAME + " curve not supported");
 		}
 	}
 

+ 7 - 25
src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java

@@ -1,35 +1,17 @@
 package org.cryptomator.ui.keyloading.hub;
 
-import com.google.common.base.Preconditions;
 import org.cryptomator.common.Environment;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.common.Destroyables;
 import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.common.NewPasswordController;
 import org.cryptomator.ui.common.UserInteractionLock;
-import org.cryptomator.ui.controls.NiceSecurePasswordField;
 import org.cryptomator.ui.keyloading.KeyLoading;
 import org.cryptomator.ui.keyloading.KeyLoadingScoped;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanExpression;
-import javafx.beans.binding.ObjectExpression;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.fxml.FXML;
-import javafx.scene.control.ContentDisplay;
-import javafx.scene.layout.VBox;
 import javafx.stage.Stage;
 import javafx.stage.WindowEvent;
-import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.KeyPair;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicReference;
 
 @KeyLoadingScoped
 public class P12Controller implements FxController {
@@ -38,21 +20,21 @@ public class P12Controller implements FxController {
 
 	private final Stage window;
 	private final Environment env;
-	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
+	private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction;
 
 	@Inject
-	public P12Controller(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock) {
+	public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction) {
 		this.window = window;
 		this.env = env;
-		this.p12LoadingLock = p12LoadingLock;
-		this.window.setOnHiding(this::windowClosed);
+		this.userInteraction = userInteraction;
+		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
 	}
 
 	private void windowClosed(WindowEvent windowEvent) {
 		// if not already interacted, mark this workflow as cancelled:
-		if (p12LoadingLock.awaitingInteraction().get()) {
-			LOG.debug("P12 loading canceled by user.");
-			p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.CANCELED);
+		if (userInteraction.awaitingInteraction().get()) {
+			LOG.debug("P12 loading cancelled by user.");
+			userInteraction.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED);
 		}
 	}
 

+ 17 - 8
src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java

@@ -1,9 +1,12 @@
 package org.cryptomator.ui.keyloading.hub;
 
 import com.google.common.base.Preconditions;
+import dagger.Lazy;
 import org.cryptomator.common.Environment;
 import org.cryptomator.cryptolib.common.Destroyables;
 import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.common.NewPasswordController;
 import org.cryptomator.ui.common.UserInteractionLock;
 import org.cryptomator.ui.keyloading.KeyLoading;
@@ -19,8 +22,10 @@ import javafx.beans.binding.ObjectExpression;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.fxml.FXML;
+import javafx.scene.Scene;
 import javafx.scene.control.ContentDisplay;
 import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.security.KeyPair;
@@ -35,25 +40,27 @@ public class P12CreateController implements FxController  {
 	private final Stage window;
 	private final Environment env;
 	private final AtomicReference<KeyPair> keyPairRef;
-	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
+	private final Lazy<Scene> authScene;
+
 	private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
 	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
 	private final BooleanProperty readyToCreate = new SimpleBooleanProperty();
 
 	public NewPasswordController newPasswordController;
 
-
 	@Inject
-	public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock) {
+	public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH) Lazy<Scene> authScene) {
 		this.window = window;
 		this.env = env;
 		this.keyPairRef = keyPairRef;
-		this.p12LoadingLock = p12LoadingLock;
+		this.authScene = authScene;
+		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
 	}
 
 	@FXML
 	public void initialize() {
 		readyToCreate.bind(newPasswordController.goodPasswordProperty());
+		newPasswordController.passwordField.requestFocus();
 	}
 
 	@FXML
@@ -61,6 +68,11 @@ public class P12CreateController implements FxController  {
 		window.close();
 	}
 
+	private void windowClosed(WindowEvent windowEvent) {
+		newPasswordController.passwordField.wipe();
+		newPasswordController.reenterField.wipe();
+	}
+
 	@FXML
 	public void create() {
 		Preconditions.checkState(newPasswordController.goodPasswordProperty().get());
@@ -70,15 +82,12 @@ public class P12CreateController implements FxController  {
 			var keyPair = P12AccessHelper.createNew(p12File, pw);
 			setKeyPair(keyPair);
 			LOG.debug("Created .p12 file {}", p12File);
-			p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.CREATED);
-			window.close();
+			window.setScene(authScene.get());
 		} catch (IOException e) {
 			LOG.error("Failed to load .p12 file.", e);
 			// TODO
 		} finally {
 			Arrays.fill(pw, '\0');
-			newPasswordController.passwordField.wipe();
-			newPasswordController.reenterField.wipe();
 		}
 	}
 

+ 19 - 6
src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java

@@ -1,10 +1,13 @@
 package org.cryptomator.ui.keyloading.hub;
 
+import dagger.Lazy;
 import org.cryptomator.common.Environment;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.common.Destroyables;
 import org.cryptomator.ui.common.Animations;
 import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.common.UserInteractionLock;
 import org.cryptomator.ui.controls.NiceSecurePasswordField;
 import org.cryptomator.ui.keyloading.KeyLoading;
@@ -20,8 +23,10 @@ import javafx.beans.binding.ObjectExpression;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.fxml.FXML;
+import javafx.scene.Scene;
 import javafx.scene.control.ContentDisplay;
 import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -37,18 +42,24 @@ public class P12LoadController implements FxController {
 	private final Stage window;
 	private final Environment env;
 	private final AtomicReference<KeyPair> keyPairRef;
-	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
+	private final Lazy<Scene> authScene;
 	private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
 	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
 
 	public NiceSecurePasswordField passwordField;
 
 	@Inject
-	public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock) {
+	public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH) Lazy<Scene> authScene) {
 		this.window = window;
 		this.env = env;
 		this.keyPairRef = keyPairRef;
-		this.p12LoadingLock = p12LoadingLock;
+		this.authScene = authScene;
+		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
+	}
+
+	@FXML
+	public void initialize() {
+		passwordField.requestFocus();
 	}
 
 	@FXML
@@ -56,6 +67,10 @@ public class P12LoadController implements FxController {
 		window.close();
 	}
 
+	private void windowClosed(WindowEvent windowEvent) {
+		passwordField.wipe();
+	}
+
 	@FXML
 	public void load() {
 		char[] pw = passwordField.copyChars();
@@ -64,8 +79,7 @@ public class P12LoadController implements FxController {
 			var keyPair = P12AccessHelper.loadExisting(p12File, pw);
 			setKeyPair(keyPair);
 			LOG.debug("Loaded .p12 file {}", p12File);
-			p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.LOADED);
-			window.close();
+			window.setScene(authScene.get());
 		} catch (InvalidPassphraseException e) {
 			LOG.warn("Invalid passphrase entered for .p12 file");
 			Animations.createShakeWindowAnimation(window).playFromStart();
@@ -75,7 +89,6 @@ public class P12LoadController implements FxController {
 			// TODO
 		} finally {
 			Arrays.fill(pw, '\0');
-			passwordField.wipe();
 		}
 	}
 

+ 0 - 31
src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java

@@ -1,31 +0,0 @@
-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<byte[]> {
-
-	private static final Logger LOG = LoggerFactory.getLogger(ReceiveEncryptedMasterkeyTask.class);
-
-	private final Consumer<URI> redirectUriConsumer;
-
-	public ReceiveEncryptedMasterkeyTask(Consumer<URI> 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);
-		}
-	}
-}

+ 2 - 2
src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java

@@ -35,12 +35,12 @@ public abstract class MasterkeyFileLoadingModule {
 
 	public enum PasswordEntry {
 		PASSWORD_ENTERED,
-		CANCELED
+		CANCELLED
 	}
 
 	public enum MasterkeyFileProvision {
 		MASTERKEYFILE_PROVIDED,
-		CANCELED
+		CANCELLED
 	}
 
 	@Provides

+ 2 - 2
src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java

@@ -95,7 +95,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
 		if (filePath.get() == null) {
 			return switch (askUserForMasterkeyFilePath()) {
 				case MASTERKEYFILE_PROVIDED -> filePath.get();
-				case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
+				case CANCELLED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
 			};
 		} else {
 			return filePath.get();
@@ -121,7 +121,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
 		if (password.get() == null) {
 			return switch (askForPassphrase()) {
 				case PASSWORD_ENTERED -> CharBuffer.wrap(password.get());
-				case CANCELED -> throw new UnlockCancelledException("Password entry cancelled.");
+				case CANCELLED -> throw new UnlockCancelledException("Password entry cancelled.");
 			};
 		} else {
 			// e.g. pre-filled from keychain or previous unlock attempt

+ 2 - 2
src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java

@@ -143,8 +143,8 @@ public class PassphraseEntryController implements FxController {
 	private void windowClosed(WindowEvent windowEvent) {
 		// if not already interacted, mark this workflow as cancelled:
 		if (passwordEntryLock.awaitingInteraction().get()) {
-			LOG.debug("Unlock canceled by user.");
-			passwordEntryLock.interacted(PasswordEntry.CANCELED);
+			LOG.debug("Unlock cancelled by user.");
+			passwordEntryLock.interacted(PasswordEntry.CANCELLED);
 		}
 	}
 

+ 2 - 2
src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java

@@ -45,8 +45,8 @@ public class SelectMasterkeyFileController implements FxController {
 	private void windowClosed(WindowEvent windowEvent) {
 		// if not already interacted, mark this workflow as cancelled:
 		if (masterkeyFileProvisionLock.awaitingInteraction().get()) {
-			LOG.debug("Unlock canceled by user.");
-			masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED);
+			LOG.debug("Unlock cancelled by user.");
+			masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELLED);
 		}
 	}
 

+ 44 - 0
src/main/resources/fxml/hub_auth.fxml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.image.Image?>
+<?import javafx.scene.image.ImageView?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.text.Text?>
+<?import javafx.scene.text.TextFlow?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.keyloading.hub.AuthController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<HBox spacing="12" VBox.vgrow="ALWAYS">
+			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" cache="true">
+				<Image url="@../img/bot/bot.png"/>
+			</ImageView>
+			<TextFlow visible="${controller.ready}" managed="${controller.ready}">
+				<Text text="TODO: please login via " />
+				<Hyperlink styleClass="hyperlink-underline" text="${controller.authUriHost}" onAction="#openBrowser"/>
+			</TextFlow>
+			<FontAwesome5Spinner glyphSize="12" visible="${!controller.ready}" managed="${!controller.ready}"/>
+		</HBox>
+
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+C">
+				<buttons>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</VBox>

+ 1 - 1
src/main/resources/fxml/hub_p12_create.fxml

@@ -17,7 +17,7 @@
 	</padding>
 	<children>
 		<HBox spacing="12" VBox.vgrow="ALWAYS">
-			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" visible="false">
+			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" cache="true">
 				<Image url="@../img/bot/bot.png"/>
 			</ImageView>
 			<fx:include fx:id="newPassword" source="new_password.fxml" disable="${controller.userInteractionDisabled}"/>

+ 1 - 1
src/main/resources/fxml/hub_p12_load.fxml

@@ -21,7 +21,7 @@
 	</padding>
 	<children>
 		<HBox spacing="12" VBox.vgrow="ALWAYS">
-			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" visible="false">
+			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" cache="true">
 				<Image url="@../img/bot/bot.png"/>
 			</ImageView>
 			<VBox spacing="6" HBox.hgrow="ALWAYS">