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

This commit is contained in:
Sebastian Stenzel
2021-07-28 17:29:44 +02:00
parent 2952733a11
commit b21ea61342
20 changed files with 598 additions and 7 deletions
+1
View File
@@ -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;
@@ -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");
}
@@ -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"), //
@@ -82,6 +82,10 @@ public class NiceSecurePasswordField extends StackPane {
return passwordField.textProperty();
}
public char[] copyChars() {
return passwordField.copyChars();
}
public CharSequence getCharacters() {
return passwordField.getCharacters();
}
@@ -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.
*
@@ -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
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
}