diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
index 01a98b832..480b0b2c4 100644
--- a/.github/workflows/appimage.yml
+++ b/.github/workflows/appimage.yml
@@ -149,6 +149,6 @@ jobs:
fail_on_unmatched_files: true
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
files: |
- *.AppImage
- *.zsync
- *.asc
\ No newline at end of file
+ cryptomator-*.AppImage
+ cryptomator-*.zsync
+ cryptomator-*.asc
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 40d2b2e1b..97d46ae9e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,6 +32,7 @@ jobs:
restore-keys: ${{ runner.os }}-sonar
- name: Build and Test
run: >
+ xvfb-run
mvn -B verify
jacoco:report
org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml
index 58b7e9408..66af92d6d 100644
--- a/.github/workflows/mac-dmg.yml
+++ b/.github/workflows/mac-dmg.yml
@@ -226,7 +226,7 @@ jobs:
fail_on_unmatched_files: true
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
files: |
- *.dmg
- *.asc
+ Cryptomator-*.dmg
+ Cryptomator-*.asc
diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml
index 9a855b646..25a0575fb 100644
--- a/.github/workflows/win-exe.yml
+++ b/.github/workflows/win-exe.yml
@@ -217,7 +217,26 @@ jobs:
run: >
"${WIX}/bin/light.exe" -b dist/win/ dist/win/bundle/bundleWithWinfsp.wixobj
-ext WixBalExtension
- -out installer/Cryptomator.exe
+ -out installer/unsigned/Cryptomator.exe
+ - name: Detach burn engine in preparation to sign
+ run: >
+ "${WIX}/bin/insignia.exe"
+ -ib installer/unsigned/Cryptomator.exe
+ -o tmp/engine.exe
+ - name: Codesign burn engine
+ uses: skymatic/code-sign-action@v1
+ with:
+ certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
+ password: ${{ secrets.WIN_CODESIGN_P12_PW }}
+ certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B
+ description: Cryptomator Installer
+ timestampUrl: 'http://timestamp.digicert.com'
+ folder: tmp
+ - name: Reattach signed burn engine to installer
+ run : >
+ "${WIX}/bin/insignia.exe"
+ -ab tmp/engine.exe installer/unsigned/Cryptomator.exe
+ -o installer/Cryptomator.exe
- name: Codesign EXE
uses: skymatic/code-sign-action@v1
with:
@@ -251,5 +270,5 @@ jobs:
fail_on_unmatched_files: true
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
files: |
- *.exe
- *.asc
\ No newline at end of file
+ Cryptomator-*.exe
+ Cryptomator-*.asc
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 8e239b35e..5c84c0dfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,7 +21,6 @@ pom.xml.versionsBackup
.idea/dictionaries/**
!.idea/dictionaries/dict_*
.idea/compiler.xml
-.idea/encodings.xml
.idea/jarRepositories.xml
.idea/uiDesigner.xml
.idea/**/libraries/
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 000000000..63574ec0a
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dist/mac/dmg/.gitignore b/dist/mac/dmg/.gitignore
index c186170c9..b8ef35283 100644
--- a/dist/mac/dmg/.gitignore
+++ b/dist/mac/dmg/.gitignore
@@ -1,4 +1,5 @@
# created during build
+Cryptomator.app/
runtime/
dmg/
-*.dmg
+*.dmg
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 469ab6398..880c07f73 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,23 +34,23 @@
1.0.1
1.3.3
1.3.3
- 1.2.6
+ 1.2.7
- 17.0.2
+ 18
3.12.0
- 3.18.3
+ 3.19.0
2.2
- 31.0-jre
- 2.40.3
- 2.8.9
+ 31.1-jre
+ 2.41
+ 2.9.0
1.5.2
- 1.7.32
- 1.2.9
+ 1.7.36
+ 1.2.11
5.8.1
- 3.12.4
+ 4.4.0
2.2
diff --git a/src/main/java/org/cryptomator/common/Passphrase.java b/src/main/java/org/cryptomator/common/Passphrase.java
new file mode 100644
index 000000000..cf64c7a10
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/Passphrase.java
@@ -0,0 +1,108 @@
+package org.cryptomator.common;
+
+import javax.security.auth.Destroyable;
+import java.util.Arrays;
+
+/**
+ * A destroyable CharSequence.
+ */
+public class Passphrase implements Destroyable, CharSequence {
+
+ private final char[] data;
+ private final int offset;
+ private final int length;
+ private boolean destroyed;
+
+ /**
+ * Wraps (doesn't copy) the given data.
+ *
+ * @param data The wrapped data. Any changes to this will be reflected in this passphrase
+ */
+ public Passphrase(char[] data) {
+ this(data, 0, data.length);
+ }
+
+ /**
+ * Wraps (doesn't copy) a subarray of the given data.
+ *
+ * @param data The wrapped data. Any changes to this will be reflected in this passphrase
+ * @param offset The subarray offset, i.e. the first character of this passphrase
+ * @param length The subarray length, i.e. the length of this passphrase
+ */
+ public Passphrase(char[] data, int offset, int length) {
+ if (offset < 0 || length < 0 || offset + length > data.length) {
+ throw new IndexOutOfBoundsException("[%1$d %1$d + %2$d[ not within [0, %3$d[".formatted(offset, length, data.length));
+ }
+ this.data = data;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ public static Passphrase copyOf(CharSequence cs) {
+ char[] result = new char[cs.length()];
+ for (int i = 0; i < cs.length(); i++) {
+ result[i] = cs.charAt(i);
+ }
+ return new Passphrase(result);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Passphrase that = (Passphrase) o;
+ // time-constant comparison
+ int diff = 0;
+ for (int i = 0; i < length; i++) {
+ diff |= charAt(i) ^ that.charAt(i);
+ }
+ return diff == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ // basically Arrays.hashCode, but only for a certain subarray
+ int result = 1;
+ for (int i = 0; i < length; i++) {
+ result = 31 * result + charAt(i);
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return new String(data, offset, length);
+ }
+
+ @Override
+ public int length() {
+ return length;
+ }
+
+ @Override
+ public char charAt(int index) {
+ if (index < 0 || index >= length) {
+ throw new IndexOutOfBoundsException("%d not within [0, %d[".formatted(index, length));
+ }
+ return data[offset + index];
+ }
+
+ @Override
+ public Passphrase subSequence(int start, int end) {
+ if (start < 0 || end < 0 || end > length || start > end) {
+ throw new IndexOutOfBoundsException("[%d, %d[ not within [0, %d[".formatted(start, end, length));
+ }
+ return new Passphrase(Arrays.copyOfRange(data, offset + start, offset + end));
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return destroyed;
+ }
+
+ @Override
+ public void destroy() {
+ Arrays.fill(data, offset, offset + length, '\0');
+ destroyed = true;
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index b8d5bbff0..bc952f9d1 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -42,7 +42,7 @@ public enum FxmlFile {
this.ressourcePathString = ressourcePathString;
}
- String getRessourcePathString() {
+ public String getRessourcePathString() {
return ressourcePathString;
}
}
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java
index c10054ef4..dcff93aab 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java
@@ -22,6 +22,10 @@ public class FxmlLoaderFactory {
this.resourceBundle = resourceBundle;
}
+ public static FxmlLoaderFactory forController(T controller, Function sceneFactory, ResourceBundle resourceBundle) {
+ return new FxmlLoaderFactory(Map.of(controller.getClass(), () -> controller), sceneFactory, resourceBundle);
+ }
+
/**
* @return A new FXMLLoader instance
*/
diff --git a/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java b/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java
deleted file mode 100644
index 12c394533..000000000
--- a/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.cryptomator.ui.common;
-
-import javafx.application.Platform;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-public class UserInteractionLock> {
-
- private final Lock lock = new ReentrantLock();
- private final Condition condition = lock.newCondition();
- private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
- private final AtomicBoolean interacted = new AtomicBoolean();
- private final AtomicReference state;
-
- public UserInteractionLock(E initialValue) {
- this.state = new AtomicReference<>(initialValue);
- }
-
- public synchronized void reset(E value) {
- state.set(value);
- interacted.set(false);
- }
-
- public void interacted(E result) {
- assert Platform.isFxApplicationThread();
- lock.lock();
- try {
- state.set(result);
- interacted.set(true);
- awaitingInteraction.set(false);
- condition.signal();
- } finally {
- lock.unlock();
- }
- }
-
- public E awaitInteraction() throws InterruptedException {
- assert !Platform.isFxApplicationThread();
- lock.lock();
- try {
- Platform.runLater(() -> awaitingInteraction.set(true));
- while (!interacted.get()) {
- condition.await();
- }
- return state.get();
- } finally {
- lock.unlock();
- }
- }
-
- public ReadOnlyBooleanProperty awaitingInteraction() {
- return awaitingInteraction;
- }
-
-}
diff --git a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
index 4a4e43fff..4d09707b9 100644
--- a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
+++ b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
@@ -1,5 +1,7 @@
package org.cryptomator.ui.controls;
+import org.cryptomator.common.Passphrase;
+
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
@@ -82,7 +84,7 @@ public class NiceSecurePasswordField extends StackPane {
return passwordField.textProperty();
}
- public CharSequence getCharacters() {
+ public Passphrase getCharacters() {
return passwordField.getCharacters();
}
diff --git a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java
index 0290f512d..66df79394 100644
--- a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java
+++ b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java
@@ -9,6 +9,7 @@
package org.cryptomator.ui.controls;
import com.google.common.base.Strings;
+import org.cryptomator.common.Passphrase;
import javafx.application.Platform;
import javafx.beans.NamedArg;
@@ -28,7 +29,6 @@ import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.TransferMode;
-import java.nio.CharBuffer;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
@@ -203,8 +203,8 @@ public class SecurePasswordField extends TextField {
* @see #wipe()
*/
@Override
- public CharSequence getCharacters() {
- return CharBuffer.wrap(content, 0, length);
+ public Passphrase getCharacters() {
+ return new Passphrase(content, 0, length);
}
/**
diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java
index bff757b1a..616e7e5e0 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java
@@ -26,7 +26,7 @@ abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
- static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map> strategies) {
+ static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map> strategies) {
try {
String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java
new file mode 100644
index 000000000..a548cd47d
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java
@@ -0,0 +1,25 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import dagger.Subcomponent;
+
+import javafx.scene.Scene;
+import java.nio.file.Path;
+import java.util.concurrent.CompletableFuture;
+
+@ChooseMasterkeyFileScoped
+@Subcomponent(modules = {ChooseMasterkeyFileModule.class})
+public interface ChooseMasterkeyFileComponent {
+
+ @ChooseMasterkeyFileScoped
+ Scene chooseMasterkeyScene();
+
+ @ChooseMasterkeyFileScoped
+ CompletableFuture result();
+
+ @Subcomponent.Builder
+ interface Builder {
+
+ ChooseMasterkeyFileComponent build();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java
new file mode 100644
index 000000000..11cf7bd6b
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java
@@ -0,0 +1,57 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.keyloading.KeyLoading;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.fxml.FXML;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ResourceBundle;
+import java.util.concurrent.CompletableFuture;
+
+@ChooseMasterkeyFileScoped
+public class ChooseMasterkeyFileController implements FxController {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ChooseMasterkeyFileController.class);
+
+ private final Stage window;
+ private final CompletableFuture result;
+ private final ResourceBundle resourceBundle;
+
+ @Inject
+ public ChooseMasterkeyFileController(@KeyLoading Stage window, CompletableFuture result, ResourceBundle resourceBundle) {
+ this.window = window;
+ this.result = result;
+ this.resourceBundle = resourceBundle;
+ this.window.setOnHiding(this::windowClosed);
+ }
+
+ @FXML
+ public void cancel() {
+ window.close();
+ }
+
+ private void windowClosed(WindowEvent windowEvent) {
+ result.cancel(true);
+ }
+
+ @FXML
+ public void proceed() {
+ LOG.trace("proceed()");
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
+ File masterkeyFile = fileChooser.showOpenDialog(window);
+ if (masterkeyFile != null) {
+ LOG.debug("Chose masterkey file: {}", masterkeyFile);
+ result.complete(masterkeyFile.toPath());
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java
new file mode 100644
index 000000000..21ae2b26c
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java
@@ -0,0 +1,29 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import dagger.Module;
+import dagger.Provides;
+import org.cryptomator.ui.common.DefaultSceneFactory;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlLoaderFactory;
+
+import javafx.scene.Scene;
+import java.nio.file.Path;
+import java.util.ResourceBundle;
+import java.util.concurrent.CompletableFuture;
+
+@Module
+interface ChooseMasterkeyFileModule {
+
+ @Provides
+ @ChooseMasterkeyFileScoped
+ static CompletableFuture provideResult() {
+ return new CompletableFuture<>();
+ }
+
+ @Provides
+ @ChooseMasterkeyFileScoped
+ static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
+ return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java
new file mode 100644
index 000000000..4bf8c5c24
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java
@@ -0,0 +1,13 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import javax.inject.Scope;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Scope
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@interface ChooseMasterkeyFileScoped {
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java
deleted file mode 100644
index 44d7ebfb0..000000000
--- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.cryptomator.ui.keyloading.masterkeyfile;
-
-import org.cryptomator.common.keychain.KeychainManager;
-import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.integrations.keychain.KeychainAccessException;
-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 java.nio.CharBuffer;
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-@KeyLoadingScoped
-class MasterkeyFileLoadingFinisher {
-
- private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
-
- private final Vault vault;
- private final Optional storedPassword;
- private final AtomicReference enteredPassword;
- private final AtomicBoolean shouldSavePassword;
- private final KeychainManager keychain;
-
- @Inject
- MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional storedPassword, AtomicReference enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) {
- this.vault = vault;
- this.storedPassword = storedPassword;
- this.enteredPassword = enteredPassword;
- this.shouldSavePassword = shouldSavePassword;
- this.keychain = keychain;
- }
-
- public void cleanup(boolean successfullyUnlocked) {
- if (successfullyUnlocked && shouldSavePassword.get()) {
- savePasswordToSystemkeychain();
- }
- wipePassword(storedPassword.orElse(null));
- wipePassword(enteredPassword.getAndSet(null));
- }
-
- private void savePasswordToSystemkeychain() {
- if (keychain.isSupported()) {
- try {
- keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(enteredPassword.get()));
- } catch (KeychainAccessException e) {
- LOG.error("Failed to store passphrase in system keychain.", e);
- }
- }
- }
-
- private void wipePassword(char[] pw) {
- if (pw != null) {
- Arrays.fill(pw, ' ');
- }
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java
index 72e902ded..9375b0cff 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java
@@ -8,54 +8,17 @@ import dagger.multibindings.StringKey;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
-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.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
-import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
-import javafx.scene.Scene;
-import javafx.stage.Stage;
-import java.nio.file.Path;
import java.util.Optional;
-import java.util.ResourceBundle;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-@Module(subcomponents = {ForgetPasswordComponent.class})
-public abstract class MasterkeyFileLoadingModule {
-
- private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class);
-
- public enum PasswordEntry {
- PASSWORD_ENTERED,
- CANCELED
- }
-
- public enum MasterkeyFileProvision {
- MASTERKEYFILE_PROVIDED,
- CANCELED
- }
-
- @Provides
- @KeyLoadingScoped
- static UserInteractionLock providePasswordEntryLock() {
- return new UserInteractionLock<>(null);
- }
-
- @Provides
- @KeyLoadingScoped
- static UserInteractionLock provideMasterkeyFileProvisionLock() {
- return new UserInteractionLock<>(null);
- }
+@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class})
+public interface MasterkeyFileLoadingModule {
@Provides
@Named("savedPassword")
@@ -67,67 +30,12 @@ public abstract class MasterkeyFileLoadingModule {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
- LOG.error("Failed to load entry from system keychain.", e);
+ LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
- @Provides
- @KeyLoadingScoped
- static AtomicReference provideUserProvidedMasterkeyPath() {
- return new AtomicReference<>();
- }
-
- @Provides
- @KeyLoadingScoped
- static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) {
- return new AtomicReference<>(storedPassword.orElse(null));
- }
-
- @Provides
- @Named("savePassword")
- @KeyLoadingScoped
- static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional storedPassword) {
- return new AtomicBoolean(storedPassword.isPresent());
- }
-
- @Provides
- @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD)
- @KeyLoadingScoped
- static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
- var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
- scene.windowProperty().addListener((prop, oldVal, newVal) -> {
- if (window.equals(newVal)) {
- window.setTitle(String.format(resourceBundle.getString("unlock.title"), v.getDisplayName()));
- }
- });
- return scene;
- }
-
- @Provides
- @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
- @KeyLoadingScoped
- static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
- var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
- scene.windowProperty().addListener((prop, oldVal, newVal) -> {
- if (window.equals(newVal)) {
- window.setTitle(String.format(resourceBundle.getString("unlock.chooseMasterkey.title"), v.getDisplayName()));
- }
- });
- return scene;
- }
-
- @Binds
- @IntoMap
- @FxControllerKey(PassphraseEntryController.class)
- abstract FxController bindUnlockController(PassphraseEntryController controller);
-
- @Binds
- @IntoMap
- @FxControllerKey(SelectMasterkeyFileController.class)
- abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller);
-
@Binds
@IntoMap
@KeyLoadingScoped
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
index 1fa7dd986..b4964f9a0 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
@@ -1,32 +1,33 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
-import dagger.Lazy;
+import org.cryptomator.common.Passphrase;
+import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.common.BackupHelper;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.cryptomator.integrations.keychain.KeychainAccessException;
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 javax.inject.Named;
import javafx.application.Platform;
-import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException;
import java.net.URI;
-import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.Optional;
+import java.util.ResourceBundle;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
@KeyLoading
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@@ -36,28 +37,26 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
private final Vault vault;
private final MasterkeyFileAccess masterkeyFileAccess;
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 final PassphraseEntryComponent.Builder passphraseEntry;
+ private final ChooseMasterkeyFileComponent.Builder masterkeyFileChoice;
+ private final KeychainManager keychain;
+ private final ResourceBundle resourceBundle;
- private boolean wrongPassword;
+ private Passphrase passphrase;
+ private boolean savePassphrase;
+ private boolean wrongPassphrase;
@Inject
- public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @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) {
+ public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
this.vault = vault;
this.masterkeyFileAccess = masterkeyFileAccess;
this.window = window;
- this.passphraseEntryScene = passphraseEntryScene;
- this.selectMasterkeyFileScene = selectMasterkeyFileScene;
- this.passwordEntryLock = passwordEntryLock;
- this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
- this.password = password;
- this.filePath = filePath;
- this.finisher = finisher;
+ this.passphraseEntry = passphraseEntry;
+ this.masterkeyFileChoice = masterkeyFileChoice;
+ this.keychain = keychain;
+ this.resourceBundle = resourceBundle;
+ this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null);
+ this.savePassphrase = savedPassphrase.isPresent();
}
@Override
@@ -66,9 +65,11 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
try {
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
if (!Files.exists(filePath)) {
- filePath = getAlternateMasterkeyFilePath();
+ filePath = askUserForMasterkeyFilePath();
+ }
+ if (passphrase == null) {
+ askForPassphrase();
}
- CharSequence passphrase = getPassphrase();
var masterkey = masterkeyFileAccess.load(filePath, passphrase);
//backup
if (filePath.startsWith(vault.getPath())) {
@@ -90,8 +91,9 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@Override
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
if (exception instanceof InvalidPassphraseException) {
- this.wrongPassword = true;
- password.set(null);
+ this.wrongPassphrase = true;
+ passphrase.destroy();
+ this.passphrase = null;
return true; // reattempting key load
} else {
return false; // nothing we can do
@@ -100,23 +102,29 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@Override
public void cleanup(boolean unlockedSuccessfully) {
- finisher.cleanup(unlockedSuccessfully);
- }
-
- private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
- if (filePath.get() == null) {
- return switch (askUserForMasterkeyFilePath()) {
- case MASTERKEYFILE_PROVIDED -> filePath.get();
- case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
- };
- } else {
- return filePath.get();
+ if (unlockedSuccessfully && savePassphrase) {
+ savePasswordToSystemkeychain(passphrase);
+ }
+ if (passphrase != null) {
+ passphrase.destroy();
}
}
- private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
+ private void savePasswordToSystemkeychain(Passphrase passphrase) {
+ if (keychain.isSupported()) {
+ try {
+ keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase);
+ } catch (KeychainAccessException e) {
+ LOG.error("Failed to store passphrase in system keychain.", e);
+ }
+ }
+ }
+
+ private Path askUserForMasterkeyFilePath() throws InterruptedException {
+ var comp = masterkeyFileChoice.build();
Platform.runLater(() -> {
- window.setScene(selectMasterkeyFileScene.get());
+ window.setScene(comp.chooseMasterkeyScene());
+ window.setTitle(resourceBundle.getString("unlock.chooseMasterkey.title").formatted(vault.getDisplayName()));
window.show();
Window owner = window.getOwner();
if (owner != null) {
@@ -126,24 +134,20 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
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());
+ try {
+ return comp.result().get();
+ } catch (CancellationException e) {
+ throw new UnlockCancelledException("Choosing masterkey file cancelled.");
+ } catch (ExecutionException e) {
+ throw new MasterkeyLoadingFailedException("Failed to select masterkey file.", e);
}
}
- private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
+ private void askForPassphrase() throws InterruptedException {
+ var comp = passphraseEntry.savedPassword(passphrase).build();
Platform.runLater(() -> {
- window.setScene(passphraseEntryScene.get());
+ window.setScene(comp.passphraseEntryScene());
+ window.setTitle(resourceBundle.getString("unlock.title").formatted(vault.getDisplayName()));
window.show();
Window owner = window.getOwner();
if (owner != null) {
@@ -152,11 +156,19 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
} else {
window.centerOnScreen();
}
- if (wrongPassword) {
+ if (wrongPassphrase) {
Animations.createShakeWindowAnimation(window).play();
}
});
- return passwordEntryLock.awaitInteraction();
+ try {
+ var result = comp.result().get();
+ this.passphrase = result.passphrase();
+ this.savePassphrase = result.savePassphrase();
+ } catch (CancellationException e) {
+ throw new UnlockCancelledException("Password entry cancelled.");
+ } catch (ExecutionException e) {
+ throw new MasterkeyLoadingFailedException("Failed to ask for password.", e);
+ }
}
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java
new file mode 100644
index 000000000..5e072efd0
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java
@@ -0,0 +1,31 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import dagger.BindsInstance;
+import dagger.Subcomponent;
+import org.cryptomator.common.Nullable;
+import org.cryptomator.common.Passphrase;
+
+import javax.inject.Named;
+import javafx.scene.Scene;
+import java.util.concurrent.CompletableFuture;
+
+@PassphraseEntryScoped
+@Subcomponent(modules = {PassphraseEntryModule.class})
+public interface PassphraseEntryComponent {
+
+ @PassphraseEntryScoped
+ Scene passphraseEntryScene();
+
+ @PassphraseEntryScoped
+ CompletableFuture result();
+
+ @Subcomponent.Builder
+ interface Builder {
+
+ @BindsInstance
+ PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") Passphrase savedPassword);
+
+ PassphraseEntryComponent build();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java
index f6ce79e51..35b1b1903 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java
@@ -1,16 +1,14 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
+import org.cryptomator.common.Nullable;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.common.Passphrase;
import org.cryptomator.ui.common.WeakBindings;
-import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
-import org.cryptomator.ui.keyloading.KeyLoadingScoped;
-import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,8 +19,8 @@ import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
+import javafx.application.Platform;
import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
@@ -37,33 +35,27 @@ import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.CompletableFuture;
-@KeyLoadingScoped
+@PassphraseEntryScoped
public class PassphraseEntryController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
private final Stage window;
private final Vault vault;
- private final AtomicReference password;
- private final AtomicBoolean savePassword;
- private final Optional savedPassword;
- private final UserInteractionLock passwordEntryLock;
+ private final CompletableFuture result;
+ private final Passphrase savedPassword;
private final ForgetPasswordComponent.Builder forgetPassword;
private final KeychainManager keychain;
- private final ObjectBinding unlockButtonContentDisplay;
- private final BooleanBinding userInteractionDisabled;
- private final BooleanProperty unlockButtonDisabled;
private final StringBinding vaultName;
+ private final BooleanProperty unlockInProgress = new SimpleBooleanProperty();
+ private final ObjectBinding unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, unlockInProgress);
+ private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty();
/* FXML */
public NiceSecurePasswordField passwordField;
public CheckBox savePasswordCheckbox;
- public FontAwesome5IconView unlockInProgressView;
public ImageView face;
public ImageView leftArm;
public ImageView rightArm;
@@ -72,29 +64,25 @@ public class PassphraseEntryController implements FxController {
public Animation unlockAnimation;
@Inject
- public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
+ public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
this.window = window;
this.vault = vault;
- this.password = password;
- this.savePassword = savePassword;
+ this.result = result;
this.savedPassword = savedPassword;
- this.passwordEntryLock = passwordEntryLock;
this.forgetPassword = forgetPassword;
this.keychain = keychain;
- this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
- this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
- this.unlockButtonDisabled = new SimpleBooleanProperty();
this.vaultName = WeakBindings.bindString(vault.displayNameProperty());
- this.window.setOnHiding(this::windowClosed);
+ window.setOnHiding(this::windowClosed);
+ result.whenCompleteAsync((r, t) -> unlockInProgress.set(false), Platform::runLater);
}
@FXML
public void initialize() {
- savePasswordCheckbox.setSelected(savedPassword.isPresent());
- if (password.get() != null) {
- passwordField.setPassword(password.get());
+ if (savedPassword != null) {
+ savePasswordCheckbox.setSelected(true);
+ passwordField.setPassword(savedPassword);
}
- unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
+ unlockButtonDisabled.bind(unlockInProgress.or(passwordField.textProperty().isEmpty()));
var leftArmTranslation = new Translate(24, 0);
var leftArmRotation = new Rotate(60, 16, 30, 0);
@@ -132,7 +120,7 @@ public class PassphraseEntryController implements FxController {
new KeyFrame(Duration.millis(1000), faceVisible) //
);
- passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation());
+ result.whenCompleteAsync((r, t) -> stopUnlockAnimation());
}
@FXML
@@ -141,26 +129,17 @@ public class PassphraseEntryController implements FxController {
}
private void windowClosed(WindowEvent windowEvent) {
- // if not already interacted, mark this workflow as cancelled:
- if (passwordEntryLock.awaitingInteraction().get()) {
- LOG.debug("Unlock canceled by user.");
- passwordEntryLock.interacted(PasswordEntry.CANCELED);
- }
+ LOG.debug("Unlock canceled by user.");
+ result.cancel(true);
}
@FXML
public void unlock() {
LOG.trace("UnlockController.unlock()");
+ unlockInProgress.set(true);
CharSequence pwFieldContents = passwordField.getCharacters();
- char[] newPw = new char[pwFieldContents.length()];
- for (int i = 0; i < pwFieldContents.length(); i++) {
- newPw[i] = pwFieldContents.charAt(i);
- }
- char[] oldPw = password.getAndSet(newPw);
- if (oldPw != null) {
- Arrays.fill(oldPw, ' ');
- }
- passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
+ Passphrase pw = Passphrase.copyOf(pwFieldContents);
+ result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected()));
startUnlockAnimation();
}
@@ -184,8 +163,7 @@ public class PassphraseEntryController implements FxController {
@FXML
private void didClickSavePasswordCheckbox() {
- savePassword.set(savePasswordCheckbox.isSelected());
- if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
+ if (!savePasswordCheckbox.isSelected() && savedPassword != null) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
}
}
@@ -205,15 +183,15 @@ public class PassphraseEntryController implements FxController {
}
public ContentDisplay getUnlockButtonContentDisplay() {
- return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
+ return unlockInProgress.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
}
- public BooleanBinding userInteractionDisabledProperty() {
- return userInteractionDisabled;
+ public ReadOnlyBooleanProperty userInteractionDisabledProperty() {
+ return unlockInProgress;
}
public boolean isUserInteractionDisabled() {
- return userInteractionDisabled.get();
+ return unlockInProgress.get();
}
public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {
@@ -227,4 +205,6 @@ public class PassphraseEntryController implements FxController {
public boolean isKeychainAccessAvailable() {
return keychain.isSupported();
}
+
+
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java
new file mode 100644
index 000000000..2c65d440b
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java
@@ -0,0 +1,28 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import dagger.Module;
+import dagger.Provides;
+import org.cryptomator.ui.common.DefaultSceneFactory;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlLoaderFactory;
+
+import javafx.scene.Scene;
+import java.util.ResourceBundle;
+import java.util.concurrent.CompletableFuture;
+
+@Module
+interface PassphraseEntryModule {
+
+ @Provides
+ @PassphraseEntryScoped
+ static CompletableFuture provideResult() {
+ return new CompletableFuture<>();
+ }
+
+ @Provides
+ @PassphraseEntryScoped
+ static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
+ return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java
new file mode 100644
index 000000000..19057acca
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java
@@ -0,0 +1,8 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import org.cryptomator.common.Passphrase;
+
+// TODO: change to package-private, as soon as this works for Dagger -.-
+public record PassphraseEntryResult(Passphrase passphrase, boolean savePassphrase) {
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java
new file mode 100644
index 000000000..a077bcf81
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java
@@ -0,0 +1,13 @@
+package org.cryptomator.ui.keyloading.masterkeyfile;
+
+import javax.inject.Scope;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Scope
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@interface PassphraseEntryScoped {
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java
deleted file mode 100644
index 39be2b36e..000000000
--- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.cryptomator.ui.keyloading.masterkeyfile;
-
-import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.common.UserInteractionLock;
-import org.cryptomator.ui.keyloading.KeyLoading;
-import org.cryptomator.ui.keyloading.KeyLoadingScoped;
-import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javafx.fxml.FXML;
-import javafx.stage.FileChooser;
-import javafx.stage.Stage;
-import javafx.stage.WindowEvent;
-import java.io.File;
-import java.nio.file.Path;
-import java.util.ResourceBundle;
-import java.util.concurrent.atomic.AtomicReference;
-
-@KeyLoadingScoped
-public class SelectMasterkeyFileController implements FxController {
-
- private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
-
- private final Stage window;
- private final AtomicReference masterkeyPath;
- private final UserInteractionLock masterkeyFileProvisionLock;
- private final ResourceBundle resourceBundle;
-
- @Inject
- public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference masterkeyPath, UserInteractionLock masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
- this.window = window;
- this.masterkeyPath = masterkeyPath;
- this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
- this.resourceBundle = resourceBundle;
- this.window.setOnHiding(this::windowClosed);
- }
-
- @FXML
- public void cancel() {
- window.close();
- }
-
- private void windowClosed(WindowEvent windowEvent) {
- // if not already interacted, mark this workflow as cancelled:
- if (masterkeyFileProvisionLock.awaitingInteraction().get()) {
- LOG.debug("Unlock canceled by user.");
- masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED);
- }
- }
-
- @FXML
- public void proceed() {
- LOG.trace("proceed()");
- FileChooser fileChooser = new FileChooser();
- fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
- fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
- File masterkeyFile = fileChooser.showOpenDialog(window);
- if (masterkeyFile != null) {
- LOG.debug("Chose masterkey file: {}", masterkeyFile);
- masterkeyPath.set(masterkeyFile.toPath());
- masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED);
- }
- }
-
-}
diff --git a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java
index c3a452acc..15cf119be 100644
--- a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java
+++ b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java
@@ -2,56 +2,48 @@ package org.cryptomator.ui.lock;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.common.UserInteractionLock;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
@LockScoped
public class LockForcedController implements FxController {
- private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class);
-
private final Stage window;
private final Vault vault;
- private final UserInteractionLock forceLockDecisionLock;
+ private final AtomicReference> forceRetryDecision;
@Inject
- public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock) {
+ public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, AtomicReference> forceRetryDecision) {
this.window = window;
this.vault = vault;
- this.forceLockDecisionLock = forceLockDecisionLock;
+ this.forceRetryDecision = forceRetryDecision;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
- forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
window.close();
}
@FXML
public void retry() {
- forceLockDecisionLock.interacted(LockModule.ForceLockDecision.RETRY);
+ forceRetryDecision.get().complete(false);
window.close();
}
@FXML
public void force() {
- forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE);
+ forceRetryDecision.get().complete(true);
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
- // if not already interacted, set the decision to CANCEL
- if (forceLockDecisionLock.awaitingInteraction().get()) {
- LOG.debug("Lock canceled in force-lock-phase by user.");
- forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
- }
+ forceRetryDecision.get().cancel(true);
}
// ----- Getter & Setter -----
diff --git a/src/main/java/org/cryptomator/ui/lock/LockModule.java b/src/main/java/org/cryptomator/ui/lock/LockModule.java
index d1eb5f189..ddee13dff 100644
--- a/src/main/java/org/cryptomator/ui/lock/LockModule.java
+++ b/src/main/java/org/cryptomator/ui/lock/LockModule.java
@@ -6,13 +6,12 @@ import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
-import org.cryptomator.ui.common.FxmlLoaderFactory;
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.StageFactory;
-import org.cryptomator.ui.common.UserInteractionLock;
import javax.inject.Named;
import javax.inject.Provider;
@@ -22,20 +21,16 @@ import javafx.stage.Stage;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
@Module
abstract class LockModule {
- enum ForceLockDecision {
- CANCEL,
- RETRY,
- FORCE;
- }
-
@Provides
@LockScoped
- static UserInteractionLock provideForceLockDecisionLock() {
- return new UserInteractionLock<>(null);
+ static AtomicReference> provideForceRetryDecisionRef() {
+ return new AtomicReference<>();
}
@Provides
diff --git a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
index 00b25c507..1e05ceb73 100644
--- a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
+++ b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
@@ -8,7 +8,6 @@ import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
-import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,6 +17,10 @@ import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
/**
* The sequence of actions performed and checked during lock of a vault.
@@ -34,43 +37,48 @@ public class LockWorkflow extends Task {
private final Stage lockWindow;
private final Vault vault;
- private final UserInteractionLock forceLockDecisionLock;
+ private final AtomicReference> forceRetryDecision;
private final Lazy lockForcedScene;
private final Lazy lockFailedScene;
private final ErrorComponent.Builder errorComponent;
@Inject
- public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene, ErrorComponent.Builder errorComponent) {
+ public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, AtomicReference> forceRetryDecision, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene, ErrorComponent.Builder errorComponent) {
this.lockWindow = lockWindow;
this.vault = vault;
- this.forceLockDecisionLock = forceLockDecisionLock;
+ this.forceRetryDecision = forceRetryDecision;
this.lockForcedScene = lockForcedScene;
this.lockFailedScene = lockFailedScene;
this.errorComponent = errorComponent;
}
@Override
- protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
+ protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException {
lock(false);
return null;
}
- private void lock(boolean forced) throws InterruptedException {
+ private void lock(boolean forced) throws InterruptedException, ExecutionException {
try {
vault.lock(forced);
} catch (Volume.VolumeException | LockNotCompletedException e) {
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
- var decision = askUserForAction();
- switch (decision) {
- case RETRY -> lock(false);
- case FORCE -> lock(true);
- case CANCEL -> cancel(false);
- }
+ retryOrCancel();
}
}
- private LockModule.ForceLockDecision askUserForAction() throws InterruptedException {
- forceLockDecisionLock.reset(null);
+ private void retryOrCancel() throws ExecutionException, InterruptedException {
+ try {
+ boolean forced = askWhetherToUseTheForce().get();
+ lock(forced);
+ } catch (CancellationException e) {
+ cancel(false);
+ }
+ }
+
+ private CompletableFuture askWhetherToUseTheForce() {
+ var decision = new CompletableFuture();
+ forceRetryDecision.set(decision);
// show forcedLock dialogue ...
Platform.runLater(() -> {
lockWindow.setScene(lockForcedScene.get());
@@ -83,8 +91,7 @@ public class LockWorkflow extends Task {
lockWindow.centerOnScreen();
}
});
- // ... and wait for answer
- return forceLockDecisionLock.awaitInteraction();
+ return decision;
}
@Override
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
index 4ce3808c4..dd08d5dc0 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
@@ -91,6 +91,8 @@ class TrayMenuController {
unlockItem.addActionListener(createActionListenerForVault(vault, this::unlockVault));
submenu.add(unlockItem);
} else if (vault.isUnlocked()) {
+ submenu.setLabel("* ".concat(submenu.getLabel()));
+
MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock"));
lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault));
submenu.add(lockItem);
diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
index 3be38568d..b7b01fdd6 100644
--- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
+++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
@@ -24,6 +24,7 @@ import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.io.File;
+import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ResourceBundle;
@@ -122,8 +123,11 @@ public class MountOptionsController implements FxController {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
try {
- var initialDir = vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home"));
- directoryChooser.setInitialDirectory(Path.of(initialDir).toFile());
+ var initialDir = Path.of(vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home")));
+
+ if(Files.exists(initialDir)) {
+ directoryChooser.setInitialDirectory(initialDir.toFile());
+ }
} catch (InvalidPathException e) {
// no-op
}
diff --git a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml
index b6539f88f..d37289fca 100644
--- a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml
+++ b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml
@@ -11,7 +11,7 @@
-
+
diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties
index 36932b877..6648496b9 100644
--- a/src/main/resources/i18n/strings.properties
+++ b/src/main/resources/i18n/strings.properties
@@ -106,6 +106,7 @@ unlock.unlockBtn=Unlock
## Select
unlock.chooseMasterkey.title=Select Masterkey of "%s"
unlock.chooseMasterkey.prompt=Could not find the masterkey file for this vault at its expected location. Please choose the key file manually.
+unlock.chooseMasterkey.chooseBtn=Choose…
unlock.chooseMasterkey.filePickerTitle=Select Masterkey File
## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible via its virtual drive.
diff --git a/src/main/resources/license/THIRD-PARTY.txt b/src/main/resources/license/THIRD-PARTY.txt
index 9c9d92c7d..fb8e73f69 100644
--- a/src/main/resources/license/THIRD-PARTY.txt
+++ b/src/main/resources/license/THIRD-PARTY.txt
@@ -17,9 +17,9 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
- jnr-constants (com.github.jnr:jnr-constants:0.10.2 - http://github.com/jnr/jnr-constants)
- jnr-ffi (com.github.jnr:jnr-ffi:2.2.7 - http://github.com/jnr/jnr-ffi)
- - Dagger (com.google.dagger:dagger:2.40.3 - https://github.com/google/dagger)
+ - Dagger (com.google.dagger:dagger:2.41 - https://github.com/google/dagger)
- Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess)
- - Guava: Google Core Libraries for Java (com.google.guava:guava:31.0-jre - https://github.com/google/guava)
+ - Guava: Google Core Libraries for Java (com.google.guava:guava:31.1-jre - https://github.com/google/guava)
- Apache Commons CLI (commons-cli:commons-cli:1.4 - http://commons.apache.org/proper/commons-cli/)
- javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/)
- Apache Commons Lang (org.apache.commons:commons-lang3:3.12.0 - https://commons.apache.org/proper/commons-lang/)
@@ -33,7 +33,7 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
- Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - https://eclipse.org/jetty/jetty-servlet-api)
Apache-2.0:
- - Gson (com.google.code.gson:gson:2.8.9 - https://github.com/google/gson/gson)
+ - Gson (com.google.code.gson:gson:2.9.0 - https://github.com/google/gson/gson)
- Java Native Access (net.java.dev.jna:jna:5.9.0 - https://github.com/java-native-access/jna)
- Java Native Access Platform (net.java.dev.jna:jna-platform:5.9.0 - https://github.com/java-native-access/jna)
BSD-3-Clause:
@@ -52,31 +52,31 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - https://eclipse.org/jetty/jetty-servlet)
- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
Eclipse Public License - v 1.0:
- - Logback Classic Module (ch.qos.logback:logback-classic:1.2.9 - http://logback.qos.ch/logback-classic)
- - Logback Core Module (ch.qos.logback:logback-core:1.2.9 - http://logback.qos.ch/logback-core)
+ - Logback Classic Module (ch.qos.logback:logback-classic:1.2.11 - http://logback.qos.ch/logback-classic)
+ - Logback Core Module (ch.qos.logback:logback-core:1.2.11 - http://logback.qos.ch/logback-core)
Eclipse Public License - v 2.0:
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
GNU Lesser General Public License:
- - Logback Classic Module (ch.qos.logback:logback-classic:1.2.9 - http://logback.qos.ch/logback-classic)
- - Logback Core Module (ch.qos.logback:logback-core:1.2.9 - http://logback.qos.ch/logback-core)
+ - Logback Classic Module (ch.qos.logback:logback-classic:1.2.11 - http://logback.qos.ch/logback-classic)
+ - Logback Core Module (ch.qos.logback:logback-core:1.2.11 - http://logback.qos.ch/logback-core)
GPLv2:
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
GPLv2+CE:
- - javafx-base (org.openjfx:javafx-base:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-base/)
- - javafx-controls (org.openjfx:javafx-controls:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
- - javafx-fxml (org.openjfx:javafx-fxml:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
- - javafx-graphics (org.openjfx:javafx-graphics:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
+ - javafx-base (org.openjfx:javafx-base:18 - https://openjdk.java.net/projects/openjfx/javafx-base/)
+ - javafx-controls (org.openjfx:javafx-controls:18 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
+ - javafx-fxml (org.openjfx:javafx-fxml:18 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
+ - javafx-graphics (org.openjfx:javafx-graphics:18 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
LGPL 2.1:
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
LGPL-2.1-or-later:
- Java Native Access (net.java.dev.jna:jna:5.9.0 - https://github.com/java-native-access/jna)
- Java Native Access Platform (net.java.dev.jna:jna-platform:5.9.0 - https://github.com/java-native-access/jna)
MIT License:
- - java jwt (com.auth0:java-jwt:3.18.2 - https://github.com/auth0/java-jwt)
+ - java jwt (com.auth0:java-jwt:3.19.0 - https://github.com/auth0/java-jwt)
- jnr-x86asm (com.github.jnr:jnr-x86asm:1.0.2 - http://github.com/jnr/jnr-x86asm)
- jnr-fuse (com.github.serceman:jnr-fuse:0.5.7 - https://github.com/SerCeMan/jnr-fuse)
- zxcvbn4j (com.nulab-inc:zxcvbn:1.5.2 - https://github.com/nulab/zxcvbn4j)
- - SLF4J API Module (org.slf4j:slf4j-api:1.7.32 - http://www.slf4j.org)
+ - SLF4J API Module (org.slf4j:slf4j-api:1.7.36 - http://www.slf4j.org)
The BSD 2-Clause License:
- EasyBind (com.tobiasdiez:easybind:2.2 - https://github.com/tobiasdiez/EasyBind)
diff --git a/src/test/java/org/cryptomator/common/PassphraseTest.java b/src/test/java/org/cryptomator/common/PassphraseTest.java
new file mode 100644
index 000000000..02f640e94
--- /dev/null
+++ b/src/test/java/org/cryptomator/common/PassphraseTest.java
@@ -0,0 +1,129 @@
+package org.cryptomator.common;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class PassphraseTest {
+
+ @ParameterizedTest
+ @CsvSource(value = {
+ "-1, 0",
+ "0, -1",
+ "0, 10",
+ "10, 0",
+ "10, 10"
+ })
+ public void testInvalidConstructorArgs(int offset, int length) {
+ char[] data = "test".toCharArray();
+ Assertions.assertThrows(IndexOutOfBoundsException.class, () -> {
+ new Passphrase(data, offset, length);
+ });
+ }
+
+ @ParameterizedTest
+ @CsvSource(value = {
+ "0, 4",
+ "0, 0",
+ "0, 1",
+ "1, 1",
+ "2, 2"
+ })
+ public void testValidConstructorArgs(int offset, int length) {
+ char[] data = "test".toCharArray();
+ var pw = new Passphrase(data, offset, length);
+ Assertions.assertEquals(length, pw.length());
+ Assertions.assertEquals("test".substring(offset, offset + length), pw.toString());
+ }
+
+ @Nested
+ public class InstanceMethods {
+
+ private Passphrase pw1;
+ private Passphrase pw2;
+
+ @BeforeEach
+ public void setup() {
+ char[] foo = "test test".toCharArray();
+ pw1 = new Passphrase(foo, 5, 4);
+ pw2 = Passphrase.copyOf("test");
+ }
+
+ @Test
+ public void testToString() {
+ Assertions.assertEquals("test", pw1.toString());
+ Assertions.assertEquals("test", pw2.toString());
+ }
+
+ @Test
+ public void testEquals() {
+ Assertions.assertEquals(pw1, pw2);
+ }
+
+ @Test
+ public void testHashcode() {
+ Assertions.assertEquals(pw1.hashCode(), pw2.hashCode());
+ }
+
+ @Test
+ public void testLength() {
+ Assertions.assertEquals(4, pw1.length());
+ Assertions.assertEquals(4, pw2.length());
+ }
+
+ @Test
+ public void testCharAt() {
+ Assertions.assertEquals('s', pw1.charAt(2));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {-1, 4, 5})
+ public void testInvalidCharAt(int idx) {
+ Assertions.assertThrows(IndexOutOfBoundsException.class, () -> pw1.charAt(idx));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2, 3})
+ public void testValidCharAt(int idx) {
+ Assertions.assertEquals("test".charAt(idx), pw1.charAt(idx));
+ }
+
+ @ParameterizedTest
+ @CsvSource(value = {
+ "-1, 0",
+ "0, -1",
+ "-1, -1",
+ "0, 5",
+ "3, 2"
+ })
+ public void testInvalidSubSequence(int start, int end) {
+ Assertions.assertThrows(IndexOutOfBoundsException.class, () -> pw1.subSequence(start, end));
+ }
+
+ @ParameterizedTest
+ @CsvSource(value = {
+ "0, 4",
+ "1, 4",
+ "0, 2",
+ "2, 4",
+ "4, 4",
+ })
+ public void testValidSubSequence(int start, int end) {
+ Assertions.assertEquals("test".substring(start, end), pw1.subSequence(start, end).toString());
+ }
+
+ @Test
+ public void testDestroy() {
+ pw2.destroy();
+ Assertions.assertFalse(pw1.isDestroyed());
+ Assertions.assertTrue(pw2.isDestroyed());
+ Assertions.assertNotEquals(pw1, pw2);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
index e82e67e2d..aa8b6e1f3 100644
--- a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
+++ b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
@@ -2,7 +2,9 @@ package org.cryptomator.common.keychain;
import org.cryptomator.integrations.keychain.KeychainAccessException;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@@ -32,7 +34,8 @@ public class KeychainManagerTest {
public static void startup() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Platform.startup(latch::countDown);
- latch.await(5, TimeUnit.SECONDS);
+ var javafxStarted = latch.await(5, TimeUnit.SECONDS);
+ Assumptions.assumeTrue(javafxStarted);
}
@Test
diff --git a/src/test/java/org/cryptomator/logging/LaunchBasedTriggeringPolicyTest.java b/src/test/java/org/cryptomator/logging/LaunchBasedTriggeringPolicyTest.java
index 16385d949..0449bd5e2 100644
--- a/src/test/java/org/cryptomator/logging/LaunchBasedTriggeringPolicyTest.java
+++ b/src/test/java/org/cryptomator/logging/LaunchBasedTriggeringPolicyTest.java
@@ -31,8 +31,8 @@ public class LaunchBasedTriggeringPolicyTest {
triggered = policy.isTriggeringEvent(activeFile, event);
Assertions.assertFalse(triggered);
- Mockito.verifyZeroInteractions(activeFile);
- Mockito.verifyZeroInteractions(event);
+ Mockito.verifyNoInteractions(activeFile);
+ Mockito.verifyNoInteractions(event);
}
}
diff --git a/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java b/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
index 865966049..bfe31816e 100644
--- a/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
+++ b/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
@@ -1,5 +1,6 @@
package org.cryptomator.ui.controls;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
@@ -8,7 +9,6 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import javafx.application.Platform;
-import java.awt.GraphicsEnvironment;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -18,13 +18,10 @@ public class SecurePasswordFieldTest {
@BeforeAll
public static void initJavaFx() throws InterruptedException {
- Assumptions.assumeFalse(GraphicsEnvironment.isHeadless());
- final CountDownLatch latch = new CountDownLatch(1);
+ CountDownLatch latch = new CountDownLatch(1);
Platform.startup(latch::countDown);
-
- if (!latch.await(5L, TimeUnit.SECONDS)) {
- throw new ExceptionInInitializerError();
- }
+ var javafxStarted = latch.await(5, TimeUnit.SECONDS);
+ Assumptions.assumeTrue(javafxStarted);
}
@Nested
diff --git a/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java b/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java
index 0df21f6c0..c9061451e 100644
--- a/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java
+++ b/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java
@@ -11,13 +11,12 @@ import org.mockito.Mockito;
import java.io.IOException;
import java.nio.file.Path;
-import java.security.SecureRandom;
public class RecoveryKeyFactoryTest {
- private WordEncoder wordEncoder = new WordEncoder();
- private MasterkeyFileAccess masterkeyFileAccess = Mockito.mock(MasterkeyFileAccess.class);
- private RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder, masterkeyFileAccess);
+ private final WordEncoder wordEncoder = new WordEncoder();
+ private final MasterkeyFileAccess masterkeyFileAccess = Mockito.mock(MasterkeyFileAccess.class);
+ private final RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder, masterkeyFileAccess);
@Test
@DisplayName("createRecoveryKey() creates 44 words")