adjusted to new vault config format and unlock status codes

This commit is contained in:
Sebastian Stenzel
2021-08-13 19:59:31 +02:00
parent e46072c726
commit 1322b872b6
16 changed files with 212 additions and 132 deletions

View File

@@ -42,6 +42,8 @@ module org.cryptomator.desktop {
uses TrayIntegrationProvider;
uses UiAppearanceProvider;
exports org.cryptomator.ui.keyloading.hub to com.fasterxml.jackson.databind;
opens org.cryptomator.common.settings to com.google.gson;
opens org.cryptomator.common to javafx.fxml;

View File

@@ -17,6 +17,7 @@ public enum FxmlFile {
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
HUB_P12("/fxml/hub_p12.fxml"), //
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
LOCK_FORCED("/fxml/lock_forced.fxml"), //
LOCK_FAILED("/fxml/lock_failed.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //

View File

@@ -46,15 +46,15 @@ class AuthFlow implements AutoCloseable {
public static final Escaper QUERY_STRING_ESCAPER = new PercentEscaper("-_.!~*'()@:$,;/?", false);
private final AuthFlowReceiver receiver;
private final URI authEndpoint;
private final URI tokenEndpoint;
private final String clientId;
private final URI authEndpoint; // see https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
private final URI tokenEndpoint; // see https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
private final String clientId; // see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
private AuthFlow(AuthFlowReceiver receiver, URI authEndpoint, URI tokenEndpoint, String clientId) {
private AuthFlow(AuthFlowReceiver receiver, HubConfig hubConfig) {
this.receiver = receiver;
this.authEndpoint = authEndpoint;
this.tokenEndpoint = tokenEndpoint;
this.clientId = clientId;
this.authEndpoint = URI.create(hubConfig.authEndpoint);
this.tokenEndpoint = URI.create(hubConfig.tokenEndpoint);
this.clientId = hubConfig.clientId;
}
/**
@@ -62,15 +62,13 @@ class AuthFlow implements AutoCloseable {
* <p>
* This will start a loopback server, so make sure to {@link #close()} this resource.
*
* @param authEndpoint Address of the <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.1">Authorization Endpoint</a>
* @param tokenEndpoint Address of the <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.2">Token Endpoint</a>
* @param clientId The <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1"><code>client_id</code></a>
* @param hubConfig A hub config object containing parameters required for this auth flow
* @return An authorization flow
* @throws Exception In case of any problems starting the server
*/
public static AuthFlow init(URI authEndpoint, URI tokenEndpoint, String clientId) throws Exception {
var receiver = AuthFlowReceiver.start();
return new AuthFlow(receiver, authEndpoint, tokenEndpoint, clientId);
public static AuthFlow init(HubConfig hubConfig) throws Exception {
var receiver = AuthFlowReceiver.start(hubConfig);
return new AuthFlow(receiver, hubConfig);
}
/**

View File

@@ -1,7 +1,6 @@
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;
@@ -24,7 +23,6 @@ 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;
@@ -33,14 +31,11 @@ import java.util.concurrent.atomic.AtomicReference;
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 HubConfig hubConfig;
private final AtomicReference<String> tokenRef;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
private final Lazy<Scene> receiveKeyScene;
@@ -50,11 +45,11 @@ public class AuthFlowController implements FxController {
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) {
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @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.hubConfig = hubConfig;
this.tokenRef = tokenRef;
this.result = result;
this.receiveKeyScene = receiveKeyScene;
@@ -67,15 +62,10 @@ public class AuthFlowController implements FxController {
@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();
}
task = new AuthFlowTask(hubConfig, this::setAuthUri);;
task.setOnFailed(this::authFailed);
task.setOnSucceeded(this::authSucceeded);
executor.submit(task);
}
@FXML
@@ -88,13 +78,6 @@ public class AuthFlowController implements FxController {
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();
@@ -138,4 +121,5 @@ public class AuthFlowController implements FxController {
return uri.getAuthority().toString();
}
}
}

View File

@@ -40,18 +40,20 @@ class AuthFlowReceiver implements AutoCloseable {
private final Server server;
private final ServerConnector connector;
private final CallbackServlet servlet;
private final HubConfig hubConfig;
private AuthFlowReceiver(Server server, ServerConnector connector, CallbackServlet servlet) {
private AuthFlowReceiver(Server server, ServerConnector connector, CallbackServlet servlet, HubConfig hubConfig) {
this.server = server;
this.connector = connector;
this.servlet = servlet;
this.hubConfig = hubConfig;
}
public static AuthFlowReceiver start() throws Exception {
public static AuthFlowReceiver start(HubConfig hubConfig) throws Exception {
var server = new Server();
var context = new ServletContextHandler();
var servlet = new CallbackServlet();
var servlet = new CallbackServlet(hubConfig);
context.addServlet(new ServletHolder(servlet), CALLBACK_PATH);
var connector = new ServerConnector(server);
@@ -60,7 +62,7 @@ class AuthFlowReceiver implements AutoCloseable {
server.setConnectors(new Connector[]{connector});
server.setHandler(context);
server.start();
return new AuthFlowReceiver(server, connector, servlet);
return new AuthFlowReceiver(server, connector, servlet, hubConfig);
}
public String getRedirectUri() {
@@ -81,6 +83,11 @@ class AuthFlowReceiver implements AutoCloseable {
private static class CallbackServlet extends HttpServlet {
private final BlockingQueue<Callback> callback = new LinkedBlockingQueue<>();
private final HubConfig hubConfig;
public CallbackServlet(HubConfig hubConfig) {
this.hubConfig = hubConfig;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
@@ -88,14 +95,15 @@ class AuthFlowReceiver implements AutoCloseable {
var code = req.getParameter("code");
var state = req.getParameter("state");
// TODO 302 use redirect to configurable site
res.setContentType("text/html;charset=utf-8");
res.getWriter().write(HTML_SUCCESS);
res.getWriter().flush();
res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
if (error == null && code != null) {
res.setHeader("Location", hubConfig.unlockSuccessUrl);
} else {
res.setHeader("Location", hubConfig.unlockErrorUrl);
}
callback.add(new Callback(error, code, state));
}
}
}

View File

@@ -7,27 +7,25 @@ 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 hubConfig Configuration object holding parameters required by {@link AuthFlow}
* @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;
public AuthFlowTask(HubConfig hubConfig, Consumer<URI> redirectUriConsumer) {
this.hubConfig = hubConfig;
this.redirectUriConsumer = redirectUriConsumer;
}
@Override
protected String call() throws Exception {
try (var authFlow = AuthFlow.init(authUri, tokenUri, clientId)) {
try (var authFlow = AuthFlow.init(hubConfig)) {
return authFlow.run(uri -> Platform.runLater(() -> redirectUriConsumer.accept(uri)));
}
}
private final HubConfig hubConfig;
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.keyloading.hub;
// needs to be accessible by JSON decoder
public class HubConfig {
public String clientId;
public String authEndpoint;
public String tokenEndpoint;
public String deviceRegistrationUrl;
public String unlockSuccessUrl;
public String unlockErrorUrl;
}

View File

@@ -5,6 +5,7 @@ import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
@@ -19,6 +20,8 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Named;
import javafx.scene.Scene;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.security.KeyPair;
import java.util.ResourceBundle;
@@ -33,6 +36,16 @@ public abstract class HubKeyLoadingModule {
CANCELLED
}
@Provides
@KeyLoadingScoped
static HubConfig provideHubConfig(@KeyLoading Vault vault) {
try {
return vault.getUnverifiedVaultConfig().getHeader("hub", HubConfig.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Provides
@KeyLoadingScoped
static AtomicReference<KeyPair> provideKeyPair() {
@@ -92,6 +105,14 @@ public abstract class HubKeyLoadingModule {
return fxmlLoaders.createScene(FxmlFile.HUB_RECEIVE_KEY);
}
@Provides
@FxmlScene(FxmlFile.HUB_REGISTER_DEVICE)
@KeyLoadingScoped
static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
}
@Binds
@IntoMap
@FxControllerKey(P12Controller.class)
@@ -124,4 +145,9 @@ public abstract class HubKeyLoadingModule {
@FxControllerKey(ReceiveKeyController.class)
abstract FxController bindReceiveKeyController(ReceiveKeyController controller);
@Binds
@IntoMap
@FxControllerKey(RegisterDeviceController.class)
abstract FxController bindRegisterDeviceController(RegisterDeviceController controller);
}

View File

@@ -29,7 +29,6 @@ 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 Vault vault;
private final Stage window;
private final Lazy<Scene> p12LoadingScene;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
@@ -37,8 +36,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
private final AtomicReference<EciesParams> eciesParams;
@Inject
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;
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> eciesParams) {
this.window = window;
this.p12LoadingScene = p12LoadingScene;
this.userInteraction = userInteraction;

View File

@@ -5,9 +5,12 @@ import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
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;
@@ -23,6 +26,7 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
@@ -47,23 +51,20 @@ public class ReceiveKeyController implements FxController {
private final Stage window;
private final String bearerToken;
private final KeyPair keyPair;
private final AtomicReference<EciesParams> eciesParamsRef;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
private final Lazy<Scene> registerDeviceScene;
private final ErrorComponent.Builder errorComponent;
private final URI vaultBaseUri;
private final ObjectProperty<ReceiveKeyState> state = new SimpleObjectProperty<>(ReceiveKeyState.LOADING);
private final HttpClient httpClient;
public TextField deviceName;
@Inject
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) {
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, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, ErrorComponent.Builder errorComponent) {
this.window = window;
this.bearerToken = Objects.requireNonNull(tokenRef.get());
this.keyPair = Objects.requireNonNull(keyPairRef.get());
this.eciesParamsRef = eciesParamsRef;
this.result = result;
this.registerDeviceScene = registerDeviceScene;
this.errorComponent = errorComponent;
this.vaultBaseUri = getVaultBaseUri(vault);
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
@@ -87,41 +88,8 @@ public class ReceiveKeyController implements FxController {
} else {
switch (response.statusCode()) {
case 200 -> retrievalSucceeded(response);
case 404 -> state.set(ReceiveKeyState.NEEDS_REGISTRATION);
default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode()));
}
}
}
@FXML
public void register() {
Preconditions.checkArgument(!Strings.isNullOrEmpty(deviceName.getText()), "device name must not be empty");
var deviceKey = BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded());
var json = """
{
"id": "desktop-app",
"name": "%s",
"publicKey": "%s"
}
""".formatted(deviceName.getText(), deviceKey); // TODO use gson
var regUri = URI.create("http://localhost:9090/devices/desktop-app"); // TODO read api base from vault config!
var request = HttpRequest.newBuilder(regUri) //
.header("Authorization", "Bearer " + bearerToken) //
.header("Content-Type", "application/json; charset=UTF-8") //
.PUT(HttpRequest.BodyPublishers.ofString(json)) //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
.whenCompleteAsync(this::createdNewDevice, Platform::runLater);
}
private void createdNewDevice(HttpResponse<InputStream> response, Throwable error) {
if (error != null) {
retrievalFailed(error);
} else {
switch (response.statusCode()) {
case 201 -> LOG.info("TODO device created, waiting for authorization");
case 409 -> LOG.info("TODO device already exists. still waiting for authorization");
case 403 -> accessNotGranted();
case 404 -> needsDeviceRegistration();
default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode()));
}
}
@@ -143,6 +111,14 @@ public class ReceiveKeyController implements FxController {
}
}
private void needsDeviceRegistration() {
window.setScene(registerDeviceScene.get());
}
private void accessNotGranted() {
LOG.warn("unauthorized device"); // TODO
}
private void retrievalFailed(Throwable cause) {
result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED);
LOG.error("Key retrieval failed", cause);
@@ -183,13 +159,5 @@ public class ReceiveKeyController implements FxController {
throw new IllegalStateException("URI constructed from params known to be valid", e);
}
}
/* Getter/Setter */
public ObjectProperty<ReceiveKeyState> stateProperty() {
return state;
}
public ReceiveKeyState getState() {
return state.get();
}
}

View File

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

View File

@@ -0,0 +1,59 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.io.BaseEncoding;
import org.cryptomator.common.vaults.Vault;
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 javax.inject.Inject;
import javafx.application.Application;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.security.KeyPair;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class RegisterDeviceController implements FxController {
private final Application application;
private final Stage window;
private final HubConfig hubConfig;
private final KeyPair keyPair;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
@Inject
public RegisterDeviceController(Application application, @KeyLoading Stage window, HubConfig hubConfig, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result) {
this.application = application;
this.window = window;
this.hubConfig = hubConfig;
this.keyPair = Objects.requireNonNull(keyPairRef.get());
this.result = result;
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
}
@FXML
public void browse() {
var deviceKey = BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded());
var url = hubConfig.deviceRegistrationUrl + "?device_key=" + deviceKey;
// TODO append further params (including hmac of shown verification code)
application.getHostServices().showDocument(url);
}
@FXML
public void close() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, mark this workflow as cancelled:
if (result.awaitingInteraction().get()) {
result.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED);
}
}
}