From 59f5c0cb124097dae24a2b7a7e2c8551e8e9b857 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 9 May 2023 17:09:49 +0200 Subject: [PATCH 01/81] started new unlock workflow using user-specific private key --- .../ui/keyloading/hub/AuthFlowController.java | 4 +- .../keyloading/hub/HubKeyLoadingModule.java | 2 +- .../keyloading/hub/HubKeyLoadingStrategy.java | 6 +- .../ui/keyloading/hub/JWEHelper.java | 42 +++++-- .../keyloading/hub/ReceiveKeyController.java | 118 ++++++++++++++++-- .../ui/keyloading/hub/ReceivedKey.java | 24 ++++ .../hub/RegisterDeviceController.java | 10 +- .../hub/RegisterFailedController.java | 4 +- .../hub/UnauthorizedDeviceController.java | 4 +- .../ui/keyloading/hub/JWEHelperTest.java | 53 ++++++-- 10 files changed, 226 insertions(+), 41 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java index 5765f56e0..06e488581 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java @@ -35,13 +35,13 @@ public class AuthFlowController implements FxController { private final String deviceId; private final HubConfig hubConfig; private final AtomicReference tokenRef; - private final CompletableFuture result; + private final CompletableFuture result; private final Lazy receiveKeyScene; private final ObjectProperty authUri; private AuthFlowTask task; @Inject - public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene) { + public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene) { this.application = application; this.window = window; this.executor = executor; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index 7b8aae875..ad4ea9408 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -69,7 +69,7 @@ public abstract class HubKeyLoadingModule { @Provides @KeyLoadingScoped - static CompletableFuture provideResult() { + static CompletableFuture provideResult() { return new CompletableFuture<>(); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index cc5edfcb4..9ea5e7735 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -36,11 +36,11 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { private final KeychainManager keychainManager; private final Lazy authFlowScene; private final Lazy noKeychainScene; - private final CompletableFuture result; + private final CompletableFuture result; private final DeviceKey deviceKey; @Inject - public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) { + public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) { this.window = window; this.keychainManager = keychainManager; window.setTitle(windowTitle); @@ -60,7 +60,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { var keypair = deviceKey.get(); showWindow(authFlowScene); var jwe = result.get(); - return JWEHelper.decrypt(jwe, keypair.getPrivate()); + return jwe.decryptMasterkey(keypair.getPrivate()); } catch (NoKeychainAccessProviderException e) { showWindow(noKeychainScene); throw new UnlockCancelledException("Unlock canceled due to missing prerequisites", e); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java index 2c2b9baa4..29bc7a98f 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java @@ -10,27 +10,55 @@ import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; import java.security.interfaces.ECPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; +import java.util.function.Function; class JWEHelper { private static final Logger LOG = LoggerFactory.getLogger(JWEHelper.class); - private static final String JWE_PAYLOAD_MASTERKEY_FIELD = "key"; + private static final String JWE_PAYLOAD_KEY_FIELD = "key"; + private static final String EC_ALG = "EC"; private JWEHelper(){} - public static Masterkey decrypt(JWEObject jwe, ECPrivateKey privateKey) throws MasterkeyLoadingFailedException { + public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) { + try { + jwe.decrypt(new ECDHDecrypter(deviceKey)); + var keySpec = readKey(jwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new); + var factory = KeyFactory.getInstance(EC_ALG); + var privateKey = factory.generatePrivate(keySpec); + if (privateKey instanceof ECPrivateKey ecPrivateKey) { + return ecPrivateKey; + } else { + throw new IllegalStateException(EC_ALG + " key factory not generating ECPrivateKeys"); + } + } catch (JOSEException e) { + LOG.warn("Failed to decrypt JWE: {}", jwe); + throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(EC_ALG + " not supported"); + } catch (InvalidKeySpecException e) { + LOG.warn("Unexpected JWE payload: {}", jwe.getPayload()); + throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e); + } + } + + public static Masterkey decryptVaultKey(JWEObject jwe, ECPrivateKey privateKey) throws MasterkeyLoadingFailedException { try { jwe.decrypt(new ECDHDecrypter(privateKey)); - return readKey(jwe); + return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, Masterkey::new); } catch (JOSEException e) { LOG.warn("Failed to decrypt JWE: {}", jwe); throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e); } } - private static Masterkey readKey(JWEObject jwe) throws MasterkeyLoadingFailedException { + private static T readKey(JWEObject jwe, String keyField, Function rawKeyFactory) throws MasterkeyLoadingFailedException { Preconditions.checkArgument(jwe.getState() == JWEObject.State.DECRYPTED); var fields = jwe.getPayload().toJSONObject(); if (fields == null) { @@ -39,11 +67,11 @@ class JWEHelper { } var keyBytes = new byte[0]; try { - if (fields.get(JWE_PAYLOAD_MASTERKEY_FIELD) instanceof String key) { + if (fields.get(keyField) instanceof String key) { keyBytes = BaseEncoding.base64().decode(key); - return new Masterkey(keyBytes); + return rawKeyFactory.apply(keyBytes); } else { - throw new IllegalArgumentException("JWE payload doesn't contain field " + JWE_PAYLOAD_MASTERKEY_FIELD); + throw new IllegalArgumentException("JWE payload doesn't contain field " + keyField); } } catch (IllegalArgumentException e) { LOG.error("Unexpected JWE payload: {}", jwe.getPayload()); 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 6f6e7cb42..630b35e1f 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -8,6 +8,8 @@ import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; 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 javax.inject.Named; @@ -17,13 +19,13 @@ import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.WindowEvent; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -33,12 +35,14 @@ import java.util.concurrent.atomic.AtomicReference; @KeyLoadingScoped public class ReceiveKeyController implements FxController { + private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class); private static final String SCHEME_PREFIX = "hub+"; private final Stage window; + private final HubConfig hubConfig; private final String deviceId; private final String bearerToken; - private final CompletableFuture result; + private final CompletableFuture result; private final Lazy registerDeviceScene; private final Lazy unauthorizedScene; private final URI vaultBaseUri; @@ -46,8 +50,9 @@ public class ReceiveKeyController implements FxController { private final HttpClient httpClient; @Inject - public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) { + 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_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) { this.window = window; + this.hubConfig = hubConfig; this.deviceId = deviceId; this.bearerToken = Objects.requireNonNull(tokenRef.get()); this.result = result; @@ -61,20 +66,109 @@ public class ReceiveKeyController implements FxController { @FXML public void initialize() { - var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId); - var request = HttpRequest.newBuilder(keyUri) // + requestUserToken(); + } + + /** + * STEP 1 (Request): GET user token for this vault + */ + private void requestUserToken() { + var userTokenUri = appendPath(vaultBaseUri, "/user-tokens/me"); + var request = HttpRequest.newBuilder(userTokenUri) // .header("Authorization", "Bearer " + bearerToken) // .GET() // .build(); - httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) // - .thenAcceptAsync(this::loadedExistingKey, Platform::runLater) // + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) // + .thenAcceptAsync(this::receivedUserTokenResponse, Platform::runLater) // .exceptionally(this::retrievalFailed); } - private void loadedExistingKey(HttpResponse response) { + /** + * STEP 1 (Response) + * + * @param response Response + */ + private void receivedUserTokenResponse(HttpResponse response) { + LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode()); try { switch (response.statusCode()) { - case 200 -> retrievalSucceeded(response); + case 200 -> requestDeviceToken(response.body()); + case 402 -> licenseExceeded(); + case 403 -> accessNotGranted(); + case 404 -> requestLegacyAccessToken(); + default -> throw new IOException("Unexpected response " + response.statusCode()); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * STEP 2 (Request): GET device token for this user + */ + private void requestDeviceToken(String userToken) { + var deviceTokenUri = appendPath(URI.create(hubConfig.devicesResourceUrl), "/%s/device-token".formatted(deviceId)); + var request = HttpRequest.newBuilder(deviceTokenUri) // + .header("Authorization", "Bearer " + bearerToken) // + .GET() // + .build(); + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) // + .thenAcceptAsync(response -> receivedDeviceTokenResponse(userToken, response), Platform::runLater) // + .exceptionally(this::retrievalFailed); + } + + /** + * STEP 2 (Response) + * + * @param response Response + */ + private void receivedDeviceTokenResponse(String userToken, HttpResponse response) { + LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode()); + try { + switch (response.statusCode()) { + case 200 -> receivedDeviceTokenSuccess(userToken, response.body()); + case 403, 404 -> needsDeviceRegistration(); + default -> throw new IOException("Unexpected response " + response.statusCode()); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void receivedDeviceTokenSuccess(String rawUserToken, String rawDeviceToken) throws IOException { + try { + var userToken = JWEObject.parse(rawUserToken); + var deviceToken = JWEObject.parse(rawDeviceToken); + result.complete(ReceivedKey.userAndDeviceKey(userToken, deviceToken)); + window.close(); + } catch (ParseException e) { + throw new IOException("Failed to parse JWE", e); + } + } + + /** + * LEGACY FALLBACK (Request): GET the legacy access token from Hub 1.x + */ + private void requestLegacyAccessToken() { + var legacyAccessTokenUri = appendPath(vaultBaseUri, "/keys/%s".formatted(deviceId)); + var request = HttpRequest.newBuilder(legacyAccessTokenUri) // + .header("Authorization", "Bearer " + bearerToken) // + .GET() // + .build(); + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) // + .thenAcceptAsync(this::receivedLegacyAccessTokenResponse, Platform::runLater) // + .exceptionally(this::retrievalFailed); + } + + /** + * LEGACY FALLBACK (Response) + * + * @param response Response + */ + private void receivedLegacyAccessTokenResponse(HttpResponse response) { + try { + switch (response.statusCode()) { + case 200 -> receivedLegacyAccessTokenSuccess(response.body()); case 402 -> licenseExceeded(); case 403 -> accessNotGranted(); case 404 -> needsDeviceRegistration(); @@ -85,10 +179,10 @@ public class ReceiveKeyController implements FxController { } } - private void retrievalSucceeded(HttpResponse response) throws IOException { + private void receivedLegacyAccessTokenSuccess(String rawToken) throws IOException { try { - var string = HttpHelper.readBody(response); - result.complete(JWEObject.parse(string)); + var token = JWEObject.parse(rawToken); + result.complete(ReceivedKey.legacyDeviceKey(token)); window.close(); } catch (ParseException e) { throw new IOException("Failed to parse JWE", e); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java new file mode 100644 index 000000000..a6f0ac397 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java @@ -0,0 +1,24 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.nimbusds.jose.JWEObject; +import org.cryptomator.cryptolib.api.Masterkey; + +import java.security.interfaces.ECPrivateKey; + +@FunctionalInterface +interface ReceivedKey { + + Masterkey decryptMasterkey(ECPrivateKey deviceKey); + + static ReceivedKey userAndDeviceKey(JWEObject userToken, JWEObject deviceToken) { + return deviceKey -> { + var userKey = JWEHelper.decryptUserKey(deviceToken, deviceKey); + return JWEHelper.decryptVaultKey(userToken, userKey); + }; + } + + static ReceivedKey legacyDeviceKey(JWEObject legacyAccessToken) { + return deviceKey -> JWEHelper.decryptVaultKey(legacyAccessToken, deviceKey); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java index 4cf2d9fa2..52901287a 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java @@ -36,6 +36,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -56,7 +57,7 @@ public class RegisterDeviceController implements FxController { private final Lazy registerFailedScene; private final String deviceId; private final P384KeyPair keyPair; - private final CompletableFuture result; + private final CompletableFuture result; private final DecodedJWT jwt; private final HttpClient httpClient; private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false); @@ -65,7 +66,7 @@ public class RegisterDeviceController implements FxController { public Button registerBtn; @Inject - public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture result, @Named("bearerToken") AtomicReference bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy registerFailedScene) { + public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture result, @Named("bearerToken") AtomicReference bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy registerFailedScene) { this.window = window; this.hubConfig = hubConfig; this.deviceId = deviceId; @@ -104,11 +105,12 @@ public class RegisterDeviceController implements FxController { var dto = new CreateDeviceDto(); dto.id = deviceId; dto.name = deviceNameField.getText(); - dto.publicKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey); + dto.publicKey = Base64.getUrlEncoder().withoutPadding().encodeToString(deviceKey); var json = GSON.toJson(dto); // TODO: do we want to keep GSON? doesn't support records -.- var request = HttpRequest.newBuilder(keyUri) // + .PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // .header("Authorization", "Bearer " + bearerToken) // - .header("Content-Type", "application/json").PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // + .header("Content-Type", "application/json") // .build(); httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) // .thenApply(response -> { diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java index 8a4278d72..57150390c 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java @@ -12,10 +12,10 @@ import java.util.concurrent.CompletableFuture; public class RegisterFailedController implements FxController { private final Stage window; - private final CompletableFuture result; + private final CompletableFuture result; @Inject - public RegisterFailedController(@KeyLoading Stage window, CompletableFuture result) { + public RegisterFailedController(@KeyLoading Stage window, CompletableFuture result) { this.window = window; this.result = result; } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java index 1a7cbab02..c42ee1cd7 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java @@ -15,10 +15,10 @@ import java.util.concurrent.CompletableFuture; public class UnauthorizedDeviceController implements FxController { private final Stage window; - private final CompletableFuture result; + private final CompletableFuture result; @Inject - public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture result) { + public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture result) { this.window = window; this.result = result; this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java index 3d495e8c1..cac307add 100644 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java @@ -3,7 +3,9 @@ package org.cryptomator.ui.keyloading.hub; import com.nimbusds.jose.JWEObject; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.cryptolib.common.P384KeyPair; +import org.cryptomator.cryptolib.shaded.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -17,16 +19,51 @@ import java.util.Base64; public class JWEHelperTest { - private static final String JWE = "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52Nm02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD-kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA"; + // key pairs from frontend tests (crypto.spec.ts): + private static final String USER_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y="; + private static final String USER_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAESzrRXmyI8VWFJg1dPUNbFcc9jZvjZEfH7ulKI1UkXAltd7RGWrcfFxqyGPcwu6AQhHUag3OvDzEr0uUQND4PXHQTXP5IDGdYhJhL+WLKjnGjQAw0rNGy5V29+aV+yseW"; + private static final String DEVICE_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg="; + private static final String DEVICE_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEem7I0xHVyliLrtQb4+mPMMkpSETsu2KZlWU2NdvCLaLwg/KXEeD5xZY7wCG9jLIQna9WpV+IOnIAzqnE3kRIjm3En7nDlPUctaSfxp1+igNHkpY65Oq8Y0g6LPGomejI"; + + // used for JWE generation in frontend: (jwe.spec.ts): private static final String PRIV_KEY = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ"; private static final String PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu"; @Test - public void testDecrypt() throws ParseException, InvalidKeySpecException { - var jwe = JWEObject.parse(JWE); - var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))); + @DisplayName("decryptUserKey") + public void testDecryptUserKey() throws ParseException, InvalidKeySpecException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ"""); + var deviceKeyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PRIV_KEY))); - var masterkey = JWEHelper.decrypt(jwe, keyPair.getPrivate()); + var userKey = JWEHelper.decryptUserKey(jwe, deviceKeyPair.getPrivate()); + + Assertions.assertArrayEquals(Base64.getDecoder().decode(USER_PRIV_KEY), userKey.getEncoded()); + } + + @Test + @DisplayName("decryptVaultKey") + public void testDecryptVaultKey() throws ParseException, InvalidKeySpecException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlA\ + tMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0\ + FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52N\ + m02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD\ + -kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2\ + U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA"""); + var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate(); + + var masterkey = JWEHelper.decryptVaultKey(jwe, privateKey); var expectedEncKey = new byte[32]; var expectedMacKey = new byte[32]; @@ -44,12 +81,12 @@ public class JWEHelperTest { "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA", // json payload field "key" not a string "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ" // json payload field "key" invalid base64 data }) - public void testDecryptInvalid(String malformed) throws ParseException, InvalidKeySpecException { + public void testDecryptInvalidVaultKey(String malformed) throws ParseException, InvalidKeySpecException { var jwe = JWEObject.parse(malformed); - var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))); + var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate(); Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { - JWEHelper.decrypt(jwe, keyPair.getPrivate()); + JWEHelper.decryptVaultKey(jwe, privateKey); }); } From fe733967dc8d0e5ab330f858ef2f4dd318cc2f49 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 1 Jun 2023 15:06:21 +0200 Subject: [PATCH 02/81] send device type in device registration request --- .../org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java | 1 + .../ui/keyloading/hub/RegisterDeviceController.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java index 71377a318..dcf9b6458 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java @@ -4,6 +4,7 @@ class CreateDeviceDto { public String id; public String name; + public final String type = "DESKTOP"; public String publicKey; } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java index 52901287a..8e204719e 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java @@ -100,14 +100,14 @@ public class RegisterDeviceController implements FxController { registerBtn.setContentDisplay(ContentDisplay.LEFT); registerBtn.setDisable(true); - var keyUri = URI.create(hubConfig.devicesResourceUrl + deviceId); + var deviceUri = URI.create(hubConfig.devicesResourceUrl + deviceId); var deviceKey = keyPair.getPublic().getEncoded(); var dto = new CreateDeviceDto(); dto.id = deviceId; dto.name = deviceNameField.getText(); dto.publicKey = Base64.getUrlEncoder().withoutPadding().encodeToString(deviceKey); var json = GSON.toJson(dto); // TODO: do we want to keep GSON? doesn't support records -.- - var request = HttpRequest.newBuilder(keyUri) // + var request = HttpRequest.newBuilder(deviceUri) // .PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // .header("Authorization", "Bearer " + bearerToken) // .header("Content-Type", "application/json") // From 5a18d086e0873855fa48ae349672adbf0cc58964 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 22 Jun 2023 17:16:00 +0200 Subject: [PATCH 03/81] register device using a setup code --- .../org/cryptomator/ui/common/FxmlFile.java | 3 +- .../ui/keyloading/hub/CreateDeviceDto.java | 5 + .../keyloading/hub/HubKeyLoadingModule.java | 12 + .../ui/keyloading/hub/JWEHelper.java | 52 ++++- .../keyloading/hub/ReceiveKeyController.java | 14 +- .../keyloading/hub/SetupDeviceController.java | 208 ++++++++++++++++++ src/main/resources/fxml/hub_setup_device.fxml | 83 +++++++ .../ui/keyloading/hub/JWEHelperTest.java | 1 - 8 files changed, 367 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java create mode 100644 src/main/resources/fxml/hub_setup_device.fxml diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 3bec75899..15df40e2d 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -22,7 +22,8 @@ public enum FxmlFile { HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), // HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), // HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), // - HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"), + HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"), // + HUB_SETUP_DEVICE("/fxml/hub_setup_device.fxml"), // HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // LOCK_FAILED("/fxml/lock_failed.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java index dcf9b6458..cf8ffdeec 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java @@ -1,10 +1,15 @@ package org.cryptomator.ui.keyloading.hub; +import java.time.Instant; + class CreateDeviceDto { public String id; public String name; public final String type = "DESKTOP"; public String publicKey; + public String userKey; + public String creationTime; + public String lastSeenTime; } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index ad4ea9408..d6d2087dc 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -134,6 +134,13 @@ public abstract class HubKeyLoadingModule { return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_FAILED); } + @Provides + @FxmlScene(FxmlFile.HUB_SETUP_DEVICE) + @KeyLoadingScoped + static Scene provideHubSetupDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_SETUP_DEVICE); + } + @Provides @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) @KeyLoadingScoped @@ -176,6 +183,11 @@ public abstract class HubKeyLoadingModule { @FxControllerKey(RegisterFailedController.class) abstract FxController bindRegisterFailedController(RegisterFailedController controller); + @Binds + @IntoMap + @FxControllerKey(SetupDeviceController.class) + abstract FxController bindSetupDeviceController(SetupDeviceController controller); + @Binds @IntoMap @FxControllerKey(UnauthorizedDeviceController.class) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java index 29bc7a98f..233f2a6df 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java @@ -2,20 +2,33 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; +import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWEHeader; import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.Payload; import com.nimbusds.jose.crypto.ECDHDecrypter; +import com.nimbusds.jose.crypto.ECDHEncrypter; +import com.nimbusds.jose.crypto.PasswordBasedDecrypter; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import com.nimbusds.jose.jwk.gen.JWKGenerator; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.KeyFactory; +import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; +import java.util.Base64; +import java.util.Map; import java.util.function.Function; class JWEHelper { @@ -25,11 +38,43 @@ class JWEHelper { private static final String EC_ALG = "EC"; private JWEHelper(){} + public static JWEObject encryptUserKey(ECPrivateKey userKey, ECPublicKey deviceKey) { + try { + var encodedUserKey = Base64.getEncoder().encodeToString(userKey.getEncoded()); + var keyGen = new ECKeyGenerator(Curve.P_384); + var ephemeralKeyPair = keyGen.generate(); + var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM).ephemeralPublicKey(ephemeralKeyPair.toPublicJWK()).build(); + var payload = new Payload(Map.of(JWE_PAYLOAD_KEY_FIELD, encodedUserKey)); + var jwe = new JWEObject(header, payload); + jwe.encrypt(new ECDHEncrypter(deviceKey)); + return jwe; + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) { + try { + jwe.decrypt(new PasswordBasedDecrypter(setupCode)); + return decodeUserKey(jwe); + } catch (JOSEException e) { + throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e); + } + } public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) { try { jwe.decrypt(new ECDHDecrypter(deviceKey)); - var keySpec = readKey(jwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new); + return decodeUserKey(jwe); + } catch (JOSEException e) { + LOG.warn("Failed to decrypt JWE: {}", jwe); + throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e); + } + } + + private static ECPrivateKey decodeUserKey(JWEObject decryptedJwe) { + try { + var keySpec = readKey(decryptedJwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new); var factory = KeyFactory.getInstance(EC_ALG); var privateKey = factory.generatePrivate(keySpec); if (privateKey instanceof ECPrivateKey ecPrivateKey) { @@ -37,13 +82,10 @@ class JWEHelper { } else { throw new IllegalStateException(EC_ALG + " key factory not generating ECPrivateKeys"); } - } catch (JOSEException e) { - LOG.warn("Failed to decrypt JWE: {}", jwe); - throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(EC_ALG + " not supported"); } catch (InvalidKeySpecException e) { - LOG.warn("Unexpected JWE payload: {}", jwe.getPayload()); + LOG.warn("Unexpected JWE payload: {}", decryptedJwe.getPayload()); throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e); } } 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 630b35e1f..1bb08c44c 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -43,6 +43,7 @@ public class ReceiveKeyController implements FxController { private final String deviceId; private final String bearerToken; private final CompletableFuture result; + private final Lazy setupDeviceScene; private final Lazy registerDeviceScene; private final Lazy unauthorizedScene; private final URI vaultBaseUri; @@ -50,12 +51,13 @@ public class ReceiveKeyController implements FxController { private final HttpClient httpClient; @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_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) { + 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_SETUP_DEVICE) Lazy setupDeviceScene, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) { this.window = window; this.hubConfig = hubConfig; this.deviceId = deviceId; this.bearerToken = Objects.requireNonNull(tokenRef.get()); this.result = result; + this.setupDeviceScene = setupDeviceScene; this.registerDeviceScene = registerDeviceScene; this.unauthorizedScene = unauthorizedScene; this.vaultBaseUri = getVaultBaseUri(vault); @@ -127,7 +129,7 @@ public class ReceiveKeyController implements FxController { try { switch (response.statusCode()) { case 200 -> receivedDeviceTokenSuccess(userToken, response.body()); - case 403, 404 -> needsDeviceRegistration(); + case 403, 404 -> needsDeviceSetup(); default -> throw new IOException("Unexpected response " + response.statusCode()); } } catch (IOException e) { @@ -135,6 +137,10 @@ public class ReceiveKeyController implements FxController { } } + private void needsDeviceSetup() { + window.setScene(setupDeviceScene.get()); + } + private void receivedDeviceTokenSuccess(String rawUserToken, String rawDeviceToken) throws IOException { try { var userToken = JWEObject.parse(rawUserToken); @@ -171,7 +177,7 @@ public class ReceiveKeyController implements FxController { case 200 -> receivedLegacyAccessTokenSuccess(response.body()); case 402 -> licenseExceeded(); case 403 -> accessNotGranted(); - case 404 -> needsDeviceRegistration(); + case 404 -> needsLegacyDeviceRegistration(); default -> throw new IOException("Unexpected response " + response.statusCode()); } } catch (IOException e) { @@ -193,7 +199,7 @@ public class ReceiveKeyController implements FxController { window.setScene(invalidLicenseScene.get()); } - private void needsDeviceRegistration() { + private void needsLegacyDeviceRegistration() { window.setScene(registerDeviceScene.get()); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java new file mode 100644 index 000000000..a43629246 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java @@ -0,0 +1,208 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.nimbusds.jose.JWEObject; +import dagger.Lazy; +import org.cryptomator.common.settings.DeviceKey; +import org.cryptomator.cryptolib.common.P384KeyPair; +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 org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import java.io.IOException; +import java.net.InetAddress; +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.text.ParseException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +@KeyLoadingScoped +public class SetupDeviceController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(SetupDeviceController.class); + private static final Gson GSON = new GsonBuilder().setLenient().create(); + + private final Stage window; + private final HubConfig hubConfig; + private final String bearerToken; + private final Lazy registerSuccessScene; + private final Lazy registerFailedScene; + private final String deviceId; + private final P384KeyPair deviceKeyPair; + private final CompletableFuture result; + private final DecodedJWT jwt; + private final HttpClient httpClient; + private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false); + + public TextField setupCodeField; + public TextField deviceNameField; + public Button registerBtn; + + @Inject + public SetupDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture result, @Named("bearerToken") AtomicReference bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy registerFailedScene) { + this.window = window; + this.hubConfig = hubConfig; + this.deviceId = deviceId; + this.deviceKeyPair = Objects.requireNonNull(deviceKey.get()); + this.result = result; + this.bearerToken = Objects.requireNonNull(bearerToken.get()); + this.registerSuccessScene = registerSuccessScene; + this.registerFailedScene = registerFailedScene; + this.jwt = JWT.decode(this.bearerToken); + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); + this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build(); + } + + public void initialize() { + deviceNameField.setText(determineHostname()); + deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false)); + } + + private String determineHostname() { + try { + var hostName = InetAddress.getLocalHost().getHostName(); + return Objects.requireNonNullElse(hostName, ""); + } catch (IOException e) { + return ""; + } + } + + @FXML + public void register() { + setupCodeField.setDisable(true); + deviceNameField.setDisable(true); + deviceNameAlreadyExists.set(false); + registerBtn.setContentDisplay(ContentDisplay.LEFT); + registerBtn.setDisable(true); + + var apiRootUrl = URI.create(hubConfig.devicesResourceUrl + "/..").normalize(); // TODO: add url to vault config file, only use this as a fallback for legacy vaults + var deviceUri = URI.create(hubConfig.devicesResourceUrl + deviceId); + var deviceKey = deviceKeyPair.getPublic().getEncoded(); + + var userReq = HttpRequest.newBuilder(apiRootUrl.resolve("users/me")) // + .GET() // + .header("Authorization", "Bearer " + bearerToken) // + .header("Content-Type", "application/json") // + .build(); + httpClient.sendAsync(userReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) // + .thenApply(response -> { + if (response.statusCode() == 200) { + return GSON.fromJson(response.body(), UserDto.class); + } else { + throw new RuntimeException("Server answered with unexpected status code " + response.statusCode()); + } + }).thenApply(user -> { + try { + var userKey = JWEHelper.decryptUserKey(JWEObject.parse(user.privateKey), setupCodeField.getText()); + return JWEHelper.encryptUserKey(userKey, deviceKeyPair.getPublic()); + } catch (ParseException e) { + throw new RuntimeException("Server answered with unparsable user key", e); + } + }).thenCompose(jwe -> { + var dto = new CreateDeviceDto(); + dto.id = deviceId; + dto.name = deviceNameField.getText(); + dto.publicKey = Base64.getUrlEncoder().withoutPadding().encodeToString(deviceKey); + dto.userKey = jwe.serialize(); + dto.creationTime = Instant.now().toString(); + dto.lastSeenTime = Instant.now().toString(); + var json = GSON.toJson(dto); + var putDeviceReq = HttpRequest.newBuilder(deviceUri) // + .PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // + .header("Authorization", "Bearer " + bearerToken) // + .header("Content-Type", "application/json") // + .build(); + return httpClient.sendAsync(putDeviceReq, HttpResponse.BodyHandlers.discarding()); + }).handleAsync((response, throwable) -> { + if (response != null) { + this.handleResponse(response); + } else { + this.registrationFailed(throwable); + } + return null; + }, Platform::runLater); + } + + private void handleResponse(HttpResponse response) { + if (response.statusCode() == 201) { + LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl); + window.setScene(registerSuccessScene.get()); + } else if (response.statusCode() == 409) { + deviceNameAlreadyExists.set(true); + registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY); + registerBtn.setDisable(false); + } else { + registrationFailed(new IllegalStateException("Unexpected http status code " + response.statusCode())); + } + } + + private void registrationFailed(Throwable cause) { + LOG.warn("Device registration failed.", cause); + window.setScene(registerFailedScene.get()); + result.completeExceptionally(cause); + } + + @FXML + public void close() { + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + result.cancel(true); + } + + /* Getter */ + + public String getUserName() { + return jwt.getClaim("email").asString(); + } + + + //--- Getters & Setters + + public BooleanProperty deviceNameAlreadyExistsProperty() { + return deviceNameAlreadyExists; + } + + public boolean getDeviceNameAlreadyExists() { + return deviceNameAlreadyExists.get(); + } + + + private class UserDto { + public String id; + public String name; + public @Nullable String publicKey; + public @Nullable String privateKey; + public @Nullable String setupCode; + } +} diff --git a/src/main/resources/fxml/hub_setup_device.fxml b/src/main/resources/fxml/hub_setup_device.fxml new file mode 100644 index 000000000..47237fe0e --- /dev/null +++ b/src/main/resources/fxml/hub_setup_device.fxml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java index cac307add..2cee9cb44 100644 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java @@ -3,7 +3,6 @@ package org.cryptomator.ui.keyloading.hub; import com.nimbusds.jose.JWEObject; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.cryptolib.common.P384KeyPair; -import org.cryptomator.cryptolib.shaded.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From 448eac8ff57d77f0300a110d5d7cded55141009e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 23 Jun 2023 12:06:06 +0200 Subject: [PATCH 04/81] cleanup --- .../ui/keyloading/hub/SetupDeviceController.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java index a43629246..7d3e11091 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/SetupDeviceController.java @@ -38,7 +38,6 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.time.Instant; -import java.time.format.DateTimeFormatter; import java.util.Base64; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -116,12 +115,14 @@ public class SetupDeviceController implements FxController { httpClient.sendAsync(userReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) // .thenApply(response -> { if (response.statusCode() == 200) { - return GSON.fromJson(response.body(), UserDto.class); + var dto = GSON.fromJson(response.body(), UserDto.class); + return Objects.requireNonNull(dto, "null or empty response body"); } else { throw new RuntimeException("Server answered with unexpected status code " + response.statusCode()); } }).thenApply(user -> { try { + // TODO: if user.privateKey == null, link to "initial setup" var userKey = JWEHelper.decryptUserKey(JWEObject.parse(user.privateKey), setupCodeField.getText()); return JWEHelper.encryptUserKey(userKey, deviceKeyPair.getPublic()); } catch (ParseException e) { @@ -180,13 +181,6 @@ public class SetupDeviceController implements FxController { result.cancel(true); } - /* Getter */ - - public String getUserName() { - return jwt.getClaim("email").asString(); - } - - //--- Getters & Setters public BooleanProperty deviceNameAlreadyExistsProperty() { @@ -198,7 +192,8 @@ public class SetupDeviceController implements FxController { } - private class UserDto { + // TODO convert to record? + private static class UserDto { public String id; public String name; public @Nullable String publicKey; From 9fc1efa005e642c761e876d38a3451f5b62ea8d1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 29 Jun 2023 10:38:31 +0200 Subject: [PATCH 05/81] adjust labels --- src/main/resources/fxml/hub_setup_device.fxml | 2 +- src/main/resources/i18n/strings.properties | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/resources/fxml/hub_setup_device.fxml b/src/main/resources/fxml/hub_setup_device.fxml index 47237fe0e..71edd45e9 100644 --- a/src/main/resources/fxml/hub_setup_device.fxml +++ b/src/main/resources/fxml/hub_setup_device.fxml @@ -46,7 +46,7 @@ -