use new auth flow

talking directly to Authorization Server and Resource Server instead of web frontend
This commit is contained in:
Sebastian Stenzel
2021-08-12 16:18:22 +02:00
parent 75644a35ec
commit a3a96496b6
19 changed files with 428 additions and 309 deletions

View File

@@ -27,7 +27,7 @@
<nonModularGroupIds>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</nonModularGroupIds>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.1.0-beta9</cryptomator.cryptofs.version>
<cryptomator.cryptofs.version>2.1.0</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.0.0-rc1</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>

View File

@@ -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"), //

View File

@@ -127,28 +127,14 @@ class AuthFlow implements AutoCloseable {
.build();
HttpResponse<InputStream> 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<InputStream> response) throws IOException {
try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
}
}
private JsonElement parseBody(HttpResponse<InputStream> 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<String, String> params) {
var oldParams = Splitter.on("&").omitEmptyStrings().splitToStream(Strings.nullToEmpty(uri.getQuery()));
var newParams = paramString(params);

View File

@@ -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<String> tokenRef;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
private final Lazy<Scene> receiveKeyScene;
private final ErrorComponent.Builder errorComponent;
private final ObjectProperty<URI> authUri;
private final StringBinding authHost;
private AuthFlowTask task;
@Inject
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @KeyLoading Vault vault, @Named("bearerToken") AtomicReference<String> tokenRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> 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();
}
}
}

View File

@@ -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<String> {
private final URI authUri;
private final URI tokenUri;
private final String clientId;
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 AuthFlowTask(URI authUri, URI tokenUri, String clientId, Consumer<URI> 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)));
}
}
}

View File

@@ -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<EciesParams> {
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 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();
}
}
}

View File

@@ -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:
* <p>
* We're spawning a local http server on a system-assigned high port and
* use <code>http://127.0.0.1:{PORT}/success</code> as a redirect URI.
* <p>
* 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<EciesParams> 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));
}
}
}
}

View File

@@ -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<InputStream> 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<InputStream> 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);
}
}
}

View File

@@ -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<EciesParams> provideAuthParamsRef() {
static AtomicReference<String> provideBearerTokenRef() {
return new AtomicReference<>();
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<AuthFlow> provideAuthFlowLock() {
static AtomicReference<EciesParams> provideEciesParamsRef() {
return new AtomicReference<>();
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<HubLoadingResult> provideResultLock() {
return new UserInteractionLock<>(null);
}
@Provides
@KeyLoadingScoped
static AtomicReference<URI> 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)

View File

@@ -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<Scene> p12LoadingScene;
private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction;
private final AtomicReference<URI> hubUriRef;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
private final AtomicReference<KeyPair> keyPairRef;
private final AtomicReference<EciesParams> authParamsRef;
private final AtomicReference<EciesParams> eciesParams;
@Inject
public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction, AtomicReference<URI> hubUriRef, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> authParamsRef) {
public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> 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();

View File

@@ -20,10 +20,10 @@ public class P12Controller implements FxController {
private final Stage window;
private final Environment env;
private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
@Inject
public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction) {
public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> 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);
}
}

View File

@@ -39,7 +39,7 @@ public class P12CreateController implements FxController {
private final Stage window;
private final Environment env;
private final AtomicReference<KeyPair> keyPairRef;
private final Lazy<Scene> receiveKeyScene;
private final Lazy<Scene> authFlowScene;
private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
private final ObjectBinding<ContentDisplay> 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<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> 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

View File

@@ -41,18 +41,18 @@ public class P12LoadController implements FxController {
private final Stage window;
private final Environment env;
private final AtomicReference<KeyPair> keyPairRef;
private final Lazy<Scene> receiveKeyScene;
private final Lazy<Scene> authFlowScene;
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, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> 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();

View File

@@ -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<EciesParams> authParamsRef;
private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> authFlowLock;
private final AtomicReference<URI> hubUriRef;
private final String bearerToken;
private final AtomicReference<EciesParams> eciesParamsRef;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
private final ErrorComponent.Builder errorComponent;
private final ObjectProperty<URI> redirectUriRef;
private final BooleanBinding ready;
private final AuthReceiveTask receiveTask;
private final URI vaultBaseUri;
private final ObjectProperty<ReceiveKeyState> state = new SimpleObjectProperty<>(ReceiveKeyState.LOADING);
private final HttpClient httpClient;
public TextField deviceName;
@Inject
public ReceiveKeyController(Application application, ExecutorService executor, @KeyLoading Stage window, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> authParamsRef, UserInteractionLock<HubKeyLoadingModule.AuthFlow> authFlowLock, AtomicReference<URI> hubUriRef, ErrorComponent.Builder errorComponent) {
this.application = application;
this.executor = executor;
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, AtomicReference<KeyPair> keyPairRef, @Named("bearerToken") AtomicReference<String> tokenRef, AtomicReference<EciesParams> eciesParamsRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> 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<InputStream> 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<InputStream> 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<ReceiveKeyState> stateProperty() {
return state;
}
public BooleanBinding readyProperty() {
return ready;
}
public boolean isReady() {
return ready.get();
public ReceiveKeyState getState() {
return state.get();
}
}

View File

@@ -0,0 +1,6 @@
package org.cryptomator.ui.keyloading.hub;
public enum ReceiveKeyState {
LOADING,
NEEDS_REGISTRATION
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<?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.AuthFlowController"
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.authHost.empty}" managed="${!controller.authHost.empty}">
<Text text="TODO: please login via " />
<Hyperlink styleClass="hyperlink-underline" text="${controller.authHost}" onAction="#browse"/>
</TextFlow>
</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>

View File

@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
<?import org.cryptomator.ui.keyloading.hub.ReceiveKeyState?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?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.ReceiveKeyController"
@@ -18,6 +18,10 @@
maxWidth="400"
minHeight="145"
spacing="12">
<fx:define>
<ReceiveKeyState fx:id="loading" fx:constant="LOADING" />
<ReceiveKeyState fx:id="needsRegistration" fx:constant="NEEDS_REGISTRATION"/>
</fx:define>
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
@@ -26,17 +30,20 @@
<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.hubUriHost}" onAction="#openBrowser"/>
</TextFlow>
<FontAwesome5Spinner glyphSize="12" visible="${!controller.ready}" managed="${!controller.ready}"/>
<FontAwesome5Spinner glyphSize="12" visible="${controller.state == loading}" managed="${controller.state == loading}"/>
<VBox spacing="6" visible="${controller.state == needsRegistration}" managed="${controller.state == needsRegistration}">
<Label text="TODO: register device" labelFor="$locationTextField"/>
<TextField fx:id="deviceName" promptText="TODO: device name" VBox.vgrow="ALWAYS"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
<Button text="TODO: register device" ButtonBar.buttonData="NEXT_FORWARD" visible="${controller.state == needsRegistration}" managed="${controller.state == needsRegistration}" defaultButton="true" onAction="#register"/>
</buttons>
</ButtonBar>
</VBox>

View File

@@ -11,7 +11,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/.
Cryptomator uses 43 third-party dependencies under the following licenses:
Cryptomator uses 46 third-party dependencies under the following licenses:
Apache License v2.0:
- jffi (com.github.jnr:jffi:1.2.23 - http://github.com/jnr/jffi)
- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
@@ -33,7 +33,10 @@ Cryptomator uses 43 third-party dependencies under the following licenses:
- Jetty :: Security (org.eclipse.jetty:jetty-security:10.0.6 - https://eclipse.org/jetty/jetty-security)
- Jetty :: Server Core (org.eclipse.jetty:jetty-server:10.0.6 - https://eclipse.org/jetty/jetty-server)
- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - https://eclipse.org/jetty/jetty-servlet)
- Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:10.0.6 - https://eclipse.org/jetty/jetty-servlets)
- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:10.0.6 - https://eclipse.org/jetty/jetty-webapp)
- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:10.0.6 - https://eclipse.org/jetty/jetty-xml)
- Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - https://eclipse.org/jetty/jetty-servlet-api)
BSD:
- asm (org.ow2.asm:asm:7.1 - http://asm.ow2.org/)
@@ -53,7 +56,10 @@ Cryptomator uses 43 third-party dependencies under the following licenses:
- Jetty :: Security (org.eclipse.jetty:jetty-security:10.0.6 - https://eclipse.org/jetty/jetty-security)
- Jetty :: Server Core (org.eclipse.jetty:jetty-server:10.0.6 - https://eclipse.org/jetty/jetty-server)
- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - https://eclipse.org/jetty/jetty-servlet)
- Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:10.0.6 - https://eclipse.org/jetty/jetty-servlets)
- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:10.0.6 - https://eclipse.org/jetty/jetty-webapp)
- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:10.0.6 - https://eclipse.org/jetty/jetty-xml)
Eclipse Public License - v 1.0:
- Logback Classic Module (ch.qos.logback:logback-classic:1.2.3 - http://logback.qos.ch/logback-classic)
- Logback Core Module (ch.qos.logback:logback-core:1.2.3 - http://logback.qos.ch/logback-core)

View File

@@ -1,23 +0,0 @@
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();
}
}
}