register device via rest api

This commit is contained in:
Sebastian Stenzel
2021-12-14 17:35:24 +01:00
parent 921dd8fe67
commit ef281f810f
6 changed files with 91 additions and 54 deletions

View File

@@ -45,6 +45,7 @@ module org.cryptomator.desktop {
exports org.cryptomator.ui.keyloading.hub to com.fasterxml.jackson.databind;
opens org.cryptomator.common.settings to com.google.gson;
opens org.cryptomator.ui.keyloading.hub to com.google.gson, javafx.fxml;
opens org.cryptomator.common to javafx.fxml;
opens org.cryptomator.common.vaults to javafx.fxml;
@@ -55,7 +56,6 @@ module org.cryptomator.desktop {
opens org.cryptomator.ui.forgetPassword to javafx.fxml;
opens org.cryptomator.ui.fxapp to javafx.fxml;
opens org.cryptomator.ui.health to javafx.fxml;
opens org.cryptomator.ui.keyloading.hub to javafx.fxml;
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
opens org.cryptomator.ui.lock to javafx.fxml;
opens org.cryptomator.ui.mainwindow to javafx.fxml;

View File

@@ -0,0 +1,9 @@
package org.cryptomator.ui.keyloading.hub;
class CreateDeviceDto {
public String id;
public String name;
public String publicKey;
}

View File

@@ -19,6 +19,7 @@ import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.net.URI;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoading

View File

@@ -10,6 +10,7 @@ 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.eclipse.jetty.io.RuntimeIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,30 +74,31 @@ public class ReceiveKeyController implements FxController {
.GET() //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
.whenCompleteAsync(this::loadedExistingKey, Platform::runLater);
.thenAcceptAsync(this::loadedExistingKey, Platform::runLater) //
.exceptionallyAsync(this::retrievalFailed, Platform::runLater);
}
private void loadedExistingKey(HttpResponse<InputStream> response, Throwable error) {
if (error != null) {
retrievalFailed(error);
} else {
private void loadedExistingKey(HttpResponse<InputStream> response) {
try {
switch (response.statusCode()) {
case 200 -> retrievalSucceeded(response);
case 403 -> accessNotGranted();
case 404 -> needsDeviceRegistration();
default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode()));
default -> throw new IOException("Unexpected response " + response.statusCode());
}
} catch (IOException e) {
throw new RuntimeIOException(e);
}
}
private void retrievalSucceeded(HttpResponse<InputStream> response) {
private void retrievalSucceeded(HttpResponse<InputStream> response) throws IOException {
try {
var string = HttpHelper.readBody(response);
jweRef.set(JWEObject.parse(string));
result.interacted(HubKeyLoadingModule.HubLoadingResult.SUCCESS);
window.close();
} catch (ParseException | IOException e) {
retrievalFailed(e);
} catch (ParseException e) {
throw new IOException("Failed to parse JWE", e);
}
}
@@ -108,10 +110,11 @@ public class ReceiveKeyController implements FxController {
window.setScene(unauthorizedScene.get());
}
private void retrievalFailed(Throwable cause) {
private Void retrievalFailed(Throwable cause) {
result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED);
LOG.error("Key retrieval failed", cause);
errorComponent.cause(cause).window(window).build().showErrorScene();
return null;
}
@FXML

View File

@@ -1,57 +1,92 @@
package org.cryptomator.ui.keyloading.hub;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.cryptomator.common.settings.DeviceKey;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.cryptolib.common.P384KeyPair;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class RegisterDeviceController implements FxController {
private final Application application;
private static final Logger LOG = LoggerFactory.getLogger(RegisterDeviceController.class);
private static final Gson GSON = new GsonBuilder().setLenient().create();
private final Stage window;
private final HubConfig hubConfig;
private final String bearerToken;
private final String deviceId;
private final P384KeyPair keyPair;
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
private final String verificationCode;
private final DecodedJWT jwt;
private final HttpClient httpClient;
public TextField deviceNameField;
@Inject
public RegisterDeviceController(Application application, SecureRandom csprng, @KeyLoading Stage window, HubConfig hubConfig, DeviceKey deviceKey, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result) {
this.application = application;
public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result, @Named("bearerToken") AtomicReference<String> bearerToken) {
this.window = window;
this.hubConfig = hubConfig;
this.deviceId = deviceId;
this.keyPair = Objects.requireNonNull(deviceKey.get());
this.result = result;
this.bearerToken = Objects.requireNonNull(bearerToken.get());
this.jwt = JWT.decode(this.bearerToken);
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.verificationCode = String.format("%06d", csprng.nextInt(1_000_000));
this.httpClient = HttpClient.newBuilder().executor(executor).build();
}
@FXML
public void browse() {
public void register() {
var keyUri = URI.create("http://localhost:9090/devices/" + deviceId); // TODO lol hubConfig.deviceRegistrationUrl
var deviceKey = keyPair.getPublic().getEncoded();
var encodedKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey);
var hashedKey = MessageDigestSupplier.SHA256.get().digest(deviceKey);
var deviceId = BaseEncoding.base16().encode(hashedKey);
var hash = computeVerificationHash(deviceId + encodedKey + verificationCode);
var url = hubConfig.deviceRegistrationUrl + "&device_key=" + encodedKey + "&device_id=" + deviceId + "&verification_hash=" + hash;
application.getHostServices().showDocument(url);
var dto = new CreateDeviceDto();
dto.id = deviceId;
dto.name = deviceNameField.getText();
dto.publicKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey);
var json = GSON.toJson(dto); // TODO: do we want to keep GSON? doesn't support records -.-
var request = HttpRequest.newBuilder(keyUri) //
.header("Authorization", "Bearer " + bearerToken) //
.header("Content-Type", "application/json").PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) //
.thenAcceptAsync(this::registrationSucceeded, Platform::runLater) //
.exceptionallyAsync(this::registrationFailed, Platform::runLater);
}
private void registrationSucceeded(HttpResponse<Void> voidHttpResponse) {
LOG.info("Registered!");
}
private Void registrationFailed(Throwable cause) {
result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED);
LOG.error("Key retrieval failed", cause);
// TODO errorComponent.cause(cause).window(window).build().showErrorScene();
return null;
}
@FXML
@@ -66,20 +101,11 @@ public class RegisterDeviceController implements FxController {
}
}
private static String computeVerificationHash(String input) {
try {
var digest = MessageDigest.getInstance("SHA-256");
digest.update(StandardCharsets.UTF_8.encode(input));
return BaseEncoding.base64Url().omitPadding().encode(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Every implementation of the Java platform is required to support SHA-256.");
}
}
/* Getter */
public String getVerificationCode() {
return verificationCode;
public String getUserName() {
return jwt.getClaim("email").asString();
}
}

View File

@@ -1,16 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?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.RegisterDeviceController"
@@ -30,11 +29,14 @@
</HBox>
<VBox spacing="6">
<Label text="This device is not yet known to Cryptomator Hub. Please register this device first." wrapText="true"/>
<TextFlow styleClass="text-flow">
<Text text="Verification Code: "/>
<Text styleClass="label-large" text="${controller.verificationCode}"/>
</TextFlow>
<Label text="TODO This device is not yet known to Cryptomator Hub. Please register this device first." wrapText="true"/>
<HBox spacing="6" alignment="CENTER_LEFT">
<FormattedLabel format="TODO User %s" arg1="${controller.userName}"/>
</HBox>
<HBox spacing="6" alignment="CENTER_LEFT">
<Label text="TODO Device Name" labelFor="$deviceNameField"/>
<TextField fx:id="deviceNameField"/>
</HBox>
</VBox>
</HBox>
@@ -42,11 +44,7 @@
<ButtonBar buttonMinWidth="120" buttonOrder="+CU">
<buttons>
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="Register Device" ButtonBar.buttonData="OTHER" defaultButton="true" onAction="#browse" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
</graphic>
</Button>
<Button text="TODO Register Device" ButtonBar.buttonData="OTHER" defaultButton="true" onAction="#register"/>
</buttons>
</ButtonBar>
</VBox>