> strategies) {
if (keyId.isEmpty()) {
return KeyLoadingStrategy.failed(new IllegalArgumentException("No key id provided"));
} else {
String scheme = keyId.get().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
- return strategies.getOrDefault(scheme, fallback);
+ return strategies.getOrDefault(scheme, () -> fallback).get();
}
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java
index 80f65b6bc..ed8ca0540 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java
@@ -1,21 +1,34 @@
package org.cryptomator.ui.keyloading;
+import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
-public interface KeyLoadingStrategy {
+import java.net.URI;
+
+/**
+ * A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions.
+ */
+@FunctionalInterface
+public interface KeyLoadingStrategy extends MasterkeyLoader {
/**
- * @return A reusable masterkey loader, preconfigured with the vault of the current unlock process
- * @throws MasterkeyLoadingFailedException If unable to provide the masterkey loader
- */
- MasterkeyLoader masterkeyLoader() throws MasterkeyLoadingFailedException;
-
- /**
- * Allows the component to try and recover from an exception thrown while loading a masterkey.
+ * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
+ *
+ * If loading fails exceptionally, this strategy might be able to {@link #recoverFromException(MasterkeyLoadingFailedException) recover from this exception}, so it can be used in a further attempt.
*
- * @param exception An exception thrown either by {@link #masterkeyLoader()} or by the returned {@link MasterkeyLoader}.
- * @return true if this component was able to handle the exception and another attempt should be made to load a masterkey
+ * @param keyId An URI uniquely identifying the source and identity of the key
+ * @return The raw key bytes. Must not be null
+ * @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
+ */
+ @Override
+ Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
+
+ /**
+ * Allows the loader to try and recover from an exception thrown during the last attempt.
+ *
+ * @param exception An exception thrown by {@link #loadKey(URI)}.
+ * @return true if this component was able to handle the exception and another attempt can be made to load a masterkey
*/
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
return false;
@@ -38,7 +51,7 @@ public interface KeyLoadingStrategy {
* @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}.
*/
static KeyLoadingStrategy failed(Exception exception) {
- return () -> {
+ return keyid -> {
if (exception instanceof MasterkeyLoadingFailedException e) {
throw e;
} else {
diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingContext.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingContext.java
deleted file mode 100644
index 7445d581d..000000000
--- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingContext.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package org.cryptomator.ui.keyloading.masterkeyfile;
-
-import dagger.Lazy;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
-import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext;
-import org.cryptomator.ui.common.Animations;
-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.KeyLoadingScoped;
-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.nio.CharBuffer;
-import java.nio.file.Path;
-import java.util.concurrent.atomic.AtomicReference;
-
-@KeyLoadingScoped
-class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
-
- private final Stage window;
- private final Lazy passphraseEntryScene;
- private final Lazy selectMasterkeyFileScene;
- private final UserInteractionLock passwordEntryLock;
- private final UserInteractionLock masterkeyFileProvisionLock;
- private final AtomicReference password;
- private final AtomicReference filePath;
-
- private boolean wrongPassword;
-
- @Inject
- public MasterkeyFileLoadingContext(@KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference filePath) {
- this.window = window;
- this.passphraseEntryScene = passphraseEntryScene;
- this.selectMasterkeyFileScene = selectMasterkeyFileScene;
- this.passwordEntryLock = passwordEntryLock;
- this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
- this.password = password;
- this.filePath = filePath;
- }
-
- @Override
- public Path getCorrectMasterkeyFilePath(String masterkeyFilePath) {
- if (filePath.get() != null) { // e.g. already chosen on previous attempt with wrong password
- return filePath.get();
- }
-
- assert filePath.get() == null;
- try {
- if (askForCorrectMasterkeyFile() == MasterkeyFileLoadingModule.MasterkeyFileProvision.MASTERKEYFILE_PROVIDED) {
- return filePath.get();
- } else {
- throw new UnlockCancelledException("Choosing masterkey file cancelled.");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new UnlockCancelledException("Choosing masterkey file interrupted", e);
- }
- }
-
- private MasterkeyFileLoadingModule.MasterkeyFileProvision askForCorrectMasterkeyFile() throws InterruptedException {
- Platform.runLater(() -> {
- window.setScene(selectMasterkeyFileScene.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 masterkeyFileProvisionLock.awaitInteraction();
- }
-
- @Override
- public CharSequence getPassphrase(Path path) throws UnlockCancelledException {
- if (password.get() != null) { // e.g. pre-filled from keychain
- return CharBuffer.wrap(password.get());
- }
-
- assert password.get() == null;
- try {
- if (askForPassphrase() == MasterkeyFileLoadingModule.PasswordEntry.PASSWORD_ENTERED) {
- assert password.get() != null;
- return CharBuffer.wrap(password.get());
- } else {
- throw new UnlockCancelledException("Password entry cancelled.");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new UnlockCancelledException("Password entry interrupted", e);
- }
- }
-
- private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
- Platform.runLater(() -> {
- window.setScene(passphraseEntryScene.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();
- }
- if (wrongPassword) {
- Animations.createShakeWindowAnimation(window).play();
- }
- });
- return passwordEntryLock.awaitInteraction();
- }
-
- public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
- if (exception instanceof InvalidPassphraseException) {
- this.wrongPassword = true;
- password.set(null);
- return true; // reattempting key load
- } else {
- return false; // nothing we can do
- }
- }
-}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java
index 31b123f53..d9413121c 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java
@@ -7,8 +7,6 @@ import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
-import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -45,12 +43,6 @@ public abstract class MasterkeyFileLoadingModule {
CANCELED
}
- @Provides
- @KeyLoadingScoped
- static MasterkeyFileLoader provideMasterkeyFileLoader(MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Vault vault, MasterkeyFileLoadingContext context) {
- return masterkeyFileAccess.keyLoader(vault.getPath(), context);
- }
-
@Provides
@KeyLoadingScoped
static UserInteractionLock providePasswordEntryLock() {
@@ -125,7 +117,7 @@ public abstract class MasterkeyFileLoadingModule {
@Binds
@IntoMap
@KeyLoadingScoped
- @StringKey("masterkeyfile")
+ @StringKey(MasterkeyFileLoadingStrategy.SCHEME)
abstract KeyLoadingStrategy bindMasterkeyFileLoadingStrategy(MasterkeyFileLoadingStrategy strategy);
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
index ae6694198..464671929 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
@@ -1,38 +1,150 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
+import com.google.common.base.Preconditions;
+import dagger.Lazy;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
-import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.cryptomator.ui.common.Animations;
+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.nio.CharBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
@KeyLoading
-class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
+public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
- private final MasterkeyFileLoader masterkeyFileLoader;
- private final MasterkeyFileLoadingContext context;
+ public static final String SCHEME = "masterkeyfile";
+
+ private final Vault vault;
+ private final MasterkeyFileAccess masterkeyFileAcccess;
+ private final Stage window;
+ private final Lazy passphraseEntryScene;
+ private final Lazy selectMasterkeyFileScene;
+ private final UserInteractionLock passwordEntryLock;
+ private final UserInteractionLock masterkeyFileProvisionLock;
+ private final AtomicReference password;
+ private final AtomicReference filePath;
private final MasterkeyFileLoadingFinisher finisher;
+ private boolean wrongPassword;
+
@Inject
- public MasterkeyFileLoadingStrategy(MasterkeyFileLoader masterkeyFileLoader, MasterkeyFileLoadingContext context, MasterkeyFileLoadingFinisher finisher) {
- this.masterkeyFileLoader = masterkeyFileLoader;
- this.context = context;
+ public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAcccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference filePath, MasterkeyFileLoadingFinisher finisher) {
+ this.vault = vault;
+ this.masterkeyFileAcccess = masterkeyFileAcccess;
+ this.window = window;
+ this.passphraseEntryScene = passphraseEntryScene;
+ this.selectMasterkeyFileScene = selectMasterkeyFileScene;
+ this.passwordEntryLock = passwordEntryLock;
+ this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
+ this.password = password;
+ this.filePath = filePath;
this.finisher = finisher;
}
@Override
- public MasterkeyFileLoader masterkeyLoader() {
- return masterkeyFileLoader;
+ public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
+ Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME);
+
+ try {
+ Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
+ if (!Files.exists(filePath)) {
+ filePath = getAlternateMasterkeyFilePath();
+ }
+ CharSequence passphrase = getPassphrase();
+ return masterkeyFileAcccess.load(filePath, passphrase);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new UnlockCancelledException("Unlock interrupted", e);
+ }
}
@Override
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
- return context.recoverFromException(exception);
+ if (exception instanceof InvalidPassphraseException) {
+ this.wrongPassword = true;
+ password.set(null);
+ return true; // reattempting key load
+ } else {
+ return false; // nothing we can do
+ }
}
@Override
public void cleanup(boolean unlockedSuccessfully) {
finisher.cleanup(unlockedSuccessfully);
}
+
+ private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
+ if (filePath == null) {
+ return switch (askUserForMasterkeyFilePath()) {
+ case MASTERKEYFILE_PROVIDED -> filePath.get();
+ case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
+ };
+ } else {
+ return filePath.get();
+ }
+ }
+
+ private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
+ Platform.runLater(() -> {
+ window.setScene(selectMasterkeyFileScene.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 masterkeyFileProvisionLock.awaitInteraction();
+ }
+
+ private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException {
+ if (password.get() == null) {
+ return switch (askForPassphrase()) {
+ case PASSWORD_ENTERED -> CharBuffer.wrap(password.get());
+ case CANCELED -> throw new UnlockCancelledException("Password entry cancelled.");
+ };
+ } else {
+ // e.g. pre-filled from keychain or previous unlock attempt
+ return CharBuffer.wrap(password.get());
+ }
+ }
+
+ private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
+ Platform.runLater(() -> {
+ window.setScene(passphraseEntryScene.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();
+ }
+ if (wrongPassword) {
+ Animations.createShakeWindowAnimation(window).play();
+ }
+ });
+ return passwordEntryLock.awaitInteraction();
+ }
+
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
index 28ac1ab0b..36c3eacf9 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
@@ -70,7 +70,7 @@ public class UnlockWorkflow extends Task {
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
boolean success = false;
try {
- vault.unlock(keyLoadingStrategy.masterkeyLoader());
+ vault.unlock(keyLoadingStrategy);
success = true;
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {