Merge pull request #3287 from cryptomator/feature/refactored-hub-unlock

Refactored Hub Unlock
This commit is contained in:
Sebastian Stenzel
2024-01-17 13:32:09 +01:00
committed by GitHub
4 changed files with 106 additions and 41 deletions

View File

@@ -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 <code>/api/</code> base resource.
*
* @return <code>/api/</code> URI
* @apiNote URI is guaranteed to end on <code>/</code>
*/
public URI getApiBaseUrl() {
if (apiBaseUrl != null) {
// make sure to end on "/":

View File

@@ -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<ReceivedKey> result;
@@ -53,14 +55,15 @@ public class ReceiveKeyController implements FxController {
private final Lazy<Scene> legacyRegisterDeviceScene;
private final Lazy<Scene> unauthorizedScene;
private final Lazy<Scene> accountInitializationScene;
private final URI vaultBaseUri;
private final Lazy<Scene> invalidLicenseScene;
private final HttpClient httpClient;
private final StringTemplate.Processor<URI, RuntimeException> API_BASE = this::resolveRelativeToApiBase;
@Inject
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy<Scene> legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy<Scene> accountInitializationScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy<Scene> 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<String> 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<String> response) {
private void receivedVaultMasterkey(String encryptedUserKey, HttpResponse<String> 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<String> response) {
private void receivedDeviceData(HttpResponse<String> 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) {}
}

View File

@@ -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<ReceivedKey> result;
private final Lazy<Scene> receiveKeyScene;
private final ReceiveKeyController receiveKeyController;
@Inject
public RegisterSuccessController(@KeyLoading Stage window) {
public RegisterSuccessController(@KeyLoading Stage window, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> 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);
}
}