mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-22 20:51:27 +00:00
Merge pull request #3041 from cryptomator/feature/new-hub-keyloading
Adjusted to Hub 1.3.x API
This commit is contained in:
@@ -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"), //
|
||||
|
||||
@@ -35,13 +35,13 @@ public class AuthFlowController implements FxController {
|
||||
private final String deviceId;
|
||||
private final HubConfig hubConfig;
|
||||
private final AtomicReference<String> tokenRef;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final Lazy<Scene> receiveKeyScene;
|
||||
private final ObjectProperty<URI> authUri;
|
||||
private AuthFlowTask task;
|
||||
|
||||
@Inject
|
||||
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
|
||||
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
|
||||
this.application = application;
|
||||
this.window = window;
|
||||
this.executor = executor;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
record CreateDeviceDto(String id, String name, String publicKey) {
|
||||
|
||||
}
|
||||
@@ -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<InputStream> response) throws IOException {
|
||||
try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<JWEObject> provideResult() {
|
||||
static CompletableFuture<ReceivedKey> 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)
|
||||
|
||||
@@ -36,11 +36,11 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
private final KeychainManager keychainManager;
|
||||
private final Lazy<Scene> authFlowScene;
|
||||
private final Lazy<Scene> noKeychainScene;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final DeviceKey deviceKey;
|
||||
|
||||
@Inject
|
||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<JWEObject> result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) {
|
||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<ReceivedKey> 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);
|
||||
|
||||
@@ -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> T readKey(JWEObject jwe, String keyField, Function<byte[], T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Integer> EXPECTED_RESPONSE_CODES = List.of(201, 409);
|
||||
|
||||
private final Stage window;
|
||||
private final HubConfig hubConfig;
|
||||
private final String bearerToken;
|
||||
private final Lazy<Scene> registerSuccessScene;
|
||||
private final Lazy<Scene> registerFailedScene;
|
||||
private final String deviceId;
|
||||
private final P384KeyPair keyPair;
|
||||
private final CompletableFuture<ReceivedKey> 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<ReceivedKey> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> 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<Void> 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<JWEObject> result;
|
||||
private final Lazy<Scene> registerDeviceScene;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final Lazy<Scene> setupDeviceScene;
|
||||
private final Lazy<Scene> legacyRegisterDeviceScene;
|
||||
private final Lazy<Scene> unauthorizedScene;
|
||||
private final URI vaultBaseUri;
|
||||
private final Lazy<Scene> invalidLicenseScene;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
@Inject
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy<Scene> invalidLicenseScene) {
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_SETUP_DEVICE) Lazy<Scene> setupDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy<Scene> legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy<Scene> 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<InputStream> response) {
|
||||
/**
|
||||
* STEP 1 (Response): GET vault key for this user
|
||||
*
|
||||
* @param response Response
|
||||
*/
|
||||
private void receivedVaultMasterkey(HttpResponse<String> 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<String> 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<String> 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<InputStream> 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) {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Integer> 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<Scene> registerSuccessScene;
|
||||
private final Lazy<Scene> registerFailedScene;
|
||||
private final String deviceId;
|
||||
private final P384KeyPair keyPair;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final DecodedJWT jwt;
|
||||
private final P384KeyPair deviceKeyPair;
|
||||
private final CompletableFuture<ReceivedKey> 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<JWEObject> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> registerFailedScene) {
|
||||
public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture<ReceivedKey> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> 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<Void> 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<Void> 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) {}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import java.util.concurrent.CompletableFuture;
|
||||
public class RegisterFailedController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
|
||||
@Inject
|
||||
public RegisterFailedController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
|
||||
public RegisterFailedController(@KeyLoading Stage window, CompletableFuture<ReceivedKey> result) {
|
||||
this.window = window;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import java.util.concurrent.CompletableFuture;
|
||||
public class UnauthorizedDeviceController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
|
||||
@Inject
|
||||
public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
|
||||
public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture<ReceivedKey> result) {
|
||||
this.window = window;
|
||||
this.result = result;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.RegisterDeviceController"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.LegacyRegisterDeviceController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
92
src/main/resources/fxml/hub_setup_device.fxml
Normal file
92
src/main/resources/fxml/hub_setup_device.fxml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.Group?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.RegisterDeviceController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
spacing="12"
|
||||
alignment="TOP_LEFT">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<Group>
|
||||
<StackPane>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="6"/>
|
||||
</padding>
|
||||
<Circle styleClass="glyph-icon-primary" radius="24"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="INFO" glyphSize="24"/>
|
||||
</StackPane>
|
||||
</Group>
|
||||
|
||||
<VBox HBox.hgrow="ALWAYS">
|
||||
<Label styleClass="label-large" text="%hub.register.message" wrapText="true" textAlignment="LEFT">
|
||||
<padding>
|
||||
<Insets bottom="6" top="6"/>
|
||||
</padding>
|
||||
</Label>
|
||||
<Label text="%hub.register.description" wrapText="true"/>
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<padding>
|
||||
<Insets top="12"/>
|
||||
</padding>
|
||||
<Label text="%hub.register.setupCodeLabel" labelFor="$setupCodeField"/>
|
||||
<TextField fx:id="setupCodeField" HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<padding>
|
||||
<Insets top="12"/>
|
||||
</padding>
|
||||
<Label text="%hub.register.nameLabel" labelFor="$deviceNameField"/>
|
||||
<TextField fx:id="deviceNameField" HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
<HBox alignment="TOP_RIGHT">
|
||||
<Label text="%hub.register.occupiedMsg" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${controller.deviceNameAlreadyExists}" managed="${controller.deviceNameAlreadyExists}" graphicTextGap="6">
|
||||
<padding>
|
||||
<Insets top="6"/>
|
||||
</padding>
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
|
||||
<Label text="%hub.register.invalidSetupCode" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${controller.invalidSetupCode}" managed="${controller.invalidSetupCode}" graphicTextGap="6">
|
||||
<padding>
|
||||
<Insets top="6"/>
|
||||
</padding>
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
</HBox>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CU">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
|
||||
<Button fx:id="registerBtn" text="%hub.register.registerBtn" ButtonBar.buttonData="OTHER" defaultButton="true" onAction="#register" contentDisplay="TEXT_ONLY" >
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12" />
|
||||
</graphic>
|
||||
</Button>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
</children>
|
||||
</HBox>
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user