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 84cac8ed2..eefad55a2 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
@@ -1,5 +1,6 @@
package org.cryptomator.ui.keyloading.hub;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.jetbrains.annotations.Nullable;
@@ -18,11 +19,18 @@ public class HubConfig {
@Deprecated // use apiBaseUrl + "/devices/"
public String devicesResourceUrl;
+ /**
+ * A collection of String template processors to construct URIs related to this Hub instance.
+ */
+ @JsonIgnore
+ public final URIProcessors URIs = new URIProcessors();
+
/**
* Get the URI pointing to the /api/ base resource.
*
* @return /api/ URI
* @apiNote URI is guaranteed to end on /
+ * @see #URIs
*/
public URI getApiBaseUrl() {
if (apiBaseUrl != null) {
@@ -38,4 +46,17 @@ public class HubConfig {
public URI getWebappBaseUrl() {
return getApiBaseUrl().resolve("../app/");
}
+
+ public class URIProcessors {
+
+ /**
+ * Resolves paths relative to the /api/ endpoint of this Hub instance.
+ */
+ public final StringTemplate.Processor API = template -> {
+ var path = template.interpolate();
+ var relPath = path.startsWith("/") ? path.substring(1) : path;
+ return getApiBaseUrl().resolve(relPath);
+ };
+
+ }
}
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 2333051be..41bb6902a 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java
@@ -1,7 +1,6 @@
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;
@@ -13,19 +12,20 @@ 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.CryptoException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.security.Key;
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.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
@@ -37,26 +37,16 @@ class JWEHelper {
private static final String JWE_PAYLOAD_KEY_FIELD = "key";
private static final String EC_ALG = "EC";
- private JWEHelper(){}
+ 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);
- }
+ return encryptKey(userKey, deviceKey);
}
public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) throws InvalidJweKeyException {
try {
jwe.decrypt(new PasswordBasedDecrypter(setupCode));
- return decodeUserKey(jwe);
+ return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, JWEHelper::decodeECPrivateKey);
} catch (JOSEException e) {
throw new InvalidJweKeyException(e);
}
@@ -65,17 +55,23 @@ class JWEHelper {
public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) throws InvalidJweKeyException {
try {
jwe.decrypt(new ECDHDecrypter(deviceKey));
- return decodeUserKey(jwe);
+ return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, JWEHelper::decodeECPrivateKey);
} catch (JOSEException e) {
throw new InvalidJweKeyException(e);
}
}
- private static ECPrivateKey decodeUserKey(JWEObject decryptedJwe) {
+ /**
+ * Attempts to decode a DER-encoded EC private key.
+ *
+ * @param encoded DER-encoded EC private key
+ * @return the decoded key
+ * @throws KeyDecodeFailedException On malformed input
+ */
+ public static ECPrivateKey decodeECPrivateKey(byte[] encoded) throws KeyDecodeFailedException {
try {
- var keySpec = readKey(decryptedJwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new);
- var factory = KeyFactory.getInstance(EC_ALG);
- var privateKey = factory.generatePrivate(keySpec);
+ KeyFactory factory = KeyFactory.getInstance(EC_ALG);
+ var privateKey = factory.generatePrivate(new PKCS8EncodedKeySpec(encoded));
if (privateKey instanceof ECPrivateKey ecPrivateKey) {
return ecPrivateKey;
} else {
@@ -84,8 +80,49 @@ class JWEHelper {
} 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);
+ throw new KeyDecodeFailedException(e);
+ }
+ }
+
+ /**
+ * Attempts to decode a DER-encoded EC public key.
+ *
+ * @param encoded DER-encoded EC public key
+ * @return the decoded key
+ * @throws KeyDecodeFailedException On malformed input
+ */
+ public static ECPublicKey decodeECPublicKey(byte[] encoded) throws KeyDecodeFailedException {
+ try {
+ KeyFactory factory = KeyFactory.getInstance(EC_ALG);
+ var publicKey = factory.generatePublic(new X509EncodedKeySpec(encoded));
+ if (publicKey instanceof ECPublicKey ecPublicKey) {
+ return ecPublicKey;
+ } else {
+ throw new IllegalStateException(EC_ALG + " key factory not generating ECPublicKeys");
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(EC_ALG + " not supported");
+ } catch (InvalidKeySpecException e) {
+ throw new KeyDecodeFailedException(e);
+ }
+ }
+
+ public static JWEObject encryptVaultKey(Masterkey vaultKey, ECPublicKey userKey) {
+ return encryptKey(vaultKey, userKey);
+ }
+
+ private static JWEObject encryptKey(Key key, ECPublicKey userKey) {
+ try {
+ var encodedVaultKey = Base64.getEncoder().encodeToString(key.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, encodedVaultKey));
+ var jwe = new JWEObject(header, payload);
+ jwe.encrypt(new ECDHEncrypter(userKey));
+ return jwe;
+ } catch (JOSEException e) {
+ throw new RuntimeException(e);
}
}
@@ -108,12 +145,12 @@ class JWEHelper {
var keyBytes = new byte[0];
try {
if (fields.get(keyField) instanceof String key) {
- keyBytes = BaseEncoding.base64().decode(key);
+ keyBytes = Base64.getDecoder().decode(key);
return rawKeyFactory.apply(keyBytes);
} else {
throw new IllegalArgumentException("JWE payload doesn't contain field " + keyField);
}
- } catch (IllegalArgumentException e) {
+ } catch (IllegalArgumentException | KeyDecodeFailedException e) {
LOG.error("Unexpected JWE payload: {}", jwe.getPayload());
throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
} finally {
@@ -127,4 +164,11 @@ class JWEHelper {
super("Invalid key", cause);
}
}
+
+ public static class KeyDecodeFailedException extends CryptoException {
+
+ public KeyDecodeFailedException(Throwable cause) {
+ super("Malformed key", cause);
+ }
+ }
}
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 11840f989..3bfb4ec8e 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
@@ -57,7 +57,6 @@ public class ReceiveKeyController implements FxController {
private final Lazy accountInitializationScene;
private final Lazy invalidLicenseScene;
private final HttpClient httpClient;
- private final StringTemplate.Processor API_BASE = this::resolveRelativeToApiBase;
@Inject
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy accountInitializationScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) {
@@ -89,7 +88,7 @@ public class ReceiveKeyController implements FxController {
* STEP 0 (Request): GET /api/config
*/
private void requestApiConfig() {
- var configUri = API_BASE."config";
+ var configUri = hubConfig.URIs.API."config";
var request = HttpRequest.newBuilder(configUri) //
.GET() //
.timeout(REQ_TIMEOUT) //
@@ -123,7 +122,7 @@ public class ReceiveKeyController implements FxController {
* STEP 1 (Request): GET user key for this device
*/
private void requestDeviceData() {
- var deviceUri = API_BASE."devices/\{deviceId}";
+ var deviceUri = hubConfig.URIs.API."devices/\{deviceId}";
var request = HttpRequest.newBuilder(deviceUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
@@ -163,7 +162,7 @@ public class ReceiveKeyController implements FxController {
* STEP 2 (Request): GET vault key for this user
*/
private void requestVaultMasterkey(String encryptedUserKey) {
- var vaultKeyUri = API_BASE."vaults/\{vaultId}/access-token";
+ var vaultKeyUri = hubConfig.URIs.API."vaults/\{vaultId}/access-token";
var request = HttpRequest.newBuilder(vaultKeyUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
@@ -206,7 +205,7 @@ public class ReceiveKeyController implements FxController {
*/
@Deprecated
private void requestLegacyAccessToken() {
- var legacyAccessTokenUri = API_BASE."vaults/\{vaultId}/keys/\{deviceId}";
+ var legacyAccessTokenUri = hubConfig.URIs.API."vaults/\{vaultId}/keys/\{deviceId}";
var request = HttpRequest.newBuilder(legacyAccessTokenUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
@@ -288,12 +287,6 @@ public class ReceiveKeyController implements FxController {
}
}
- private URI resolveRelativeToApiBase(StringTemplate template) {
- var path = template.interpolate();
- var relPath = path.startsWith("/") ? path.substring(1) : path;
- return hubConfig.getApiBaseUrl().resolve(relPath);
- }
-
private static String extractVaultId(URI vaultKeyUri) {
assert vaultKeyUri.getScheme().startsWith(SCHEME_PREFIX);
var path = vaultKeyUri.getPath();
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 08af2492a..b00d49874 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java
@@ -31,19 +31,25 @@ import javafx.scene.control.TextField;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
+import java.security.interfaces.ECPublicKey;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
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;
+import java.util.stream.Collectors;
@KeyLoadingScoped
public class RegisterDeviceController implements FxController {
@@ -108,9 +114,8 @@ public class RegisterDeviceController implements FxController {
public void register() {
workInProgress.set(true);
- var apiRootUrl = hubConfig.getApiBaseUrl();
- var userReq = HttpRequest.newBuilder(apiRootUrl.resolve("users/me")) //
+ var userReq = HttpRequest.newBuilder(hubConfig.URIs.API."users/me") //
.GET() //
.timeout(REQ_TIMEOUT) //
.header("Authorization", "Bearer " + bearerToken) //
@@ -126,17 +131,19 @@ public class RegisterDeviceController implements FxController {
}
}).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
+ assert user.privateKey != null && user.publicKey != null; // api/vaults/{v}/user-tokens/me would have returned 403, if user wasn't fully set up yet
+ var userPublicKey = JWEHelper.decodeECPublicKey(Base64.getDecoder().decode(user.publicKey));
+ migrateLegacyDevices(userPublicKey); // TODO: remove eventually, when most users have migrated to Hub 1.3.x or newer
var userKey = JWEHelper.decryptUserKey(JWEObject.parse(user.privateKey), setupCodeField.getText());
return JWEHelper.encryptUserKey(userKey, deviceKeyPair.getPublic());
- } catch (ParseException e) {
+ } catch (ParseException | JWEHelper.KeyDecodeFailedException 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 deviceUri = hubConfig.URIs.API."devices/\{deviceId}";
var putDeviceReq = HttpRequest.newBuilder(deviceUri) //
.PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
.timeout(REQ_TIMEOUT) //
@@ -154,6 +161,46 @@ public class RegisterDeviceController implements FxController {
}, Platform::runLater);
}
+ private void migrateLegacyDevices(ECPublicKey userPublicKey) {
+ try {
+ // GET legacy access tokens
+ var getUri = hubConfig.URIs.API."devices/\{deviceId}/legacy-access-tokens";
+ var getReq = HttpRequest.newBuilder(getUri).GET().timeout(REQ_TIMEOUT).header("Authorization", "Bearer " + bearerToken).build();
+ var getRes = httpClient.send(getReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (getRes.statusCode() != 200) {
+ LOG.debug("GET {} resulted in status code {}. Skipping migration.", getUri, getRes.statusCode());
+ return;
+ }
+ Map legacyAccessTokens = JSON.readerForMapOf(String.class).readValue(getRes.body());
+ if (legacyAccessTokens.isEmpty()) {
+ return; // no migration required
+ }
+
+ // POST new access tokens
+ Map newAccessTokens = legacyAccessTokens.entrySet().stream().>mapMulti((entry, consumer) -> {
+ try (var vaultKey = JWEHelper.decryptVaultKey(JWEObject.parse(entry.getValue()), deviceKeyPair.getPrivate())) {
+ var newAccessToken = JWEHelper.encryptVaultKey(vaultKey, userPublicKey).serialize();
+ consumer.accept(Map.entry(entry.getKey(), newAccessToken));
+ } catch (ParseException | JWEHelper.InvalidJweKeyException e) {
+ LOG.warn("Failed to decrypt legacy access token for vault {}. Skipping migration.", entry.getKey());
+ }
+ }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ var postUri = hubConfig.URIs.API."users/me/access-tokens";
+ var postBody = JSON.writer().writeValueAsString(newAccessTokens);
+ var postReq = HttpRequest.newBuilder(postUri).POST(HttpRequest.BodyPublishers.ofString(postBody)).timeout(REQ_TIMEOUT).header("Authorization", "Bearer " + bearerToken).build();
+ var postRes = httpClient.send(postReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (postRes.statusCode() != 200) {
+ throw new IOException(STR."Unexpected response from POST \{postUri}: \{postRes.statusCode()}");
+ }
+ } catch (IOException e) {
+ // log and ignore: this is merely a best-effort attempt of migrating legacy devices. Failure is uncritical as this is merely a convenience feature.
+ LOG.error("Legacy Device Migration failed.", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new UncheckedIOException(new InterruptedIOException("Legacy Device Migration interrupted"));
+ }
+ }
+
private UserDto fromJson(String json) {
try {
return JSON.reader().readValue(json, UserDto.class);
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 a2e46cc28..42147110a 100644
--- a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java
+++ b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java
@@ -1,10 +1,13 @@
package org.cryptomator.ui.keyloading.hub;
import com.nimbusds.jose.JWEObject;
+import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.P384KeyPair;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -29,6 +32,35 @@ public class JWEHelperTest {
private static final String PRIV_KEY = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ";
private static final String PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu";
+
+ @Nested
+ @DisplayName("DER decoding")
+ public class Decoders {
+
+ private static P384KeyPair keyPair;
+
+ @BeforeAll
+ public static void setup() throws InvalidKeySpecException {
+ keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PRIV_KEY)));
+ }
+
+ @Test
+ @DisplayName("decodeECPublicKey")
+ public void testDecodeECPublicKey() {
+ var decodedPublicKey = JWEHelper.decodeECPublicKey(Base64.getDecoder().decode(DEVICE_PUB_KEY));
+
+ Assertions.assertArrayEquals(keyPair.getPublic().getEncoded(), decodedPublicKey.getEncoded());
+ }
+
+ @Test
+ @DisplayName("decodeECPrivateKey")
+ public void testDecodeECPrivateKey() {
+ var decodedPrivateKey = JWEHelper.decodeECPrivateKey(Base64.getDecoder().decode(DEVICE_PRIV_KEY));
+
+ Assertions.assertArrayEquals(keyPair.getPrivate().getEncoded(), decodedPrivateKey.getEncoded());
+ }
+ }
+
@Test
@DisplayName("decryptUserKey with device key")
public void testDecryptUserKeyECDHES() throws ParseException, InvalidKeySpecException {
@@ -140,4 +172,31 @@ public class JWEHelperTest {
Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> JWEHelper.decryptVaultKey(jwe, privateKey));
}
+ @Test
+ @DisplayName("decrypt(encrypt(vaultKey, userPublicKey), userPrivateKey) == vaultKey")
+ public void testEncryptAndDecryptVaultKey() {
+ var keyBytes = new byte[64];
+ Arrays.fill(keyBytes, 0, 32, (byte) 0x55);
+ Arrays.fill(keyBytes, 32, 64, (byte) 0x77);
+ var vaultKey = new Masterkey(keyBytes);
+ var userKey = P384KeyPair.generate();
+
+ var encrypted = JWEHelper.encryptVaultKey(vaultKey, userKey.getPublic());
+ var decrypted = JWEHelper.decryptVaultKey(encrypted, userKey.getPrivate());
+
+ Assertions.assertArrayEquals(keyBytes, decrypted.getEncoded());
+ }
+
+ @Test
+ @DisplayName("decrypt(encrypt(userKey, devicePublicKey), devicePrivateKey) == userKey")
+ public void testEncryptAndDecryptUserKey() {
+ var userKey = P384KeyPair.generate();
+ var deviceKey = P384KeyPair.generate();
+
+ var encrypted = JWEHelper.encryptUserKey(userKey.getPrivate(), deviceKey.getPublic());
+ var decrypted = JWEHelper.decryptUserKey(encrypted, deviceKey.getPrivate());
+
+ Assertions.assertArrayEquals(userKey.getPrivate().getEncoded(), decrypted.getEncoded());
+ }
+
}
\ No newline at end of file