diff --git a/pom.xml b/pom.xml index 2fdd357f7..45fc9ac1b 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh - 2.1.0-beta9 + 2.1.0 1.0.0-rc1 1.0.0-beta2 1.0.0-beta2 diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index c9f687ea4..aec859702 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -14,6 +14,7 @@ public enum FxmlFile { HEALTH_START("/fxml/health_start.fxml"), // HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // + HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), // HUB_P12("/fxml/hub_p12.fxml"), // HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java index 9f02de59a..c587889f6 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java @@ -127,28 +127,14 @@ class AuthFlow implements AutoCloseable { .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream()); if (response.statusCode() == 200) { - var json = parseBody(response); + var json = HttpHelper.parseBody(response); return json.getAsJsonObject().get("access_token").getAsString(); } else { - LOG.error("Unexpected HTTP response {}: {}", response.statusCode(), readBody(response)); + LOG.error("Unexpected HTTP response {}: {}", response.statusCode(), HttpHelper.readBody(response)); throw new IOException("Unexpected HTTP response code " + response.statusCode()); } } - private String readBody(HttpResponse response) throws IOException { - try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - return CharStreams.toString(reader); - } - } - - private JsonElement parseBody(HttpResponse response) throws IOException { - try (InputStream in = response.body(); Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - return JsonParser.parseReader(reader); - } catch (JsonParseException e) { - throw new IOException("Failed to parse JSON", e); - } - } - private URI appendQueryParams(URI uri, Map params) { var oldParams = Splitter.on("&").omitEmptyStrings().splitToStream(Strings.nullToEmpty(uri.getQuery())); var newParams = paramString(params); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java new file mode 100644 index 000000000..42114f29f --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java @@ -0,0 +1,141 @@ +package org.cryptomator.ui.keyloading.hub; + +import dagger.Lazy; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.ErrorComponent; +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.keyloading.KeyLoading; +import org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.application.Application; +import javafx.beans.binding.Bindings; +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.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +@KeyLoadingScoped +public class AuthFlowController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(AuthFlowController.class); + private static final String JWT_KEY_AUTH_ENDPOINT = "authEndpoint"; + private static final String JWT_KEY_TOKEN_ENDPOINT = "tokenEndpoint"; + private static final String JWT_KEY_CLIENT_ID = "clientId"; + + private final Application application; + private final Stage window; + private final ExecutorService executor; + private final Vault vault; + private final AtomicReference tokenRef; + private final UserInteractionLock result; + private final Lazy receiveKeyScene; + private final ErrorComponent.Builder errorComponent; + private final ObjectProperty authUri; + private final StringBinding authHost; + private AuthFlowTask task; + + @Inject + public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @KeyLoading Vault vault, @Named("bearerToken") AtomicReference tokenRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene, ErrorComponent.Builder errorComponent) { + this.application = application; + this.window = window; + this.executor = executor; + this.vault = vault; + this.tokenRef = tokenRef; + this.result = result; + this.receiveKeyScene = receiveKeyScene; + this.errorComponent = errorComponent; + this.authUri = new SimpleObjectProperty<>(); + this.authHost = Bindings.createStringBinding(this::getAuthHost, authUri); + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); + } + + @FXML + public void initialize() { + assert task == null; + try { + task = setupTask(); + task.setOnFailed(this::authFailed); + task.setOnSucceeded(this::authSucceeded); + executor.submit(task); + } catch (IOException e) { + LOG.error("Unreadable vault config", e); + errorComponent.cause(e).window(window).build().showErrorScene(); + } + } + + @FXML + public void browse() { + application.getHostServices().showDocument(authUri.get().toString()); + } + + @FXML + public void cancel() { + window.close(); + } + + private AuthFlowTask setupTask() throws IOException { + var authUri = URI.create(vault.getUnverifiedVaultConfig().get(JWT_KEY_AUTH_ENDPOINT).asString()); + var tokenUri = URI.create(vault.getUnverifiedVaultConfig().get(JWT_KEY_TOKEN_ENDPOINT).asString()); + var clientId = vault.getUnverifiedVaultConfig().get(JWT_KEY_CLIENT_ID).asString(); + return new AuthFlowTask(authUri, tokenUri, clientId, this::setAuthUri); + } + + private void setAuthUri(URI uri) { + authUri.set(uri); + browse(); + } + + private void windowClosed(WindowEvent windowEvent) { + // stop server, if it is still running + task.cancel(); + // if not already interacted, mark this workflow as cancelled: + if (result.awaitingInteraction().get()) { + LOG.debug("Authorization cancelled by user."); + result.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED); + } + } + + private void authSucceeded(WorkerStateEvent workerStateEvent) { + tokenRef.set(task.getValue()); + window.requestFocus(); + window.setScene(receiveKeyScene.get()); + } + + private void authFailed(WorkerStateEvent workerStateEvent) { + result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED); + window.requestFocus(); + var exception = workerStateEvent.getSource().getException(); + LOG.error("Authentication failed", exception); + errorComponent.cause(exception).window(window).build().showErrorScene(); + } + + /* Getter/Setter */ + + public StringBinding authHostProperty() { + return authHost; + } + + public String getAuthHost() { + var uri = authUri.get(); + if (uri == null) { + return ""; + } else { + return uri.getAuthority().toString(); + } + } +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java new file mode 100644 index 000000000..4f303b712 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java @@ -0,0 +1,33 @@ +package org.cryptomator.ui.keyloading.hub; + +import javafx.application.Platform; +import javafx.concurrent.Task; +import java.net.URI; +import java.util.function.Consumer; + +class AuthFlowTask extends Task { + + private final URI authUri; + private final URI tokenUri; + private final String clientId; + private final Consumer 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 AuthFlowTask(URI authUri, URI tokenUri, String clientId, Consumer redirectUriConsumer) { + this.authUri = authUri; + this.tokenUri = tokenUri; + this.clientId = clientId; + this.redirectUriConsumer = redirectUriConsumer; + } + + @Override + protected String call() throws Exception { + try (var authFlow = AuthFlow.init(authUri, tokenUri, clientId)) { + return authFlow.run(uri -> Platform.runLater(() -> redirectUriConsumer.accept(uri))); + } + } +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java deleted file mode 100644 index bab4d6cf9..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java +++ /dev/null @@ -1,35 +0,0 @@ -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 { - - private static final Logger LOG = LoggerFactory.getLogger(AuthReceiveTask.class); - - private final Consumer 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 redirectUriConsumer) { - this.redirectUriConsumer = redirectUriConsumer; - } - - @Override - protected EciesParams 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(); - } - } -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java deleted file mode 100644 index bb2c5b76a..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.cryptomator.ui.keyloading.hub; - -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -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.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; - -/** - * 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"} - """; - - private final Server server; - private final ServerConnector connector; - private final CallbackServlet servlet; - - private AuthReceiver(Server server, ServerConnector connector, CallbackServlet servlet) { - assert server.isRunning(); - this.server = server; - this.connector = connector; - this.servlet = servlet; - } - - 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 { - 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(context); - server.start(); - return new AuthReceiver(server, connector, servlet); - } - - public EciesParams receive() throws InterruptedException { - return servlet.receivedKeys.take(); - } - - @Override - public void close() throws Exception { - server.stop(); - } - - private static class CallbackServlet extends HttpServlet { - - private final BlockingQueue receivedKeys = new LinkedBlockingQueue<>(); - - // TODO change to POST? - @Override - 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; - if (m != null && epk != 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 (m != null && epk != null) { - receivedKeys.add(new EciesParams(m, epk)); - } - } - } -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java new file mode 100644 index 000000000..7de2902be --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java @@ -0,0 +1,31 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.google.common.io.CharStreams; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; + +class HttpHelper { + + public static String readBody(HttpResponse response) throws IOException { + try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + return CharStreams.toString(reader); + } + } + + public static JsonElement parseBody(HttpResponse response) throws IOException { + try (InputStream in = response.body(); Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + return JsonParser.parseReader(reader); + } catch (JsonParseException e) { + throw new IOException("Failed to parse JSON", e); + } + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index 9c5d7dd94..5de793857 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -17,6 +17,7 @@ import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingScoped; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; +import javax.inject.Named; import javafx.scene.Scene; import java.net.URI; import java.security.KeyPair; @@ -26,7 +27,7 @@ import java.util.concurrent.atomic.AtomicReference; @Module public abstract class HubKeyLoadingModule { - public enum AuthFlow { + public enum HubLoadingResult { SUCCESS, FAILED, CANCELLED @@ -39,23 +40,24 @@ public abstract class HubKeyLoadingModule { } @Provides + @Named("bearerToken") @KeyLoadingScoped - static AtomicReference provideAuthParamsRef() { + static AtomicReference provideBearerTokenRef() { return new AtomicReference<>(); } @Provides @KeyLoadingScoped - static UserInteractionLock provideAuthFlowLock() { + static AtomicReference provideEciesParamsRef() { + return new AtomicReference<>(); + } + + @Provides + @KeyLoadingScoped + static UserInteractionLock provideResultLock() { return new UserInteractionLock<>(null); } - @Provides - @KeyLoadingScoped - static AtomicReference provideHubUri() { - return new AtomicReference<>(); - } - @Binds @IntoMap @KeyLoadingScoped @@ -75,10 +77,18 @@ public abstract class HubKeyLoadingModule { return fxmlLoaders.createScene(FxmlFile.HUB_P12); } + @Provides + @FxmlScene(FxmlFile.HUB_AUTH_FLOW) + @KeyLoadingScoped + static Scene provideHubAuthFlowScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_AUTH_FLOW); + } + + @Provides @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) @KeyLoadingScoped - static Scene provideHubAuthScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + static Scene provideHubReceiveKeyScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { return fxmlLoaders.createScene(FxmlFile.HUB_RECEIVE_KEY); } @@ -97,6 +107,11 @@ public abstract class HubKeyLoadingModule { @FxControllerKey(P12CreateController.class) abstract FxController bindP12CreateController(P12CreateController controller); + @Binds + @IntoMap + @FxControllerKey(AuthFlowController.class) + abstract FxController bindAuthFlowController(AuthFlowController controller); + @Provides @IntoMap @FxControllerKey(NewPasswordController.class) 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 a7f8f41e0..1343f8cc9 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -19,7 +19,6 @@ import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import java.net.URI; -import java.net.URISyntaxException; import java.security.KeyPair; import java.util.concurrent.atomic.AtomicReference; @@ -33,28 +32,26 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { private final Vault vault; private final Stage window; private final Lazy p12LoadingScene; - private final UserInteractionLock userInteraction; - private final AtomicReference hubUriRef; + private final UserInteractionLock userInteraction; private final AtomicReference keyPairRef; - private final AtomicReference authParamsRef; + private final AtomicReference eciesParams; @Inject - public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock userInteraction, AtomicReference hubUriRef, AtomicReference keyPairRef, AtomicReference authParamsRef) { + public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock userInteraction, AtomicReference keyPairRef, AtomicReference eciesParams) { this.vault = vault; this.window = window; this.p12LoadingScene = p12LoadingScene; this.userInteraction = userInteraction; - this.hubUriRef = hubUriRef; this.keyPairRef = keyPairRef; - this.authParamsRef = authParamsRef; + this.eciesParams = eciesParams; } @Override public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { - hubUriRef.set(getHubUri(keyId)); + Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); try { return switch (auth()) { - case SUCCESS -> EciesHelper.decryptMasterkey(keyPairRef.get(), authParamsRef.get()); + case SUCCESS -> EciesHelper.decryptMasterkey(keyPairRef.get(), eciesParams.get()); case FAILED -> throw new MasterkeyLoadingFailedException("failed to load keypair"); case CANCELLED -> throw new UnlockCancelledException("User cancelled auth workflow"); }; @@ -72,17 +69,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { } } - private URI getHubUri(URI keyId) { - Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); - var hubUriScheme = keyId.getScheme().substring(SCHEME_PREFIX.length()); - try { - return new URI(hubUriScheme, keyId.getSchemeSpecificPart(), keyId.getFragment()); - } catch (URISyntaxException e) { - throw new IllegalStateException("URI constructed from params known to be valid", e); - } - } - - private HubKeyLoadingModule.AuthFlow auth() throws InterruptedException { + private HubKeyLoadingModule.HubLoadingResult auth() throws InterruptedException { Platform.runLater(() -> { window.setScene(p12LoadingScene.get()); window.show(); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java index 84df68749..e9a70dc8d 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java @@ -20,10 +20,10 @@ public class P12Controller implements FxController { private final Stage window; private final Environment env; - private final UserInteractionLock userInteraction; + private final UserInteractionLock userInteraction; @Inject - public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock userInteraction) { + public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock userInteraction) { this.window = window; this.env = env; this.userInteraction = userInteraction; @@ -34,7 +34,7 @@ public class P12Controller implements FxController { // if not already interacted, mark this workflow as cancelled: if (userInteraction.awaitingInteraction().get()) { LOG.debug("P12 loading cancelled by user."); - userInteraction.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED); + userInteraction.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java index 0700d496b..3f98e62ca 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java @@ -39,7 +39,7 @@ public class P12CreateController implements FxController { private final Stage window; private final Environment env; private final AtomicReference keyPairRef; - private final Lazy receiveKeyScene; + private final Lazy authFlowScene; private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty(); private final ObjectBinding unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled); @@ -48,11 +48,11 @@ public class P12CreateController implements FxController { public NewPasswordController newPasswordController; @Inject - public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene) { + public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene) { this.window = window; this.env = env; this.keyPairRef = keyPairRef; - this.receiveKeyScene = receiveKeyScene; + this.authFlowScene = authFlowScene; this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); } @@ -81,7 +81,7 @@ public class P12CreateController implements FxController { var keyPair = P12AccessHelper.createNew(p12File, pw); setKeyPair(keyPair); LOG.debug("Created .p12 file {}", p12File); - window.setScene(receiveKeyScene.get()); + window.setScene(authFlowScene.get()); } catch (IOException e) { LOG.error("Failed to load .p12 file.", e); // TODO diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java index 9759e92e9..24af08384 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java @@ -41,18 +41,18 @@ public class P12LoadController implements FxController { private final Stage window; private final Environment env; private final AtomicReference keyPairRef; - private final Lazy receiveKeyScene; + private final Lazy authFlowScene; private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty(); private final ObjectBinding unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled); public NiceSecurePasswordField passwordField; @Inject - public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene) { + public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene) { this.window = window; this.env = env; this.keyPairRef = keyPairRef; - this.receiveKeyScene = receiveKeyScene; + this.authFlowScene = authFlowScene; this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); } @@ -78,7 +78,7 @@ public class P12LoadController implements FxController { var keyPair = P12AccessHelper.loadExisting(p12File, pw); setKeyPair(keyPair); LOG.debug("Loaded .p12 file {}", p12File); - window.setScene(receiveKeyScene.get()); + window.setScene(authFlowScene.get()); } catch (InvalidPassphraseException e) { LOG.warn("Invalid passphrase entered for .p12 file"); Animations.createShakeWindowAnimation(window).playFromStart(); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java index 77f33ee5e..7597f2b6e 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -2,6 +2,8 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; +import com.google.gson.JsonElement; +import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.UserInteractionLock; @@ -11,17 +13,25 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; import javafx.application.Application; -import javafx.beans.binding.BooleanBinding; +import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.concurrent.WorkerStateEvent; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.fxml.FXML; +import javafx.scene.control.TextField; import javafx.stage.Stage; import javafx.stage.WindowEvent; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.security.KeyPair; import java.util.Objects; import java.util.concurrent.ExecutorService; @@ -31,53 +41,88 @@ import java.util.concurrent.atomic.AtomicReference; public class ReceiveKeyController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class); + private static final String SCHEME_PREFIX = "hub+"; - private final Application application; - private final ExecutorService executor; private final Stage window; - private final KeyPair keyPair; - private final AtomicReference authParamsRef; - private final UserInteractionLock authFlowLock; - private final AtomicReference hubUriRef; + private final String bearerToken; + private final AtomicReference eciesParamsRef; + private final UserInteractionLock result; private final ErrorComponent.Builder errorComponent; - private final ObjectProperty redirectUriRef; - private final BooleanBinding ready; - private final AuthReceiveTask receiveTask; + private final URI vaultBaseUri; + private final ObjectProperty state = new SimpleObjectProperty<>(ReceiveKeyState.LOADING); + private final HttpClient httpClient; + + public TextField deviceName; @Inject - public ReceiveKeyController(Application application, ExecutorService executor, @KeyLoading Stage window, AtomicReference keyPairRef, AtomicReference authParamsRef, UserInteractionLock authFlowLock, AtomicReference hubUriRef, ErrorComponent.Builder errorComponent) { - this.application = application; - this.executor = executor; + public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, AtomicReference keyPairRef, @Named("bearerToken") AtomicReference tokenRef, AtomicReference eciesParamsRef, UserInteractionLock result, ErrorComponent.Builder errorComponent) { this.window = window; - this.keyPair = Objects.requireNonNull(keyPairRef.get()); - this.authParamsRef = authParamsRef; - this.authFlowLock = authFlowLock; - this.hubUriRef = hubUriRef; + this.bearerToken = Objects.requireNonNull(tokenRef.get()); + this.eciesParamsRef = eciesParamsRef; + this.result = result; this.errorComponent = errorComponent; - this.redirectUriRef = new SimpleObjectProperty<>(); - this.ready = redirectUriRef.isNotNull(); - this.receiveTask = new AuthReceiveTask(redirectUriRef::set); + this.vaultBaseUri = getVaultBaseUri(vault); this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); + this.httpClient = HttpClient.newBuilder().executor(executor).build(); +// var deviceKey = BaseEncoding.base64Url().omitPadding().encode(keyPairRef.get().getPublic().getEncoded()); +// LOG.info("deviceKey {}", deviceKey); } @FXML public void initialize() { - Preconditions.checkState(hubUriRef.get() != null); - receiveTask.setOnSucceeded(this::receivedKey); - receiveTask.setOnFailed(this::keyRetrievalFailed); - executor.submit(receiveTask); + var keyUri = appendPath(vaultBaseUri, "/keys/desktop-app-3000"); // TODO use actual device id + var request = HttpRequest.newBuilder(keyUri) // + .header("Authorization", "Bearer " + bearerToken) // + .GET() // + .build(); + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) // + .whenCompleteAsync(this::loadedExistingKey, Platform::runLater); } - 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 loadedExistingKey(HttpResponse response, Throwable error) { + if (error != null) { + retrievalFailed(error); + } else { + switch (response.statusCode()) { + case 200 -> retrievalSucceeded(response); + case 404 -> state.set(ReceiveKeyState.NEEDS_REGISTRATION); + default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode())); + } + } } - private void receivedKey(WorkerStateEvent workerStateEvent) { - authParamsRef.set(Objects.requireNonNull(receiveTask.getValue())); - authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.SUCCESS); - window.close(); + @FXML + public void register() { + Preconditions.checkArgument(deviceName.textProperty().isNotEmpty().get(), "device name must not be empty"); +// var keyUri = appendPath(vaultBaseUri, "../../devices/desktop-app-3000"); +// var request = HttpRequest.newBuilder(keyUri) // +// .header("Authorization", "Bearer " + bearerToken) // +// .GET() // +// .build(); +// httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) // +// .whenCompleteAsync(this::loadedExistingKey, Platform::runLater); + } + + private void retrievalSucceeded(HttpResponse response) { + try { + var json = HttpHelper.parseBody(response); + Preconditions.checkArgument(json.isJsonObject()); + Preconditions.checkArgument(json.getAsJsonObject().has("device_specific_masterkey")); + Preconditions.checkArgument(json.getAsJsonObject().has("ephemeral_public_key")); + var m = json.getAsJsonObject().get("device_specific_masterkey").getAsString(); + var epk = json.getAsJsonObject().get("ephemeral_public_key").getAsString(); + eciesParamsRef.set(new EciesParams(m, epk)); + result.interacted(HubKeyLoadingModule.HubLoadingResult.SUCCESS); + window.close(); + } catch (IOException | IllegalArgumentException e) { + retrievalFailed(e); + } + } + + private void retrievalFailed(Throwable cause) { + result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED); + LOG.error("Key retrieval failed", cause); + errorComponent.cause(cause).window(window).build().showErrorScene(); } @FXML @@ -86,44 +131,41 @@ public class ReceiveKeyController implements FxController { } 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()) { + if (result.awaitingInteraction().get()) { LOG.debug("Authorization cancelled by user."); - authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED); + result.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED); } } - @FXML - public void openBrowser() { - 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); + private static URI appendPath(URI base, String path) { + try { + var newPath = base.getPath() + path; + return new URI(base.getScheme(), base.getAuthority(), newPath, base.getQuery(), base.getFragment()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't append '" + path + "' to URI: " + base, e); + } } + private static URI getVaultBaseUri(Vault vault) { + try { + var kid = vault.getUnverifiedVaultConfig().getKeyId(); + assert kid.getScheme().startsWith(SCHEME_PREFIX); + var hubUriScheme = kid.getScheme().substring(SCHEME_PREFIX.length()); + return new URI(hubUriScheme, kid.getSchemeSpecificPart(), kid.getFragment()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (URISyntaxException e) { + throw new IllegalStateException("URI constructed from params known to be valid", e); + } + } /* Getter/Setter */ - public String getHubUriHost() { - var hubUri = hubUriRef.get(); - if (hubUri == null) { - return null; - } else { - return hubUri.getHost(); - } + public ObjectProperty stateProperty() { + return state; } - public BooleanBinding readyProperty() { - return ready; - } - - public boolean isReady() { - return ready.get(); + public ReceiveKeyState getState() { + return state.get(); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyState.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyState.java new file mode 100644 index 000000000..ae1562ded --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyState.java @@ -0,0 +1,6 @@ +package org.cryptomator.ui.keyloading.hub; + +public enum ReceiveKeyState { + LOADING, + NEEDS_REGISTRATION +} diff --git a/src/main/resources/fxml/hub_auth_flow.fxml b/src/main/resources/fxml/hub_auth_flow.fxml new file mode 100644 index 000000000..a6d086ea3 --- /dev/null +++ b/src/main/resources/fxml/hub_auth_flow.fxml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +