diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
index ee89a17ec..15c1ea3a9 100644
--- a/.github/workflows/appimage.yml
+++ b/.github/workflows/appimage.yml
@@ -10,8 +10,8 @@ on:
required: false
env:
- JAVA_DIST: 'temurin'
- JAVA_VERSION: 20
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
jobs:
get-version:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1416c3809..fbb57cbbf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,8 +6,8 @@ on:
types: [labeled]
env:
- JAVA_DIST: 'temurin'
- JAVA_VERSION: 20
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
defaults:
run:
diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml
index 6c1a68944..054d6cadf 100644
--- a/.github/workflows/debian.yml
+++ b/.github/workflows/debian.yml
@@ -16,8 +16,9 @@ on:
type: boolean
env:
- JAVA_DIST: 'temurin'
- JAVA_VERSION: 20
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
+ COFFEELIBS_JDK_VERSION: 21
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip'
OPENJFX_JMODS_AMD64_HASH: 'f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
@@ -42,7 +43,7 @@ jobs:
run: |
sudo add-apt-repository ppa:coffeelibs/openjdk
sudo apt-get update
- sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.JAVA_VERSION }} libgtk2.0-0
+ sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK_VERSION }} libgtk2.0-0
- name: Setup Java
uses: actions/setup-java@v3
with:
diff --git a/.github/workflows/get-version.yml b/.github/workflows/get-version.yml
index f05d7742a..1bed1cff8 100644
--- a/.github/workflows/get-version.yml
+++ b/.github/workflows/get-version.yml
@@ -22,8 +22,8 @@ on:
value: ${{ jobs.determine-version.outputs.type }}
env:
- JAVA_DIST: 'temurin'
- JAVA_VERSION: 20
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
jobs:
determine-version:
diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml
index ea6fed274..8db4296d9 100644
--- a/.github/workflows/mac-dmg.yml
+++ b/.github/workflows/mac-dmg.yml
@@ -15,8 +15,8 @@ on:
type: boolean
env:
- JAVA_DIST: 'temurin'
- JAVA_VERSION: 20
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
jobs:
get-version:
diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index 9b615756a..931817418 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -4,8 +4,8 @@ on:
pull_request:
env:
- JAVA_DIST: 'temurin'
- JAVA_VERSION: 20
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
defaults:
run:
diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml
index 024f38696..2b72a50a8 100644
--- a/.github/workflows/win-exe.yml
+++ b/.github/workflows/win-exe.yml
@@ -14,8 +14,8 @@ on:
env:
- JAVA_DIST: 'temurin'
- JAVA_VERSION: 20
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_windows-x64_bin-jmods.zip'
OPENJFX_JMODS_AMD64_HASH: '18625bbc13c57dbf802486564247a8d8cab72ec558c240a401bf6440384ebd77'
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 1d25cbef3..df360a26d 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -14,10 +14,10 @@
-
-
+
+
-
+
@@ -26,20 +26,19 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 891096945..ee700a1c2 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -8,7 +8,7 @@
-
+
\ No newline at end of file
diff --git a/dist/linux/debian/control b/dist/linux/debian/control
index b04812fbb..148fdb213 100644
--- a/dist/linux/debian/control
+++ b/dist/linux/debian/control
@@ -2,7 +2,7 @@ Source: cryptomator
Maintainer: Cryptobot
Section: utils
Priority: optional
-Build-Depends: debhelper (>=10), coffeelibs-jdk-20, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1
+Build-Depends: debhelper (>=10), coffeelibs-jdk-21, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1
Standards-Version: 4.5.0
Homepage: https://cryptomator.org
Vcs-Git: https://github.com/cryptomator/cryptomator.git
diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules
index c12879025..d0a12e380 100755
--- a/dist/linux/debian/rules
+++ b/dist/linux/debian/rules
@@ -4,7 +4,7 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
-JAVA_HOME = /usr/lib/jvm/java-20-coffeelibs
+JAVA_HOME = /usr/lib/jvm/java-21-coffeelibs
DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
ifeq ($(DEB_BUILD_ARCH),amd64)
JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods
diff --git a/pom.xml b/pom.xml
index 2c6faa90c..99cf4961f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
UTF-8
- 20
+ 21
@@ -35,22 +35,22 @@
2.6.7
1.3.0
- 1.2.3
+ 1.2.4
1.2.2
- 1.3.0
- 3.0.0
+ 1.4.0-beta2
+ 4.0.0-beta1
2.0.0
- 2.0.4
+ 2.0.5
3.13.0
- 2.48
+ 2.48.1
2.2
- 32.1.2-jre
- 2.15.2
+ 32.1.3-jre
+ 2.15.3
20.0.2
4.4.0
- 9.31
+ 9.36
1.4.11
2.0.9
0.6.0
@@ -64,7 +64,7 @@
24.0.1
8.4.0
- 0.8.10
+ 0.8.11
2.2.0
1.2.1
3.11.0
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index 678795662..144b8bbb6 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -20,9 +20,10 @@ public enum FxmlFile {
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
HUB_INVALID_LICENSE("/fxml/hub_invalid_license.fxml"), //
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
- HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
+ HUB_LEGACY_REGISTER_DEVICE("/fxml/hub_legacy_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/health/StartController.java b/src/main/java/org/cryptomator/ui/health/StartController.java
index 4e95b6b0f..9ff2502da 100644
--- a/src/main/java/org/cryptomator/ui/health/StartController.java
+++ b/src/main/java/org/cryptomator/ui/health/StartController.java
@@ -101,16 +101,16 @@ public class StartController implements FxController {
}
}
- private void loadingKeyFailed(Throwable e) {
- switch (e) {
- case UnlockCancelledException uce -> {} //ok
- case VaultKeyInvalidException vkie -> {
- LOG.error("Invalid key"); //TODO: specific error screen
+ private void loadingKeyFailed(Throwable t) {
+ switch (t) {
+ case UnlockCancelledException e -> {} // ok // TODO: rename to _ with JEP 443
+ case VaultKeyInvalidException e -> { // TODO: rename to _ with JEP 443
+ LOG.error("Invalid key"); // TODO: specific error screen
appWindows.showErrorWindow(e, window, null);
}
default -> {
- LOG.error("Failed to load key.", e);
- appWindows.showErrorWindow(e, window, null);
+ LOG.error("Failed to load key.", t);
+ appWindows.showErrorWindow(t, window, null);
}
}
}
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/CreateDeviceDto.java b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java
deleted file mode 100644
index ed10a9257..000000000
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.cryptomator.ui.keyloading.hub;
-
-record CreateDeviceDto(String id, String name, String publicKey) {
-
-}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java
deleted file mode 100644
index 0077467cc..000000000
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.cryptomator.ui.keyloading.hub;
-
-import com.google.common.io.CharStreams;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
-
-class HttpHelper {
-
- public static String readBody(HttpResponse response) throws IOException {
- try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
- return CharStreams.toString(reader);
- }
- }
-
-}
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 5f462b170..ef1fe30df 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
@@ -1,6 +1,10 @@
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;
// needs to be accessible by JSON decoder
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -9,8 +13,19 @@ public class HubConfig {
public String clientId;
public String authEndpoint;
public String tokenEndpoint;
- public String devicesResourceUrl;
public String authSuccessUrl;
public String authErrorUrl;
+ public @Nullable String apiBaseUrl;
+ @Deprecated // use apiBaseUrl + "/devices/"
+ public String devicesResourceUrl;
+ public URI getApiBaseUrl() {
+ if (apiBaseUrl != null) {
+ return URI.create(apiBaseUrl);
+ } else {
+ // legacy approach
+ assert devicesResourceUrl != null;
+ return URI.create(devicesResourceUrl + "/..").normalize();
+ }
+ }
}
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..c8d308e8e 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
@@ -1,7 +1,6 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.io.BaseEncoding;
-import com.nimbusds.jose.JWEObject;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -69,7 +68,7 @@ public abstract class HubKeyLoadingModule {
@Provides
@KeyLoadingScoped
- static CompletableFuture provideResult() {
+ static CompletableFuture provideResult() {
return new CompletableFuture<>();
}
@@ -114,10 +113,10 @@ public abstract class HubKeyLoadingModule {
}
@Provides
- @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE)
+ @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE)
@KeyLoadingScoped
- static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
- return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
+ static Scene provideHubLegacyRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE);
}
@Provides
@@ -134,6 +133,13 @@ public abstract class HubKeyLoadingModule {
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_FAILED);
}
+ @Provides
+ @FxmlScene(FxmlFile.HUB_SETUP_DEVICE)
+ @KeyLoadingScoped
+ static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene(FxmlFile.HUB_SETUP_DEVICE);
+ }
+
@Provides
@FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE)
@KeyLoadingScoped
@@ -166,6 +172,11 @@ public abstract class HubKeyLoadingModule {
@FxControllerKey(RegisterDeviceController.class)
abstract FxController bindRegisterDeviceController(RegisterDeviceController controller);
+ @Binds
+ @IntoMap
+ @FxControllerKey(LegacyRegisterDeviceController.class)
+ abstract FxController bindLegacyRegisterDeviceController(LegacyRegisterDeviceController controller);
+
@Binds
@IntoMap
@FxControllerKey(RegisterSuccessController.class)
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..2333051be 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java
@@ -2,35 +2,103 @@ 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 {
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 JWEObject encryptUserKey(ECPrivateKey userKey, ECPublicKey deviceKey) {
try {
- jwe.decrypt(new ECDHDecrypter(privateKey));
- return readKey(jwe);
+ 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) {
- LOG.warn("Failed to decrypt JWE: {}", jwe);
- throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e);
+ throw new RuntimeException(e);
}
}
- private static Masterkey readKey(JWEObject jwe) throws MasterkeyLoadingFailedException {
+ public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) throws InvalidJweKeyException {
+ try {
+ jwe.decrypt(new PasswordBasedDecrypter(setupCode));
+ return decodeUserKey(jwe);
+ } catch (JOSEException e) {
+ throw new InvalidJweKeyException(e);
+ }
+ }
+
+ public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) throws InvalidJweKeyException {
+ try {
+ jwe.decrypt(new ECDHDecrypter(deviceKey));
+ return decodeUserKey(jwe);
+ } catch (JOSEException e) {
+ throw new InvalidJweKeyException(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) {
+ return ecPrivateKey;
+ } else {
+ throw new IllegalStateException(EC_ALG + " key factory not generating ECPrivateKeys");
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(EC_ALG + " not supported");
+ } catch (InvalidKeySpecException e) {
+ LOG.warn("Unexpected JWE payload: {}", decryptedJwe.getPayload());
+ throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
+ }
+ }
+
+ public static Masterkey decryptVaultKey(JWEObject jwe, ECPrivateKey privateKey) throws InvalidJweKeyException {
+ try {
+ jwe.decrypt(new ECDHDecrypter(privateKey));
+ return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, Masterkey::new);
+ } catch (JOSEException e) {
+ throw new InvalidJweKeyException(e);
+ }
+ }
+
+ 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 +107,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());
@@ -52,4 +120,11 @@ class JWEHelper {
Arrays.fill(keyBytes, (byte) 0x00);
}
}
+
+ public static class InvalidJweKeyException extends MasterkeyLoadingFailedException {
+
+ public InvalidJweKeyException(Throwable cause) {
+ super("Invalid key", cause);
+ }
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java
new file mode 100644
index 000000000..113ecd249
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java
@@ -0,0 +1,191 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.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.util.Base64;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoadingScoped
+public class LegacyRegisterDeviceController implements FxController {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LegacyRegisterDeviceController.class);
+ private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
+ private static final List EXPECTED_RESPONSE_CODES = List.of(201, 409);
+
+ 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 keyPair;
+ private final CompletableFuture result;
+ private final DecodedJWT jwt;
+ private final HttpClient httpClient;
+ private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
+
+ public TextField deviceNameField;
+ public Button registerBtn;
+
+ @Inject
+ public LegacyRegisterDeviceController(@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.keyPair = 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() {
+ deviceNameAlreadyExists.set(false);
+ registerBtn.setContentDisplay(ContentDisplay.LEFT);
+ registerBtn.setDisable(true);
+
+ 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 = toJson(dto);
+ var request = HttpRequest.newBuilder(deviceUri) //
+ .PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
+ .header("Authorization", "Bearer " + bearerToken) //
+ .header("Content-Type", "application/json") //
+ .build();
+ httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) //
+ .thenApply(response -> {
+ if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) {
+ return response;
+ } else {
+ throw new RuntimeException("Server answered with unexpected status code " + response.statusCode());
+ }
+ }).handleAsync((response, throwable) -> {
+ if (response != null) {
+ this.handleResponse(response);
+ } else {
+ this.registrationFailed(throwable);
+ }
+ return null;
+ }, Platform::runLater);
+ }
+
+ private String toJson(CreateDeviceDto dto) {
+ try {
+ return JSON.writer().writeValueAsString(dto);
+ } catch (JacksonException e) {
+ throw new IllegalStateException("Failed to serialize DTO", e);
+ }
+ }
+
+ private void handleResponse(HttpResponse voidHttpResponse) {
+ assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode());
+
+ if (voidHttpResponse.statusCode() == 409) {
+ deviceNameAlreadyExists.set(true);
+ registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY);
+ registerBtn.setDisable(false);
+ } else {
+ LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl);
+ window.setScene(registerSuccessScene.get());
+ }
+ }
+
+ 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 static class CreateDeviceDto {
+ public String id;
+ public String name;
+ public final String type = "DESKTOP";
+ public String publicKey;
+
+ }
+
+}
\ No newline at end of file
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 bd7497bec..66566a98a 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
@@ -1,5 +1,8 @@
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.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
@@ -8,6 +11,9 @@ 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.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
@@ -17,14 +23,16 @@ 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.time.Duration;
+import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@@ -33,25 +41,32 @@ 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 static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
+ private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10);
private final Stage window;
+ private final HubConfig hubConfig;
private final String deviceId;
private final String bearerToken;
- private final CompletableFuture result;
- private final Lazy registerDeviceScene;
+ private final CompletableFuture result;
+ private final Lazy setupDeviceScene;
+ private final Lazy legacyRegisterDeviceScene;
private final Lazy unauthorizedScene;
private final URI vaultBaseUri;
private final Lazy invalidLicenseScene;
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_SETUP_DEVICE) Lazy setupDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, @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.registerDeviceScene = registerDeviceScene;
+ this.setupDeviceScene = setupDeviceScene;
+ this.legacyRegisterDeviceScene = legacyRegisterDeviceScene;
this.unauthorizedScene = unauthorizedScene;
this.vaultBaseUri = getVaultBaseUri(vault);
this.invalidLicenseScene = invalidLicenseScene;
@@ -61,23 +76,120 @@ public class ReceiveKeyController implements FxController {
@FXML
public void initialize() {
- var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
- var request = HttpRequest.newBuilder(keyUri) //
+ requestVaultMasterkey();
+ }
+
+ /**
+ * STEP 1 (Request): GET vault key for this user
+ */
+ private void requestVaultMasterkey() {
+ var accessTokenUri = appendPath(vaultBaseUri, "/access-token");
+ var request = HttpRequest.newBuilder(accessTokenUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
+ .timeout(REQ_TIMEOUT) //
.build();
- httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
- .thenAcceptAsync(this::loadedExistingKey, Platform::runLater) //
+ httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
+ .thenAcceptAsync(this::receivedVaultMasterkey, Platform::runLater) //
.exceptionally(this::retrievalFailed);
}
- private void loadedExistingKey(HttpResponse response) {
+ /**
+ * STEP 1 (Response): GET vault key for this user
+ *
+ * @param response Response
+ */
+ private void receivedVaultMasterkey(HttpResponse response) {
+ LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
+ switch (response.statusCode()) {
+ case 200 -> requestUserKey(response.body());
+ case 402 -> licenseExceeded();
+ case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access - TODO: add specific dialog?
+ case 404 -> requestLegacyAccessToken();
+ default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
+ }
+ }
+
+ /**
+ * STEP 2 (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) //
+ .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) //
+ .exceptionally(this::retrievalFailed);
+ }
+
+ /**
+ * STEP 2 (Response): GET user key for this device
+ *
+ * @param response Response
+ */
+ private void receivedUserKey(String encryptedVaultKey, HttpResponse response) {
+ LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
try {
switch (response.statusCode()) {
- case 200 -> retrievalSucceeded(response);
+ case 200 -> {
+ var device = JSON.reader().readValue(response.body(), DeviceDto.class);
+ receivedBothEncryptedKeys(encryptedVaultKey, device.userPrivateKey);
+ }
+ case 404 -> needsDeviceSetup(); // TODO: using the setup code, we can theoretically immediately unlock
+ default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private void needsDeviceSetup() {
+ window.setScene(setupDeviceScene.get());
+ }
+
+ private void receivedBothEncryptedKeys(String encryptedVaultKey, String encryptedUserKey) throws IOException {
+ 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);
+ }
+ }
+
+ /**
+ * LEGACY FALLBACK (Request): GET the legacy access token from Hub 1.x
+ */
+ @Deprecated
+ private void requestLegacyAccessToken() {
+ var legacyAccessTokenUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
+ var request = HttpRequest.newBuilder(legacyAccessTokenUri) //
+ .header("Authorization", "Bearer " + bearerToken) //
+ .GET() //
+ .timeout(REQ_TIMEOUT) //
+ .build();
+ httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
+ .thenAcceptAsync(this::receivedLegacyAccessTokenResponse, Platform::runLater) //
+ .exceptionally(this::retrievalFailed);
+ }
+
+ /**
+ * LEGACY FALLBACK (Response)
+ *
+ * @param response Response
+ */
+ @Deprecated
+ private void receivedLegacyAccessTokenResponse(HttpResponse response) {
+ try {
+ switch (response.statusCode()) {
+ case 200 -> receivedLegacyAccessTokenSuccess(response.body());
case 402 -> licenseExceeded();
case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access
- case 404 -> needsDeviceRegistration();
+ case 404 -> needsLegacyDeviceRegistration();
default -> throw new IOException("Unexpected response " + response.statusCode());
}
} catch (IOException e) {
@@ -85,10 +197,11 @@ public class ReceiveKeyController implements FxController {
}
}
- private void retrievalSucceeded(HttpResponse response) throws IOException {
+ @Deprecated
+ 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);
@@ -99,8 +212,9 @@ public class ReceiveKeyController implements FxController {
window.setScene(invalidLicenseScene.get());
}
- private void needsDeviceRegistration() {
- window.setScene(registerDeviceScene.get());
+ @Deprecated
+ private void needsLegacyDeviceRegistration() {
+ window.setScene(legacyRegisterDeviceScene.get());
}
private void accessNotGranted() {
@@ -132,14 +246,17 @@ public class ReceiveKeyController implements FxController {
private static URI getVaultBaseUri(Vault vault) {
try {
- var kid = vault.getVaultConfigCache().get().getKeyId();
- assert kid.getScheme().startsWith(SCHEME_PREFIX);
- var hubUriScheme = kid.getScheme().substring(SCHEME_PREFIX.length());
- return new URI(hubUriScheme, kid.getSchemeSpecificPart(), kid.getFragment());
+ 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);
}
}
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
}
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..74da388d7
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java
@@ -0,0 +1,45 @@
+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 {
+
+ /**
+ * Decrypts the vault key.
+ *
+ * @param deviceKey This device's private key.
+ * @return The decrypted vault key
+ */
+ Masterkey decryptMasterkey(ECPrivateKey deviceKey);
+
+ /**
+ * Creates an unlock response object from the user key + vault key.
+ *
+ * @param vaultKeyJwe a JWE containing the symmetric vault key, encrypted for this device's user.
+ * @param userKeyJwe a JWE containing the user's private key, encrypted for this device.
+ * @return Ciphertext received by Hub, which can be decrypted using this device's private key.
+ */
+ static ReceivedKey vaultKeyAndUserKey(JWEObject vaultKeyJwe, JWEObject userKeyJwe) {
+ return deviceKey -> {
+ var userKey = JWEHelper.decryptUserKey(userKeyJwe, deviceKey);
+ return JWEHelper.decryptVaultKey(vaultKeyJwe, userKey);
+ };
+ }
+
+ /**
+ * Creates an unlock response object from the received legacy "access token" JWE.
+ *
+ * @param vaultKeyJwe a JWE containing the symmetric vault key, encrypted for this device.
+ * @return Ciphertext received by Hub, which can be decrypted using this device's private key.
+ * @deprecated Only for compatibility with Hub 1.0 - 1.2
+ */
+ @Deprecated
+ static ReceivedKey legacyDeviceKey(JWEObject vaultKeyJwe) {
+ return deviceKey -> JWEHelper.decryptVaultKey(vaultKeyJwe, 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 6fa6aa424..837dc5032 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java
@@ -1,7 +1,7 @@
package org.cryptomator.ui.keyloading.hub;
-import com.auth0.jwt.JWT;
-import com.auth0.jwt.interfaces.DecodedJWT;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.BaseEncoding;
@@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
@@ -31,14 +32,16 @@ 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.util.List;
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@@ -47,7 +50,7 @@ public class RegisterDeviceController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RegisterDeviceController.class);
private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
- private static final List EXPECTED_RESPONSE_CODES = List.of(201, 409);
+ private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10);
private final Stage window;
private final HubConfig hubConfig;
@@ -55,26 +58,27 @@ public class RegisterDeviceController implements FxController {
private final Lazy registerSuccessScene;
private final Lazy registerFailedScene;
private final String deviceId;
- private final P384KeyPair keyPair;
- private final CompletableFuture result;
- private final DecodedJWT jwt;
+ private final P384KeyPair deviceKeyPair;
+ private final CompletableFuture result;
private final HttpClient httpClient;
- private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
+ private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
+ private final BooleanProperty invalidSetupCode = new SimpleBooleanProperty(false);
+ private final BooleanProperty workInProgress = new SimpleBooleanProperty(false);
+ public TextField setupCodeField;
public TextField deviceNameField;
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;
- this.keyPair = Objects.requireNonNull(deviceKey.get());
+ 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();
}
@@ -82,6 +86,13 @@ public class RegisterDeviceController implements FxController {
public void initialize() {
deviceNameField.setText(determineHostname());
deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false));
+ deviceNameField.disableProperty().bind(workInProgress);
+ setupCodeField.textProperty().addListener(observable -> invalidSetupCode.set(false));
+ setupCodeField.disableProperty().bind(workInProgress);
+ var missingSetupCode = setupCodeField.textProperty().isEmpty();
+ var missingDeviceName = deviceNameField.textProperty().isEmpty();
+ registerBtn.disableProperty().bind(workInProgress.or(missingSetupCode).or(missingDeviceName));
+ registerBtn.contentDisplayProperty().bind(Bindings.when(workInProgress).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY));
}
private String determineHostname() {
@@ -95,35 +106,62 @@ public class RegisterDeviceController implements FxController {
@FXML
public void register() {
- deviceNameAlreadyExists.set(false);
- registerBtn.setContentDisplay(ContentDisplay.LEFT);
- registerBtn.setDisable(true);
+ workInProgress.set(true);
- var keyUri = URI.create(hubConfig.devicesResourceUrl + deviceId);
- var deviceKey = keyPair.getPublic().getEncoded();
- var dto = new CreateDeviceDto(deviceId, deviceNameField.getText(), BaseEncoding.base64Url().omitPadding().encode(deviceKey));
- var json = toJson(dto);
- var request = HttpRequest.newBuilder(keyUri) //
+ var apiRootUrl = hubConfig.getApiBaseUrl();
+
+ var userReq = HttpRequest.newBuilder(apiRootUrl.resolve("users/me")) //
+ .GET() //
+ .timeout(REQ_TIMEOUT) //
.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()) //
+ httpClient.sendAsync(userReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) //
.thenApply(response -> {
- if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) {
- return response;
+ if (response.statusCode() == 200) {
+ var dto = fromJson(response.body());
+ return Objects.requireNonNull(dto, "null or empty response body");
} else {
throw new RuntimeException("Server answered with unexpected status code " + response.statusCode());
}
- }).handleAsync((response, throwable) -> {
+ }).thenApply(user -> {
+ try {
+ assert user.privateKey != null; // api/vaults/{v}/user-tokens/me would have returned 403, if user wasn't fully set up yet
+ 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 now = Instant.now().toString();
+ var dto = new CreateDeviceDto(deviceId, deviceNameField.getText(), BaseEncoding.base64().encode(deviceKeyPair.getPublic().getEncoded()), "DESKTOP", jwe.serialize(), now);
+ var json = toJson(dto);
+ var deviceUri = apiRootUrl.resolve("devices/" + deviceId);
+ var putDeviceReq = HttpRequest.newBuilder(deviceUri) //
+ .PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
+ .timeout(REQ_TIMEOUT) //
+ .header("Authorization", "Bearer " + bearerToken) //
+ .header("Content-Type", "application/json") //
+ .build();
+ return httpClient.sendAsync(putDeviceReq, HttpResponse.BodyHandlers.discarding());
+ }).whenCompleteAsync((response, throwable) -> {
if (response != null) {
this.handleResponse(response);
} else {
- this.registrationFailed(throwable);
+ this.setupFailed(throwable);
}
- return null;
+ workInProgress.set(false);
}, Platform::runLater);
}
+ private UserDto fromJson(String json) {
+ try {
+ return JSON.reader().readValue(json, UserDto.class);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to deserialize DTO", e);
+ }
+ }
+
private String toJson(CreateDeviceDto dto) {
try {
return JSON.writer().writeValueAsString(dto);
@@ -132,23 +170,26 @@ public class RegisterDeviceController implements FxController {
}
}
- private void handleResponse(HttpResponse voidHttpResponse) {
- assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode());
-
- if (voidHttpResponse.statusCode() == 409) {
- deviceNameAlreadyExists.set(true);
- registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY);
- registerBtn.setDisable(false);
- } else {
+ 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);
+ } else {
+ setupFailed(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);
+ private void setupFailed(Throwable cause) {
+ switch (cause) {
+ case CompletionException e when e.getCause() instanceof JWEHelper.InvalidJweKeyException -> invalidSetupCode.set(true);
+ default -> {
+ LOG.warn("Device setup failed.", cause);
+ window.setScene(registerFailedScene.get());
+ result.completeExceptionally(cause);
+ }
+ }
}
@FXML
@@ -160,13 +201,6 @@ public class RegisterDeviceController implements FxController {
result.cancel(true);
}
- /* Getter */
-
- public String getUserName() {
- return jwt.getClaim("email").asString();
- }
-
-
//--- Getters & Setters
public BooleanProperty deviceNameAlreadyExistsProperty() {
@@ -177,5 +211,21 @@ public class RegisterDeviceController implements FxController {
return deviceNameAlreadyExists.get();
}
+ public BooleanProperty invalidSetupCodeProperty() {
+ return invalidSetupCode;
+ }
+ public boolean isInvalidSetupCode() {
+ return invalidSetupCode.get();
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record UserDto(String id, String name, String publicKey, String privateKey, String setupCode) {}
+
+ private record CreateDeviceDto(@JsonProperty(required = true) String id, //
+ @JsonProperty(required = true) String name, //
+ @JsonProperty(required = true) String publicKey, //
+ @JsonProperty(required = true, defaultValue = "DESKTOP") String type, //
+ @JsonProperty(required = true) String userPrivateKey, //
+ @JsonProperty(required = true) String creationTime) {}
}
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/main/java/org/cryptomator/ui/mainwindow/ResizeController.java b/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java
index b136fa55c..2c3838ea0 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java
@@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.binding.BooleanBinding;
-import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Rectangle2D;
import javafx.scene.input.MouseEvent;
@@ -67,37 +66,7 @@ public class ResizeController implements FxController {
return (settings.windowHeight.get() == 0) && (settings.windowWidth.get() == 0) && (settings.windowXPosition.get() == 0) && (settings.windowYPosition.get() == 0);
}
- private boolean isWithinDisplayBounds() {
- // (x1, y1) is the top left corner of the window, (x2, y2) is the bottom right corner
- final double slack = 10;
- final double width = window.getWidth() - 2 * slack;
- final double height = window.getHeight() - 2 * slack;
- final double x1 = window.getX() + slack;
- final double y1 = window.getY() + slack;
- final double x2 = x1 + width;
- final double y2 = y1 + height;
-
- final ObservableList screens = Screen.getScreensForRectangle(x1, y1, width, height);
-
- // Find the total visible area of the window
- double visibleArea = 0;
- for (Screen screen : screens) {
- Rectangle2D bounds = screen.getVisualBounds();
-
- double xOverlap = Math.min(x2, bounds.getMaxX()) - Math.max(x1, bounds.getMinX());
- double yOverlap = Math.min(y2, bounds.getMaxY()) - Math.max(y1, bounds.getMinY());
-
- visibleArea += xOverlap * yOverlap;
- }
-
- final double windowArea = width * height;
-
- // Within bounds if the visible area matches the window area
- return visibleArea == windowArea;
- }
-
private void checkDisplayBounds(WindowEvent evt) {
-
// Minimizing a window in Windows and closing it could result in an out of bounds position at (x, y) = (-32000, -32000)
// See https://devblogs.microsoft.com/oldnewthing/20041028-00/?p=37453
// If the position is (-32000, -32000), restore to the last saved position
@@ -108,8 +77,9 @@ public class ResizeController implements FxController {
window.setHeight(settings.windowHeight.get());
}
- if (!isWithinDisplayBounds()) {
+ if (isOutOfDisplayBounds()) {
// If the position is illegal, then the window appears on the main screen in the middle of the window.
+ LOG.debug("Resetting window position due to insufficient screen overlap");
Rectangle2D primaryScreenBounds = Screen.getPrimary().getBounds();
window.setX((primaryScreenBounds.getWidth() - window.getMinWidth()) / 2);
window.setY((primaryScreenBounds.getHeight() - window.getMinHeight()) / 2);
@@ -119,6 +89,22 @@ public class ResizeController implements FxController {
}
}
+ private boolean isOutOfDisplayBounds() {
+ // define a rect which is inset on all sides from the window's rect:
+ final double x = window.getX() + 20; // 20px left
+ final double y = window.getY() + 5; // 5px top
+ final double w = window.getWidth() - 40; // 20px left + 20px right
+ final double h = window.getHeight() - 25; // 5px top + 20px bottom
+ return isRectangleOutOfScreen(x, y, 0, h) // Left pixel column
+ || isRectangleOutOfScreen(x + w, y, 0, h) // Right pixel column
+ || isRectangleOutOfScreen(x, y, w, 0) // Top pixel row
+ || isRectangleOutOfScreen(x, y + h, w, 0); // Bottom pixel row
+ }
+
+ private boolean isRectangleOutOfScreen(double x, double y, double width, double height) {
+ return Screen.getScreensForRectangle(x, y, width, height).isEmpty();
+ }
+
private void startResize(MouseEvent evt) {
origX = window.getX();
origY = window.getY();
diff --git a/src/main/resources/fxml/hub_register_device.fxml b/src/main/resources/fxml/hub_legacy_register_device.fxml
similarity index 96%
rename from src/main/resources/fxml/hub_register_device.fxml
rename to src/main/resources/fxml/hub_legacy_register_device.fxml
index 8db67c272..51d4cf8b7 100644
--- a/src/main/resources/fxml/hub_register_device.fxml
+++ b/src/main/resources/fxml/hub_legacy_register_device.fxml
@@ -15,7 +15,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties
index bba386cee..aee61fe19 100644
--- a/src/main/resources/i18n/strings.properties
+++ b/src/main/resources/i18n/strings.properties
@@ -154,9 +154,11 @@ hub.auth.loginLink=Not redirected? Click here to open it.
hub.receive.message=Processing response…
hub.receive.description=Cryptomator is receiving and processing the response from Hub. Please wait.
### Register Device
-hub.register.message=Device name required
-hub.register.description=This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.
+hub.register.message=New Device
+hub.register.description=This is the first Hub access from this device. Please authorize it using your setup code.
hub.register.nameLabel=Device Name
+hub.register.setupCodeLabel=Setup Code
+hub.register.invalidSetupCode=Invalid Setup Code
hub.register.occupiedMsg=Name already in use
hub.register.registerBtn=Confirm
### Registration Success
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..a2e46cc28 100644
--- a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java
+++ b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java
@@ -4,6 +4,7 @@ import com.nimbusds.jose.JWEObject;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.P384KeyPair;
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;
@@ -15,18 +16,106 @@ import java.text.ParseException;
import java.util.Arrays;
import java.util.Base64;
+@SuppressWarnings("resource")
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 with device key")
+ public void testDecryptUserKeyECDHES() 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("decryptUserKey with incorrect device key")
+ public void testDecryptUserKeyECDHESWrongKey() 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 userKeyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(USER_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(USER_PRIV_KEY)));
+ var incorrectDevicePrivateKey = userKeyPair.getPrivate();
+
+ Assertions.assertThrows(JWEHelper.InvalidJweKeyException.class, () -> JWEHelper.decryptUserKey(jwe, incorrectDevicePrivateKey));
+ }
+
+ @Test
+ @DisplayName("decryptUserKey with setup code")
+ public void testDecryptUserKeyPBES2() throws ParseException {
+ var jwe = JWEObject.parse("""
+ eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\
+ xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\
+ aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\
+ P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\
+ mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\
+ kDjUaxwUKqpvT7qaAQ
+ """);
+
+ var userKey = JWEHelper.decryptUserKey(jwe, "123456");
+
+ Assertions.assertArrayEquals(Base64.getDecoder().decode(PRIV_KEY), userKey.getEncoded());
+ }
+
+ @Test
+ @DisplayName("decryptUserKey with incorrect setup code")
+ public void testDecryptUserKeyPBES2WrongKey() throws ParseException {
+ var jwe = JWEObject.parse("""
+ eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\
+ xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\
+ aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\
+ P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\
+ mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\
+ kDjUaxwUKqpvT7qaAQ
+ """);
+
+ Assertions.assertThrows(JWEHelper.InvalidJweKeyException.class, () -> JWEHelper.decryptUserKey(jwe, "654321"));
+ }
+
+ @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,13 +133,11 @@ 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());
- });
+ Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> JWEHelper.decryptVaultKey(jwe, privateKey));
}
}
\ No newline at end of file