Browse Source

add first draft for `hub+http` / `hub+https` keyloading scheme

Sebastian Stenzel 3 years ago
parent
commit
b21ea61342

+ 1 - 1
.idea/runConfigurations/Cryptomator_Linux.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
+    <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

File diff suppressed because it is too large
+ 1 - 1
.idea/runConfigurations/Cryptomator_Linux_Dev.xml


File diff suppressed because it is too large
+ 1 - 1
.idea/runConfigurations/Cryptomator_Windows.xml


File diff suppressed because it is too large
+ 1 - 1
.idea/runConfigurations/Cryptomator_Windows_Dev.xml


+ 1 - 1
.idea/runConfigurations/Cryptomator_macOS.xml

@@ -5,7 +5,7 @@
     </envs>
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
+    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

+ 1 - 1
.idea/runConfigurations/Cryptomator_macOS_Dev.xml

@@ -5,7 +5,7 @@
     </envs>
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
+    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

+ 1 - 0
src/main/java/module-info.java

@@ -47,6 +47,7 @@ module org.cryptomator.desktop {
 	opens org.cryptomator.ui.forgetPassword to javafx.fxml;
 	opens org.cryptomator.ui.fxapp to javafx.fxml;
 	opens org.cryptomator.ui.health to javafx.fxml;
+	opens org.cryptomator.ui.keyloading.hub to javafx.fxml;
 	opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
 	opens org.cryptomator.ui.mainwindow to javafx.fxml;
 	opens org.cryptomator.ui.migration to javafx.fxml;

+ 5 - 0
src/main/java/org/cryptomator/common/Environment.java

@@ -33,6 +33,7 @@ public class Environment {
 		LOG.debug("user.region: {}", System.getProperty("user.region"));
 		LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
 		LOG.debug("cryptomator.settingsPath: {}", System.getProperty("cryptomator.settingsPath"));
+		LOG.debug("cryptomator.p12Path: {}", System.getProperty("cryptomator.p12Path"));
 		LOG.debug("cryptomator.ipcSocketPath: {}", System.getProperty("cryptomator.ipcSocketPath"));
 		LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
 		LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
@@ -51,6 +52,10 @@ public class Environment {
 		return getPaths("cryptomator.settingsPath");
 	}
 
+	public Stream<Path> getP12Path() {
+		return getPaths("cryptomator.p12Path");
+	}
+
 	public Stream<Path> ipcSocketPath() {
 		return getPaths("cryptomator.ipcSocketPath");
 	}

+ 1 - 0
src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -14,6 +14,7 @@ public enum FxmlFile {
 	HEALTH_START("/fxml/health_start.fxml"), //
 	HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), //
 	HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
+	HUB_P12("/fxml/hub_p12.fxml"), //
 	LOCK_FORCED("/fxml/lock_forced.fxml"), //
 	LOCK_FAILED("/fxml/lock_failed.fxml"), //
 	MAIN_WINDOW("/fxml/main_window.fxml"), //

+ 4 - 0
src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java

@@ -82,6 +82,10 @@ public class NiceSecurePasswordField extends StackPane {
 		return passwordField.textProperty();
 	}
 
+	public char[] copyChars() {
+		return passwordField.copyChars();
+	}
+
 	public CharSequence getCharacters() {
 		return passwordField.getCharacters();
 	}

+ 9 - 0
src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java

@@ -194,6 +194,15 @@ public class SecurePasswordField extends TextField {
 		}
 	}
 
+	/**
+	 * Retrieves a copy of the password characters. This copy needs to be wiped by the caller when done.
+	 *
+	 * @return A copy of the password
+	 */
+	public char[] copyChars() {
+		return Arrays.copyOf(content, length);
+	}
+
 	/**
 	 * Creates a CharSequence by wrapping the password characters.
 	 *

+ 2 - 1
src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java

@@ -7,6 +7,7 @@ import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
 import org.cryptomator.ui.common.DefaultSceneFactory;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlLoaderFactory;
+import org.cryptomator.ui.keyloading.hub.HubKeyLoadingModule;
 import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
 
 import javax.inject.Provider;
@@ -16,7 +17,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.ResourceBundle;
 
-@Module(includes = {MasterkeyFileLoadingModule.class})
+@Module(includes = {MasterkeyFileLoadingModule.class, HubKeyLoadingModule.class})
 abstract class KeyLoadingModule {
 
 	@Provides

+ 87 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java

@@ -0,0 +1,87 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoMap;
+import dagger.multibindings.StringKey;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxControllerKey;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlLoaderFactory;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.NewPasswordController;
+import org.cryptomator.ui.common.PasswordStrengthUtil;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.keyloading.KeyLoading;
+import org.cryptomator.ui.keyloading.KeyLoadingScoped;
+import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
+
+import javafx.scene.Scene;
+import java.security.KeyPair;
+import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Module
+public abstract class HubKeyLoadingModule {
+
+	public enum P12KeyLoading {
+		LOADED,
+		CREATED,
+		CANCELED
+	}
+
+	@Provides
+	@KeyLoadingScoped
+	static AtomicReference<KeyPair> provideKeyPair() {
+		return new AtomicReference<>();
+	}
+
+	@Provides
+	@KeyLoadingScoped
+	static UserInteractionLock<P12KeyLoading> provideP12KeyLoadingLock() {
+		return new UserInteractionLock<>(null);
+	}
+
+	@Binds
+	@IntoMap
+	@KeyLoadingScoped
+	@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTP)
+	abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttp(HubKeyLoadingStrategy strategy);
+
+	@Binds
+	@IntoMap
+	@KeyLoadingScoped
+	@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS)
+	abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttps(HubKeyLoadingStrategy strategy);
+
+	@Provides
+	@FxmlScene(FxmlFile.HUB_P12)
+	@KeyLoadingScoped
+	static Scene provideHubP12LoadingScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene(FxmlFile.HUB_P12);
+	}
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(P12Controller.class)
+	abstract FxController bindP12Controller(P12Controller controller);
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(P12LoadController.class)
+	abstract FxController bindP12LoadController(P12LoadController controller);
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(P12CreateController.class)
+	abstract FxController bindP12CreateController(P12CreateController controller);
+
+	@Provides
+	@IntoMap
+	@FxControllerKey(NewPasswordController.class)
+	static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
+		return new NewPasswordController(resourceBundle, strengthRater);
+	}
+
+}

+ 88 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java

@@ -0,0 +1,88 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import dagger.Lazy;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.keyloading.KeyLoading;
+import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
+import org.cryptomator.ui.unlock.UnlockCancelledException;
+
+import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyPair;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoading
+public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
+
+	static final String SCHEME_HUB_HTTP = "hub+http";
+	static final String SCHEME_HUB_HTTPS = "hub+https";
+	private static final String SCHEME_HTTP = "http";
+	private static final String SCHEME_HTTPS = "https";
+
+	private final Vault vault;
+	private final Stage window;
+	private final Lazy<Scene> p12LoadingScene;
+	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
+	private final AtomicReference<KeyPair> keyPairRef;
+
+	@Inject
+	public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock, AtomicReference<KeyPair> keyPairRef) {
+		this.vault = vault;
+		this.window = window;
+		this.p12LoadingScene = p12LoadingScene;
+		this.p12LoadingLock = p12LoadingLock;
+		this.keyPairRef = keyPairRef;
+	}
+
+	@Override
+	public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
+		return switch (keyId.getScheme().toLowerCase()) {
+			case SCHEME_HUB_HTTP -> loadKey(keyId, SCHEME_HTTP);
+			case SCHEME_HUB_HTTPS -> loadKey(keyId, SCHEME_HTTPS);
+			default -> throw new IllegalArgumentException("Only supports keys with schemes " + SCHEME_HUB_HTTP + " or " + SCHEME_HUB_HTTPS);
+		};
+	}
+
+	private Masterkey loadKey(URI keyId, String adjustedScheme) {
+		try {
+			var foo = new URI(adjustedScheme, keyId.getSchemeSpecificPart(), keyId.getFragment());
+		} catch (URISyntaxException e) {
+			throw new IllegalStateException("URI known to be valid, if old URI was valid", e);
+		}
+
+		try {
+			loadP12();
+			LOG.info("keypair loaded {}", keyPairRef.get().getPublic());
+			throw new UnlockCancelledException("not yet implemented"); // TODO
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			throw new UnlockCancelledException("Loading interrupted", e);
+		}
+	}
+
+	private HubKeyLoadingModule.P12KeyLoading loadP12() throws InterruptedException {
+		Platform.runLater(() -> {
+			window.setScene(p12LoadingScene.get());
+			window.show();
+			Window owner = window.getOwner();
+			if (owner != null) {
+				window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
+				window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
+			} else {
+				window.centerOnScreen();
+			}
+		});
+		return p12LoadingLock.awaitInteraction();
+	}
+
+}

+ 65 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java

@@ -0,0 +1,65 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.base.Preconditions;
+import org.cryptomator.common.Environment;
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.common.Destroyables;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.NewPasswordController;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.controls.NiceSecurePasswordField;
+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 javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanExpression;
+import javafx.beans.binding.ObjectExpression;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoadingScoped
+public class P12Controller implements FxController {
+
+	private static final Logger LOG = LoggerFactory.getLogger(P12Controller.class);
+
+	private final Stage window;
+	private final Environment env;
+	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
+
+	@Inject
+	public P12Controller(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock) {
+		this.window = window;
+		this.env = env;
+		this.p12LoadingLock = p12LoadingLock;
+		this.window.setOnHiding(this::windowClosed);
+	}
+
+	private void windowClosed(WindowEvent windowEvent) {
+		// if not already interacted, mark this workflow as cancelled:
+		if (p12LoadingLock.awaitingInteraction().get()) {
+			LOG.debug("P12 loading canceled by user.");
+			p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.CANCELED);
+		}
+	}
+
+	/* Getter/Setter */
+
+	public boolean isP12Present() {
+		return env.getP12Path().anyMatch(Files::isRegularFile);
+	}
+
+}

+ 118 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java

@@ -0,0 +1,118 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.base.Preconditions;
+import org.cryptomator.common.Environment;
+import org.cryptomator.cryptolib.common.Destroyables;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.NewPasswordController;
+import org.cryptomator.ui.common.UserInteractionLock;
+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 javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanExpression;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.binding.ObjectExpression;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.ContentDisplay;
+import javafx.stage.Stage;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoadingScoped
+public class P12CreateController implements FxController  {
+
+	private static final Logger LOG = LoggerFactory.getLogger(P12LoadController.class);
+
+	private final Stage window;
+	private final Environment env;
+	private final AtomicReference<KeyPair> keyPairRef;
+	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
+	private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
+	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
+	private final BooleanProperty readyToCreate = new SimpleBooleanProperty();
+
+	public NewPasswordController newPasswordController;
+
+
+	@Inject
+	public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock) {
+		this.window = window;
+		this.env = env;
+		this.keyPairRef = keyPairRef;
+		this.p12LoadingLock = p12LoadingLock;
+	}
+
+	@FXML
+	public void initialize() {
+		readyToCreate.bind(newPasswordController.goodPasswordProperty());
+	}
+
+	@FXML
+	public void cancel() {
+		window.close();
+	}
+
+	@FXML
+	public void create() {
+		Preconditions.checkState(newPasswordController.goodPasswordProperty().get());
+		char[] pw = newPasswordController.passwordField.copyChars();
+		try {
+			Path p12File = env.getP12Path().findFirst().orElseThrow(IllegalStateException::new);
+			var keyPair = P12AccessHelper.createNew(p12File, pw);
+			setKeyPair(keyPair);
+			LOG.debug("Created .p12 file {}", p12File);
+			p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.CREATED);
+			window.close();
+		} catch (IOException e) {
+			LOG.error("Failed to load .p12 file.", e);
+			// TODO
+		} finally {
+			Arrays.fill(pw, '\0');
+			newPasswordController.passwordField.wipe();
+			newPasswordController.reenterField.wipe();
+		}
+	}
+
+	private void setKeyPair(KeyPair keyPair) {
+		var oldKeyPair = keyPairRef.getAndSet(keyPair);
+		if (oldKeyPair != null) {
+			Destroyables.destroySilently(oldKeyPair.getPrivate());
+		}
+	}
+	/* Getter/Setter */
+
+
+	public BooleanExpression userInteractionDisabledProperty() {
+		return userInteractionDisabled;
+	}
+
+	public boolean isUserInteractionDisabled() {
+		return userInteractionDisabled.get();
+	}
+
+	public ObjectExpression<ContentDisplay> unlockButtonContentDisplayProperty() {
+		return unlockButtonContentDisplay;
+	}
+
+	public ContentDisplay getUnlockButtonContentDisplay() {
+		return userInteractionDisabled.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
+	}
+
+	public BooleanProperty readyToCreateProperty() {
+		return readyToCreate;
+	}
+
+	public boolean isReadyToCreate() {
+		return readyToCreate.get();
+	}
+
+}

+ 106 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java

@@ -0,0 +1,106 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.cryptomator.common.Environment;
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.common.Destroyables;
+import org.cryptomator.ui.common.Animations;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.controls.NiceSecurePasswordField;
+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 javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanExpression;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.binding.ObjectExpression;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.ContentDisplay;
+import javafx.stage.Stage;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoadingScoped
+public class P12LoadController implements FxController {
+
+	private static final Logger LOG = LoggerFactory.getLogger(P12LoadController.class);
+
+	private final Stage window;
+	private final Environment env;
+	private final AtomicReference<KeyPair> keyPairRef;
+	private final UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock;
+	private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
+	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
+
+	public NiceSecurePasswordField passwordField;
+
+	@Inject
+	public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock) {
+		this.window = window;
+		this.env = env;
+		this.keyPairRef = keyPairRef;
+		this.p12LoadingLock = p12LoadingLock;
+	}
+
+	@FXML
+	public void cancel() {
+		window.close();
+	}
+
+	@FXML
+	public void load() {
+		char[] pw = passwordField.copyChars();
+		try {
+			Path p12File = env.getP12Path().filter(Files::isRegularFile).findFirst().orElseThrow(IllegalStateException::new);
+			var keyPair = P12AccessHelper.loadExisting(p12File, pw);
+			setKeyPair(keyPair);
+			LOG.debug("Loaded .p12 file {}", p12File);
+			p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.LOADED);
+			window.close();
+		} catch (InvalidPassphraseException e) {
+			LOG.warn("Invalid passphrase entered for .p12 file");
+			Animations.createShakeWindowAnimation(window).playFromStart();
+			// TODO
+		} catch (IOException e) {
+			LOG.error("Failed to load .p12 file.", e);
+			// TODO
+		} finally {
+			Arrays.fill(pw, '\0');
+			passwordField.wipe();
+		}
+	}
+
+	private void setKeyPair(KeyPair keyPair) {
+		var oldKeyPair = keyPairRef.getAndSet(keyPair);
+		if (oldKeyPair != null) {
+			Destroyables.destroySilently(oldKeyPair.getPrivate());
+		}
+	}
+
+	/* Getter/Setter */
+
+	public BooleanExpression userInteractionDisabledProperty() {
+		return userInteractionDisabled;
+	}
+
+	public boolean isUserInteractionDisabled() {
+		return userInteractionDisabled.get();
+	}
+
+	public ObjectExpression<ContentDisplay> unlockButtonContentDisplayProperty() {
+		return unlockButtonContentDisplay;
+	}
+
+	public ContentDisplay getUnlockButtonContentDisplay() {
+		return userInteractionDisabled.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
+	}
+}

+ 19 - 0
src/main/resources/fxml/hub_p12.fxml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.layout.VBox?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.keyloading.hub.P12Controller"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<fx:include source="hub_p12_load.fxml" visible="${controller.p12Present}" managed="${controller.p12Present}"/>
+		<fx:include source="hub_p12_create.fxml" visible="${!controller.p12Present}" managed="${!controller.p12Present}"/>
+	</children>
+</VBox>

+ 39 - 0
src/main/resources/fxml/hub_p12_create.fxml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.image.Image?>
+<?import javafx.scene.image.ImageView?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.keyloading.hub.P12CreateController"
+	  spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<HBox spacing="12" VBox.vgrow="ALWAYS">
+			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" visible="false">
+				<Image url="@../img/bot/bot.png"/>
+			</ImageView>
+			<fx:include fx:id="newPassword" source="new_password.fxml" disable="${controller.userInteractionDisabled}"/>
+		</HBox>
+
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+CO">
+				<buttons>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
+					<Button text="%generic.button.next" ButtonBar.buttonData="OK_DONE" defaultButton="true" onAction="#create" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${!controller.readyToCreate || controller.userInteractionDisabled}">
+						<graphic>
+							<FontAwesome5Spinner glyphSize="12"/>
+						</graphic>
+					</Button>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</VBox>

+ 48 - 0
src/main/resources/fxml/hub_p12_load.fxml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
+<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.CheckBox?>
+<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.image.Image?>
+<?import javafx.scene.image.ImageView?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.keyloading.hub.P12LoadController"
+	  spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<HBox spacing="12" VBox.vgrow="ALWAYS">
+			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" visible="false">
+				<Image url="@../img/bot/bot.png"/>
+			</ImageView>
+			<VBox spacing="6" HBox.hgrow="ALWAYS">
+				<Label text="TODO: Please enter your device secret to start communicating with Cryptomator Hub"/>
+				<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
+				<CheckBox fx:id="savePasswordCheckbox" text="TODO save password" disable="${controller.userInteractionDisabled}"/>
+				<Hyperlink text="TODO: Click to reset your device secret (you need to re-apply for vault access)"/>
+			</VBox>
+		</HBox>
+
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+CO">
+				<buttons>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
+					<Button text="%generic.button.next" ButtonBar.buttonData="OK_DONE" defaultButton="true" onAction="#load" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${controller.userInteractionDisabled}">
+						<graphic>
+							<FontAwesome5Spinner glyphSize="12"/>
+						</graphic>
+					</Button>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</VBox>