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 @@