diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
index f8ec7b854..84cac8ed2 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
@@ -1,7 +1,6 @@
package org.cryptomator.ui.keyloading.hub;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
@@ -19,6 +18,12 @@ public class HubConfig {
@Deprecated // use apiBaseUrl + "/devices/"
public String devicesResourceUrl;
+ /**
+ * Get the URI pointing to the /api/ base resource.
+ *
+ * @return /api/ URI
+ * @apiNote URI is guaranteed to end on /
+ */
public URI getApiBaseUrl() {
if (apiBaseUrl != null) {
// make sure to end on "/":
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 e0f305fd2..86f034a0c 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
@@ -3,6 +3,7 @@ package org.cryptomator.ui.keyloading.hub;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
@@ -46,6 +47,7 @@ public class ReceiveKeyController implements FxController {
private final Stage window;
private final HubConfig hubConfig;
+ private final String vaultId;
private final String deviceId;
private final String bearerToken;
private final CompletableFuture result;
@@ -53,14 +55,15 @@ public class ReceiveKeyController implements FxController {
private final Lazy legacyRegisterDeviceScene;
private final Lazy unauthorizedScene;
private final Lazy accountInitializationScene;
- private final URI vaultBaseUri;
private final Lazy invalidLicenseScene;
private final HttpClient httpClient;
+ private final StringTemplate.Processor API_BASE = this::resolveRelativeToApiBase;
@Inject
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy accountInitializationScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) {
this.window = window;
this.hubConfig = hubConfig;
+ this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
this.deviceId = deviceId;
this.bearerToken = Objects.requireNonNull(tokenRef.get());
this.result = result;
@@ -68,7 +71,6 @@ public class ReceiveKeyController implements FxController {
this.legacyRegisterDeviceScene = legacyRegisterDeviceScene;
this.unauthorizedScene = unauthorizedScene;
this.accountInitializationScene = accountInitializationScene;
- this.vaultBaseUri = getVaultBaseUri(vault);
this.invalidLicenseScene = invalidLicenseScene;
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
@@ -76,70 +78,107 @@ public class ReceiveKeyController implements FxController {
@FXML
public void initialize() {
- requestVaultMasterkey();
+ receiveKey();
+ }
+
+ public void receiveKey() {
+ requestApiConfig();
}
/**
- * STEP 1 (Request): GET vault key for this user
+ * STEP 0 (Request): GET /api/config
*/
- private void requestVaultMasterkey() {
- var accessTokenUri = appendPath(vaultBaseUri, "/access-token");
- var request = HttpRequest.newBuilder(accessTokenUri) //
+ private void requestApiConfig() {
+ var configUri = API_BASE."config";
+ var request = HttpRequest.newBuilder(configUri) //
+ .GET() //
+ .timeout(REQ_TIMEOUT) //
+ .build();
+ httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
+ .thenAcceptAsync(this::receivedApiConfig, Platform::runLater) //
+ .exceptionally(this::retrievalFailed);
+ }
+
+ /**
+ * STEP 0 (Response): GET /api/config
+ *
+ * @param response Response
+ */
+ private void receivedApiConfig(HttpResponse response) {
+ LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
+ Preconditions.checkState(response.statusCode() == 200, "Unexpected response " + response.statusCode());
+ try {
+ var config = JSON.reader().readValue(response.body(), ConfigDto.class);
+ if (config.apiLevel >= 1) {
+ requestDeviceData();
+ } else {
+ requestLegacyAccessToken();
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * STEP 2 (Request): GET vault key for this user
+ */
+ private void requestVaultMasterkey(String encryptedUserKey) {
+ var vaultKeyUri = API_BASE."vaults/\{vaultId}/access-token";
+ var request = HttpRequest.newBuilder(vaultKeyUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
.timeout(REQ_TIMEOUT) //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
- .thenAcceptAsync(this::receivedVaultMasterkey, Platform::runLater) //
+ .thenAcceptAsync(response -> receivedVaultMasterkey(encryptedUserKey, response), Platform::runLater) //
.exceptionally(this::retrievalFailed);
}
/**
- * STEP 1 (Response): GET vault key for this user
+ * STEP 2 (Response): GET vault key for this user
*
* @param response Response
*/
- private void receivedVaultMasterkey(HttpResponse response) {
+ private void receivedVaultMasterkey(String encryptedUserKey, HttpResponse response) {
LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
switch (response.statusCode()) {
- case 200 -> requestUserKey(response.body());
+ case 200 -> receivedBothEncryptedKeys(response.body(), encryptedUserKey);
case 402 -> licenseExceeded();
case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access - TODO: add specific dialog?
case 449 -> accountInitializationRequired();
- case 404 -> requestLegacyAccessToken();
default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
}
}
/**
- * STEP 2 (Request): GET user key for this device
+ * STEP 1 (Request): GET user key for this device
*/
- private void requestUserKey(String encryptedVaultKey) {
- var deviceTokenUri = URI.create(hubConfig.getApiBaseUrl() + "/devices/" + deviceId);
- var request = HttpRequest.newBuilder(deviceTokenUri) //
+ private void requestDeviceData() {
+ var deviceUri = API_BASE."devices/\{deviceId}";
+ var request = HttpRequest.newBuilder(deviceUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
.timeout(REQ_TIMEOUT) //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) //
- .thenAcceptAsync(response -> receivedUserKey(encryptedVaultKey, response), Platform::runLater) //
+ .thenAcceptAsync(this::receivedDeviceData, Platform::runLater) //
.exceptionally(this::retrievalFailed);
}
/**
- * STEP 2 (Response): GET user key for this device
+ * STEP 1 (Response): GET user key for this device
*
* @param response Response
*/
- private void receivedUserKey(String encryptedVaultKey, HttpResponse response) {
+ private void receivedDeviceData(HttpResponse response) {
LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
try {
switch (response.statusCode()) {
case 200 -> {
var device = JSON.reader().readValue(response.body(), DeviceDto.class);
- receivedBothEncryptedKeys(encryptedVaultKey, device.userPrivateKey);
+ requestVaultMasterkey(device.userPrivateKey);
}
- case 404 -> needsDeviceRegistration(); // TODO: using the setup code, we can theoretically immediately unlock
+ case 404 -> needsDeviceRegistration();
default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
}
} catch (IOException e) {
@@ -151,14 +190,14 @@ public class ReceiveKeyController implements FxController {
window.setScene(registerDeviceScene.get());
}
- private void receivedBothEncryptedKeys(String encryptedVaultKey, String encryptedUserKey) throws IOException {
+ private void receivedBothEncryptedKeys(String encryptedVaultKey, String encryptedUserKey) {
try {
var vaultKeyJwe = JWEObject.parse(encryptedVaultKey);
var userKeyJwe = JWEObject.parse(encryptedUserKey);
result.complete(ReceivedKey.vaultKeyAndUserKey(vaultKeyJwe, userKeyJwe));
window.close();
} catch (ParseException e) {
- throw new IOException("Failed to parse JWE", e);
+ retrievalFailed(e);
}
}
@@ -167,7 +206,7 @@ public class ReceiveKeyController implements FxController {
*/
@Deprecated
private void requestLegacyAccessToken() {
- var legacyAccessTokenUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
+ var legacyAccessTokenUri = API_BASE."vaults/\{vaultId}/keys/\{deviceId}";
var request = HttpRequest.newBuilder(legacyAccessTokenUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
@@ -249,19 +288,21 @@ public class ReceiveKeyController implements FxController {
}
}
- private static URI getVaultBaseUri(Vault vault) {
- try {
- var url = vault.getVaultConfigCache().get().getKeyId();
- assert url.getScheme().startsWith(SCHEME_PREFIX);
- var correctedScheme = url.getScheme().substring(SCHEME_PREFIX.length());
- return new URI(correctedScheme, url.getSchemeSpecificPart(), url.getFragment());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- } catch (URISyntaxException e) {
- throw new IllegalStateException("URI constructed from params known to be valid", e);
- }
+ private URI resolveRelativeToApiBase(StringTemplate template) {
+ var path = template.interpolate();
+ var relPath = path.startsWith("/") ? path.substring(1) : path;
+ return hubConfig.getApiBaseUrl().resolve(relPath);
+ }
+
+ private static String extractVaultId(URI vaultKeyUri) {
+ assert vaultKeyUri.getScheme().startsWith(SCHEME_PREFIX);
+ var path = vaultKeyUri.getPath();
+ return path.substring(path.lastIndexOf('/') + 1);
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record ConfigDto(@JsonProperty(value = "apiLevel") int apiLevel) {}
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java
index bba13516c..6988283a3 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java
@@ -1,24 +1,43 @@
package org.cryptomator.ui.keyloading.hub;
+import dagger.Lazy;
import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import javax.inject.Inject;
import javafx.fxml.FXML;
+import javafx.scene.Scene;
import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.util.concurrent.CompletableFuture;
public class RegisterSuccessController implements FxController {
private final Stage window;
+ private final CompletableFuture result;
+ private final Lazy receiveKeyScene;
+ private final ReceiveKeyController receiveKeyController;
@Inject
- public RegisterSuccessController(@KeyLoading Stage window) {
+ public RegisterSuccessController(@KeyLoading Stage window, CompletableFuture result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene, ReceiveKeyController receiveKeyController) {
this.window = window;
+ this.result = result;
+ this.receiveKeyScene = receiveKeyScene;
+ this.receiveKeyController = receiveKeyController;
+ this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
}
@FXML
- public void close() {
- window.close();
+ public void complete() {
+ window.setScene(receiveKeyScene.get());
+ receiveKeyController.receiveKey();
}
+ private void windowClosed(WindowEvent windowEvent) {
+ result.cancel(true);
+ }
+
+
}
diff --git a/src/main/resources/fxml/hub_register_success.fxml b/src/main/resources/fxml/hub_register_success.fxml
index 822a4489e..c8309c2f7 100644
--- a/src/main/resources/fxml/hub_register_success.fxml
+++ b/src/main/resources/fxml/hub_register_success.fxml
@@ -41,9 +41,9 @@
-
+
-
+